You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 18:00:31