C++ 模板类型推导 gaunthan Posted on Jul 30 2019 ? C++ ? ## 概述 C++ 模板类型推导是复杂系统设计的一大成功,它使得程序员可以忽略类型推导的复杂过程,简单地向模板函数传递参数即可完成预期功能。对于大部分 C++ 程序员而言,模板类型推导是难以简单阐明、复杂的系统。本文是笔者阅读 Effective Modern C++ Item 1 时的笔记,旨在加强对模板类型推导工作过程的理解。 ## 一个例子 讲述模板类型推导,得有一个例子搭配。下面是一个函数模板的定义及与它相关的一个调用: ```cpp template<typename T> void f(ParamType param); f(expr); ``` 在编译时,编译器会利用 expr 包含的信息推导两个类型:一个给 T,一个给 ParamType。这两个类型常常是不一样的,因为 ParamType 往往带有诸如 `const` 和 `&` 之类的修饰符。如: ```cpp template<typename T> void f(const T& param); // ParamType is const T& int x = 0; f(x); // call f with an int ``` 对于函数调用 `f(x)`,T 会被推导为 int,而 ParamType 则被推导为 `const int&`。 很容易误以为 T 与实参 x 的类型肯定相同,特别是在上面这个例子里它们都为 int。然而这只是凑巧罢了。T 的类型推导不仅依赖于 expr 的类型,还取决于 ParamType。ParamType 取三种不同的类型时,T 的类型推导过程都不同: - ParamType 是指针或引用类型,但不是**通用引用**(universal reference),如 `const T&`、`T*`。 - ParamType 是通用引用类型,如 `T&& param`。 - ParamType 不是指针也不是引用,如 `T param`。 ## 函数形参定义对模板类型推导的影响 ### 当形参是指针或引用类型 最简单的推导情况是 ParamType 为指针或引用类型,而非通用引用。在这种情况下,类型推导如下工作: 1. 如果 expr 的类型是引用,则忽略引用。 2. 接着将 expr 的类型与 ParamType 相匹配,以决定 T 的类型。 例如对于这段代码: ```cpp template<typename T> void f(T& param); // param is a reference int x = 27; // x is an int const int cx = x; // cx is a const int const int& rx = x; // rx is a reference to x as a const int ``` 在下面这些函数调用中,为 param 和 T 推导出的类型如注释所言: ```cpp f(x); // T is int, param's type is int& f(cx); // T is const int, param's type is const int& f(rx); // T is const int, param's type is const int& ``` 如果我们将函数的形参列表改为 `const T& param`,则对于上面三个调用来说,T 的类型都被推导为 int。特别地,对于第一个调用,param 的类型为 `const int&`。其他调用的 param 的类型则无变化。 如果我们将引用换为指针,即 `const T* param`,则对于 T 而言,推导结果不变,但对于 param 的类型而言,则从引用转变为指针类型。 ### 当形参是通用引用类型 当形参是指针或引用类型时,模板类型推导过程很直接,没有理解的难点。然而当形参是通用引用类型 `T&&` 时,事情就变得比较复杂。在这种情况下,模板类型推导如下工作: - 如果 expr 是**左值**(lvalue),则 T 和 ParamType 都被推导为左值引用。注意,**仅在这种情况 T 才会被推导为引用类型**。另外,尽管形参的定义是 `T&& param`,但 param 本身是一个参数变量,因此它的类型是左值而不是右值。**所有的形参都是左值类型**。 - 如果 expr 是**右值**(rvalue),按之前论述的第一种情形处理(即形参是指针或引用类型时的处理方式)。 看看例子: ```cpp template<typename T> void f(T&& param); // param is now a universal reference int x = 27; // as before const int cx = x; // as before const int& rx = x; // as before f(x); // x is lvalue, so T is int&, param's type is also int& f(cx); // cx is lvalue, so T is const int&, param's type is also const int& f(rx); // rx is lvalue, so T is const int&, param's type is also const int& f(27); // 27 is rvalue, so T is int, param's type is therefore int&& ``` ### 当形参既非指针也非引用类型 当 ParamType 既不是指针也不是引用,我们便在**传值**(pass-by-value): template<typename T> void f(T param); // param is now passed by value 这意味着 param 是实参的一份拷贝,一个新的对象。因此工作过程为: 1. 像之前一样,如果 expr 的类型是引用,则忽略引用部分。 2. 如果忽略引用性质后,expr 是 `const`,则把 const 也忽略。如果还用 volatile 修饰了,则也忽略 volatile。 因此,对于这三个调用,推导结果就很清晰了: ```cpp f(x); // T's and param's type are both int f(cx); // T's and param's type are again both int f(rx); // T's and param's type are still both int ``` 尽管 cx 和 rx 都代表 const 值,但 param 却不是。因为实参虽然无法修改,但我们可以修改其拷贝,因此 param 不为 const 是很显然的事情。但需要注意,当传递的是 pointer-to-const 时,如 `const char*`,param 的类型会被推导为 `const char*`。如果我们拷贝一个指向 const 的指针后自动得到指向 non-const 的指针,我们肯定想打人:)。 看起来情形三没什么好讲了,然而就跟上面的 pointer-to-const 一样,指针总是出人意料。 #### 数组参数 当向情形三定义的函数 `f` 传递一个数组实参时,会发生什么? const char str[] = "foobar"; // str's type is const char[7] const char* pstr = str; // array decay to pointer f(str); // ??? T 和 param 会被推导为什么类型?由于数组形参会**退化**(decay)为指向其第一个元素的指针,因此 T 和 param 会被推导为 `const char*` 类型。 然而如果我们修改一下函数 `f` 的形参定义,则情况又有所不同: template<typename T> void f(T& param); f(str); // pass array to f 这里为 T 和 param 推导出来的类型分别是 `const char[7]` 和 `const char (&)[7]`。原因是 C++ 允许传递数组引用。将上述代表编译链接,你会得到如下链接错误: undefined reference to `void f<char [7]>(char (&) [7])' 这印证了我们的推导结果。 #### 函数参数 由于函数会退化为函数指针,因此它的推导情况与数组的推导情况是同样的: ```cpp void foo(int, double); // foo is a function: type is void(int, double) template<typename T> void f1(T param); template<typename T> void f2(T& param); f1(foo); // param deduced as ptr-to-func: type is void (*)(int, double) f2(foo); // param deduced as ref-to-func: type is void (&)(int, double) ``` ## 总结 - 在模板类型推导时,引用类型实参会被当成非引用类型的,即忽略引用性。 - 为通用引用类型形参推导类型时,左值实参会被特殊对待。 - 为值传递类型形参推导类型时,const 和 volatile 实参会被当成 non-const 和 non-volatile。 - 在模板类型推导时,数组或函数实参会退化为指针,除非它们被用于初始化引用。 ## References - ScottMeyers, 迈耶斯. Effective Modern C++[M]. 东南大学出版社, 2015. 赏 Wechat Pay Alipay C++ 泛型编程之静态分派 C++ 泛型编程之静态断言