关于cppreference中std::visit的overloaded辅助模板示例的疑问
核心概念解释
1. 这里的operator()指代的是什么
operator()是C++中的函数调用运算符,重载了该运算符的类/结构体对象可以像普通函数一样被调用,这类对象也被称为仿函数或函数对象。
你看到的lambda表达式本质就是编译器自动生成的匿名仿函数类型:每写一个lambda,编译器都会生成一个唯一的、重载了operator()的结构体类型,lambda的捕获参数就是这个结构体的成员变量,lambda的函数体就是这个operator()的实现。比如你写的[](double arg) { std::cout << std::fixed << arg << ' '; },对应的匿名类大概长这样:
struct __lambda_unique_name { // 无捕获所以没有成员变量 void operator()(double arg) const { std::cout << std::fixed << arg << ' '; } };
所以示例里提到的Ts::operator(),指的就是每个lambda对应匿名类里的这个函数调用运算符。
2. 三个lambda类型作为overloaded基类的原理
整个overloaded的实现用到了三个C核心特性:可变参数模板、多重继承、名字注入,外加C17的类模板实参推导,我们拆解开看:
第一步:可变参数模板接收任意多个类型
template<class... Ts> struct overloaded : Ts... 是一个可变参数模板类,你传入多少个类型参数,它就会从多少个基类继承。比如你传入三个lambda对应的类型L1, L2, L3,那么overloaded<L1, L2, L3>就同时继承这三个lambda的匿名类。
第二步:名字注入把所有基类的operator()引入派生类作用域
using Ts::operator()...; 是C++17的折叠表达式写法,作用是把所有基类的operator()函数都引入到overloaded派生类的作用域里:
- 如果没有这行代码,派生类默认会隐藏基类的同名函数,即使基类的
operator()参数列表不一样也会被隐藏,无法实现重载效果 - 加了这行之后,三个基类的
operator()就都暴露在派生类里,变成了同一个作用域下的重载函数,编译器会根据调用时传入的参数类型自动匹配最合适的重载版本,这也是为什么std::visit能根据variant当前持有的类型自动选对lambda的原因。
第三步:类模板实参推导自动推导类型
你写的overloaded { lambda1, lambda2, lambda3 }是C++17的聚合初始化+类模板实参推导的用法:
- 首先
overloaded是聚合类(没有自定义构造函数、所有基类都是公开的),可以直接用大括号把基类的对象传进去初始化 - 后面的推导指引
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;的作用是告诉编译器:你传入了多少个参数,参数类型是什么,就把这些类型作为overloaded的模板参数。比如你传入三个lambda,编译器就会自动推导出overloaded<L1, L2, L3>这个具体类型,不需要你手动写模板参数。C++20之后因为新增了聚合类的模板实参推导规则,这个显式推导指引就可以省略了。
补充你示例里提到的隐式转换问题:因为重载决议的优先级是「精确匹配 > 提升转换 > 标准转换 > 用户自定义转换」,
auto写的泛型lambda的operator()是模板函数,传入int的时候会直接生成精确匹配int的实例,所以优先级比需要做隐式转换的double版本高。如果删掉泛型lambda,传入int就会匹配到double版本,这就是文档里提示要注意隐式转换的原因。
内容的提问来源于stack exchange,提问作者Dean

