这里,来聊聊本章已多次涉及圆括号的话题。现在,我们将展示本书中可能最重要的那对圆括号 —— 请看以下两个函数:
static int y;
decltype(auto) number(int x) {
return y;
}
decltype(auto) reference(int x) {
return (y);
}
这两个函数看似几乎相同,唯一的区别在于返回值周围那对微小的圆括号。但正是这对括号造成了天壤之别。C++14引入的decltype(auto)是一个结合了decltype和自动类型推导的类型说明符,它既能根据初始化表达式推导类型,又能保留表达式的值类别特性(如引用或非引用)。与auto基于值类别推导类型不同,decltype(auto)会忠实保留原始表达式的值类别。
简而言之:number()函数返回int,而reference()函数返回int&。
为验证上述结论,以下代码片段可作有力佐证:
using namespace std;
if (is_reference<decltype(number(42))>::value) {
cout << "Reference to ";
cout << typeid(typename
remove_reference<decltype(number(42))>::type).name() << endl;
} else {
cout << "Not a reference: " << typeid(decltype(number(42))).name() << endl;
}
上述代码片段检测了number函数的返回类型。正如其名称直白暗示的那样,它返回的当然是...一个数字。当使用MSVC编译并执行时,输出如下:
Not a reference: int
其他编译器的行为基本一致,只不过它们不会打印完整类型名称 —— 比如gcc和clang对于int类型仅显示单个字母i,这就没那么直观了。
检验以下代码:
if (is_reference<decltype(reference(42))>::value) {
cout << "Reference to: ";
cout << typeid(typename
remove_reference<decltype(reference(42))>::type).name() << endl;
} else {
cout << "Not a reference: " << typeid(decltype(number(42))).name() << endl;
}
这段代码与前例几乎完全相同,只是改用了reference方法。不出所料,执行结果(再次以MSVC为例)如下:
Reference to: int
至此我们已证明:一对括号与decltype(auto)结合能产生惊人的效果。但请注意,如果省略decltype,像下面这样:
auto reference(int x) {
return (y);
}
此时编译器会忽略括号,直接返回普通数值。C++标准在[dcl.type.decltype]章节明确规定了这一行为,笔者强烈建议阅读该章节,以透彻理解背后的原理及其合理性。
既然C++开发者始终追求高效、优质和清晰的代码,您可能会问:为何要通过重复代码来判断返回类型是否为引用?难道不能直接写成下面这样吗?
template <typename T>
void printType(T&& var) {
if (std::is_reference<T>::value) {
if (std::is_lvalue_reference<T>::value) {
printf("lvalue ref ");
} else {
printf("rvalue ref ");
}
printf("
std::remove_reference<T>::type).name()));
} else {
printf("
}
}
这段代码与前前段("前前段"即当前参照点往前数第二段)几乎相同,只是增加了验证引用类型的额外检查(同时将std::cout替换为printf以生成更简洁的汇编代码,并封装为函数体)。让我们将其置于以下上下文并调用:
printType(number(42));
printType(reference(42));
将得到符合预期的正确输出:
int
lvalue ref int
值得一提的是,其他非"小巧软萌"的编译器也能得到相同结果。
该函数模板通过转发引用(T&& var
)同时处理左值和右值引用,从而能够推导并保留传入变量的引用类型。借助类型特征库,使用is_reference
检查T是否为引用类型,并通过is_lvalue_reference
进一步区分左值/右值引用。
void printType |
void printType |
---|---|
push rbp mov rbp, rsp sub rsp, 16mov QWORD PTR [rbp-8], rdi mov edi, OFFSET FLAT:typeinfo call std::type_ info::name() mov rdi, rax call puts nop leave ret |
push rbp mov rbp, rsp sub rsp, 16mov QWORD PTR [rbp-8], rdi mov edi, OFFSET FLAT:.LC0 mov eax, 0 call printf mov edi, OFFSET FLAT:typeinfo call std::type_info::name() mov rdi, rax call puts nop leave ret .LC0: .string "lvalue ref " |
我们可以清晰地看到,针对两个函数返回的不同类型,编译器分别生成了printType函数的两个实例化版本。所有类型特征(type traits)的调用都在源代码层面得到了完美实现,从而消除了不必要的条件分支。此外,编译器还优化移除了未使用的字符串(比如生成的汇编代码中完全找不到"rvalue ref"字样,因为编译器已识别出相关分支在最终代码中永远不会被执行)。
这难道不是C++精妙之处的绝佳体现吗?