本文基于《Effective Modern C++》前四个条款总结了 C++ 的类型推导规则。理解这些规则对于编写现代 C++ 代码至关重要。

条款一:理解模板类型推导

模板类型推导是理解 auto 类型推导的基础。对于函数模板:

C++

 template<typename T>
 void f(ParamType param);
 f(expr); // 从 expr 推导 T 和 ParamType

类型推导分为三种情景:

情景一:ParamType 是指针或引用(非通用引用)

  • 规则

    1. expr 是引用,忽略引用部分。

    2. 利用 expr 的类型与 ParamType 进行模式匹配,决定 T

  • 示例

    C++

     template<typename T> void f(T& param);
     int x = 27;
     const int cx = x;
     const int& rx = x;
     ​
     f(x);  // T 是 int, param 是 int&
     f(cx); // T 是 const int, param 是 const int&
     f(rx); // T 是 const int, param 是 const int& (rx 的引用被忽略)

    ParamTypeconst T&,则 const 不再被推导为 T 的一部分。

情景二:ParamType 是通用引用 (T&&)

  • 规则

    • expr 是左值,TParamType 均被推导为左值引用(这是 T 被推导为引用的唯一情形)。

    • expr 是右值,应用正常推导规则(即情景一规则)。

  • 示例

    C++

     template<typename T> void f(T&& param);
     int x = 27;
     f(x);  // x 是左值 -> T 是 int&, param 是 int&
     f(27); // 27 是右值 -> T 是 int, param 是 int&&

情景三:ParamType 既非指针也非引用(传值)

  • 规则

    1. expr 是引用,忽略引用部分。

    2. 忽略 expr 的顶层 constvolatile

  • 示例

    C++

     template<typename T> void f(T param);
     const int cx = x;
     const int& rx = x;
     f(cx); // T 和 param 都是 int (const 被忽略)
     f(rx); // T 和 param 都是 int (引用和 const 都被忽略)

    注意:若 exprconst char* const(顶层 const 指针指向 const 数据),传值时顶层 const 被忽略,底层 const 保留,即推导为 const char*

特殊情况:数组与函数实参

  • 数组退化:数组传递给传值形参时会退化为指针;传递给引用形参时推导为数组引用(保留大小信息)。

    C++

     const char name[] = "J. P. Briggs";
     template<typename T> void f(T param);
     f(name); // T 推导为 const char*
     ​
     template<typename T> void f(T& param);
     f(name); // T 推导为 const char[13]
  • 函数退化:同理,函数传值时退化为函数指针,传引用时推导为函数引用。


条款二:理解 auto 类型推导

auto 类型推导与模板类型推导机制几乎完全一致,存在直接映射关系:auto 对应模板中的 T,类型说明符对应 ParamType

映射关系

  • auto x = 27; 对应情景三(非指针非引用)。

  • const auto& rx = x; 对应情景一(非通用引用)。

  • auto&& uref = x; 对应情景二(通用引用)。

唯一的例外:花括号初始化

  • 差异:当使用花括号初始化时,auto 推导结果为 std::initializer_list,而模板推导会失败。

    C++

     auto x = { 27 }; // 类型是 std::initializer_list<int>
     ​
     template<typename T> void f(T param);
     f({ 27 }); // 错误!无法推导 T
  • 注意:在 C++14 中,auto 用于函数返回值或 lambda 形参时,使用的是模板类型推导规则,而非 auto 类型推导规则,因此不能推导花括号初始化列表。


条款三:理解 decltype

decltype 通常返回名字或表达式的精确类型,不进行类似模板推导的类型调整。

基本用法

  • decltype(i) 返回变量的声明类型(包括 const 和引用)。

  • 在 C++11 中常用于尾置返回类型,以根据参数推导返回值类型。

    C++

     template<typename Container, typename Index>
     auto authAndAccess(Container& c, Index i) -> decltype(c[i]);

decltype(auto) (C++14)

  • 含义:使用 auto 告知编译器进行推导,但推导规则遵循 decltype 规则(即精确保留类型,包括引用)。

  • 用途:用于函数返回值推导,避免 auto 剥离引用导致的错误(如返回右值而非期望的左值引用)。

特殊情况

  • 变量名 vs. 表达式

    • decltype(x):返回变量 x 的声明类型。

    • decltype((x))x 加了括号变成复杂的左值表达式,decltype 对所有非单纯变量名的左值表达式返回左值引用。因此 decltype((x)) 返回 T&

    • return 语句中,return (x); 结合 decltype(auto) 会导致返回局部变量的引用,这是未定义行为。


条款四:学会查看类型推导结果

掌握查看推导结果的方法有助于验证理解。

  1. IDE 编辑器:鼠标悬停查看,适用于简单类型,但复杂类型显示可能不准确或难以阅读。

  2. 编译器诊断

    • 利用未定义的类模板触发错误信息。

    • 声明 template<typename T> class TD; 并实例化 TD<decltype(x)> xType;,编译器报错信息会包含 x 的具体类型。

  3. 运行时输出

    • typeid(x).name():通常不可靠,因为 std::type_info::name 会像传值参数一样忽略引用和 cv 限定符(例如 const 被忽略)。

    • Boost.TypeIndex:推荐使用 boost::typeindex::type_id_with_cvr<T>().pretty_name(),它能准确显示包含 constvolatile 和引用修饰符的完整类型。

核心原则:工具只是辅助,深入理解条款 1-3 的规则才是根本。