C++检测惯用语示例中std::declval与未求值表达式的作用问询
咱们先把原示例代码贴出来,方便对照理解:
// 主模板处理不支持前置自增的类型 template< class, class = void_t<> > struct has_pre_increment_member : false_type { }; // 特化版本识别支持前置自增的类型 template< class T > struct has_pre_increment_member<T, void_t<decltype( ++declval<T&>() )>> : true_type { };
接下来咱们拆解核心的两个部分,一步步搞懂它们的运作逻辑:
一、未求值表达式:只做类型合法性检查,不会真执行代码
这里的关键是decltype()操作符——它属于未求值上下文。简单说就是:decltype(...)括号里的表达式,编译器根本不会去实际执行它,只会干两件事:判断这个表达式是否合法,以及推导它的结果类型。
拿示例里的decltype( ++declval<T&>() )来说,编译器不会真的去构造一个T对象、再调用++操作,它只会检查:如果给一个T的左值引用执行前置自增,这个操作能不能成立?比如T有没有重载operator++()成员函数,或者有没有合适的全局operator++重载。如果合法,decltype就能拿到这个操作的返回类型;如果不合法,这个表达式会触发SFINAE(替换失败不是错误)规则,直接把对应的特化模板从候选列表里排除掉。
二、std::declval:不用构造对象,就能“模拟”出类型的引用
那std::declval<T&>又是干啥的?
你想啊,如果我们直接写++T()来检查,那T必须要有默认构造函数,不然T()这一步就会编译报错。但很多类型可能没有默认构造函数,或者构造代价极高,这时候std::declval就解决了这个痛点——它是标准库提供的一个模板,只需要声明不需要定义(因为它只在未求值上下文里用),作用是生成一个指定类型的引用,完全不需要构造该类型的对象。
declval<T&>会生成T的左值引用,这样我们就能模拟对T左值的操作(比如前置自增通常是针对左值的);如果用declval<T>,生成的就是右值引用。有了它,不管T有没有默认构造函数,我们都能“假装”有一个T类型的对象,用来验证对应的操作是否合法。
三、结合示例看完整流程
现在把两者结合起来,看整个类型检测模板的工作逻辑:
- 当我们使用
has_pre_increment_member<SomeType>::value时,编译器会优先尝试匹配更具体的特化模板。 - 特化模板的第二个参数是
void_t<decltype( ++declval<T&>() )>:- 如果
++declval<T&>()是合法表达式(也就是SomeType支持前置自增),decltype能推导出有效类型,void_t会把这个类型转换成void,此时特化模板的第二个参数和主模板的默认参数void_t<>(本质就是void)匹配,特化模板生效,value就是true。 - 如果
++declval<T&>()不合法(SomeType不支持前置自增),这个表达式触发SFINAE,特化模板被排除,编译器就会选择主模板,value就是false。
- 如果
内容的提问来源于stack exchange,提问作者user9196120

