std::visit使用开销对比及虚函数调用成本差异技术问询
关于std::variant与直接对象调用的开销问题
#include <fmt/core.h> #include <string> #include <array> #include <variant> struct Student { std::string name; Student(const char* name) : name(name) {} void display() { fmt::print("Student {}\n", name); } virtual void play() { fmt::print("Student {} played\n", name); } }; struct Teacher { std::string name; Teacher(const char* name) : name(name) {} void display() { fmt::print("Teacher {}\n", name); } virtual void play() { fmt::print("Teacher {} played\n", name); } }; int main() { // case 1: use Student Student student{"Alice"}; student.display(); // case 2: use std::variant which holds a Student std::variant<Student, Teacher> person(std::in_place_type<Student>, "Charlie"); std::visit([](auto&& arg) { arg.display(); }, person); }
问题1:std::variant+std::visit的额外开销是什么?是不是仅通过类型索引做cast后调用?
和直接调用Student::display的case1相比,case2的额外开销非常有限:
- 首先,
std::variant会存储一个整数类型索引,std::visit运行时会读取这个索引,然后通过编译期生成的跳转表(相当于预先编译好的switch分支)跳转到对应类型的lambda实例。这就是多出来的核心步骤:读取索引+一次分支跳转,开销极小。 - 其次,这里不是简单的
reinterpret_cast。std::variant内部用对齐的union存储对象,std::visit会根据索引安全地转换到对应类型,整个过程是编译期验证过的,不存在未定义行为。case1是编译期直接绑定的函数调用,完全没有运行时额外开销;case2多的那点开销可以忽略,远小于虚函数调用的成本。
问题2:改成调用虚函数play,成本会有差异吗?
会存在差异,具体情况如下:
- case1:调用
student.play()是标准的虚函数调用流程——读取对象的虚函数表指针(vptr),再通过vptr找到虚函数表(vtable)中play的地址,最后执行间接跳转。这是一次内存读取+间接跳转的开销。 - case2:在
std::visit的lambda里,arg的静态类型是明确的Student&或Teacher&(因为std::visit已经确定了当前variant持有的具体类型),因此调用arg.play()时,编译器可以直接绑定到Student::play或Teacher::play的具体实现,不需要走虚函数表的间接跳转。此时case2的开销是std::visit的索引读取+分支跳转,加上直接函数调用。
如果编译器开启了O2这类优化,std::visit的分支跳转甚至可能被完全优化掉,此时两者的开销几乎没有区别;未开启优化时,case2会比case1多一点点分支跳转的开销。
内容的提问来源于stack exchange,提问作者doraemon
相关产品推荐
相关产品推荐

