9.3. 关于括号

这里,来聊聊本章已多次涉及圆括号的话题。现在,我们将展示本书中可能最重要的那对圆括号 —— 请看以下两个函数:

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::value检查T是否为引用类型,并通过is_lvalue_reference::value进一步区分左值/右值引用。

void printType (int&&) void printType (int&)
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 "
表9.1:不同printType实例化的汇编代码对比

我们可以清晰地看到,针对两个函数返回的不同类型,编译器分别生成了printType函数的两个实例化版本。所有类型特征(type traits)的调用都在源代码层面得到了完美实现,从而消除了不必要的条件分支。此外,编译器还优化移除了未使用的字符串(比如生成的汇编代码中完全找不到"rvalue ref"字样,因为编译器已识别出相关分支在最终代码中永远不会被执行)。

这难道不是C++精妙之处的绝佳体现吗?