C++类型推导

ps. 本文引用了<<Effective Modern C++>>一书中的内容, 作为学习后的精炼和总结. 建议读原版书籍, 内容更丰富更精彩:).

模板类型推导

在编译期间, 模板推导需要推导两部分, 一是T(模板类型), 一是ParamType(含有模板类型的形参类型).
需要谨记, T的最终类型是由ParamType和实参类型共同决定的, 对于ParamType的推导, 它最终应该满足自身的性质, 如它需要是引用类型, 那么最终它就应该是引用类型, 同时保留实参的一些性质, 如实参具有常量性, 那么ParamType在引用实参就应该保留其常量性, 可以理解为, 模板推导的结果是T, ParamType都最合理的结果.

template<typename T>
void func(ParamType param);

func(expr); // 编译会通过expr推导T和ParamType的类型

Case 1: 当ParamType是指针或者引用类型, 却又不是万能引用(universal reference)时.

  • 若expr是引用, 则T忽略它自身的引用.
  • 此后在根据ParamType决定T的类型.
template<typename T>
void func(T& param); // ParamType是引用类型

int x = 27; // x是 int 类型
const int cx = x; // cx是 const int 类型
const int& crx = x; // crx是 const int& 类型

func(x); // ①T被推导为 int, ParamType被推导为 int&
func(cx); // ②T被推导为 const int, ParamType被推导为 const int&
func(crx); // ③T被推导为 const int, ParamType被推导为 const int&

const char name[] = "J. P. Briggs"; // name是 const char[13] 类型
func(name); // ④T被推导为 const char [13], ParamType被推导为 const char (&)[13]

void some_func(int, double); // some_func是 void(int, double) 类型函数
func(some_func); // ⑤T被推导为void(int, double), ParamType被推导为 void (&)(int, double)

此例中, 首先ParamType是T的引用类型, 然后
①中实参x是int类型, ParamType被推导为int&, T被推导为int是比较好理解的.
②中实参cx是const int类型, ParamType被推导为const int&, 常量性得以保留, 因为此处引用了一个常量, 若去掉const, 则将导致cx在func内部可以被修改, 显然不被允许. T此时被推导为const int.
③中实参crx是const int&类型, ParamType被推导为const int&, crx自身的引用被忽略, 最终推导出T为const int.
④中实参name是const char[13]类型数组, 因ParamType是引用类型, 引用前后类型需要完全一致, 因此数组类型不会被弱化(decay)指针, T被推导为 const char [13], ParamType被推导为 const char (&)[13].
⑤中实参some_func是void(int, double)类型函数, 同④函数类型此时不会被弱化为(decay)指针, T被推导为void (int, double), ParamType被推导为 void (&)(int, double).

Case 2: 当ParamType是万能引用时.

  • 若expr是左值时, T和ParamType都被推导为左值引用.
  • 若expr是右值时, 则T忽略自身的引用, ParamType最终为右值引用.
template<typename T>
void func(T&& param); // ParamType是万能引用

int x = 27; // x是 int 类型
const int cx = x; // cx是 const int 类型
const int& crx = x; // crx是 const int& 类型

func(x); // ①x是左值, T和ParamType都被推导为 int&
func(cx); // ②cx是左值, T和ParamType都被推导为 const int&
func(crx); // ③crx是左值, T和ParamType都被推导为 const int&

func(27); // ④27是右值, T被推导为 int, ParamType被推导为 int&&
func(std::move(x)); // ⑤std::move(x)后得到一个右值, T被推导为 int, ParamType被推导为 int&&

此例中, 首先ParamType是T的万能引用类型, 然后
①中实参x是int类型左值, T和ParamType都被推导为int&, 可能不好理解? 当T是int&时, ParamType实际会得到int& &&, 此时会发生引用折叠, 最终ParamType得到int&.
②中实参cx是const int类型左值, T和ParamType都被推导为const int&, 常量性得以保留, 理由同上.
③中实参crx是const int&类型左值, T和ParamType都被推导为const int&.
④中实参27是int类型的右值, T被推导为int, ParamType被推导int&&.
⑤中实参std::move(x)得到一个int&&类型的右值, 其自身的引用将被忽略, T被推导为int, ParamType被推导int&&.

Case 3: 当ParamType既非指针也非引用.

  • 若expr是引用, 则T忽略它自身的引用, cv(const volatile)性质同样被忽略.
  • 若expr是指针, 则指针指向对象的常量性需要保留, 指针自身的常量性被忽略.
template<typename T>
void func(T param); // ParamType为非引用类型, 按值传递, param是一份拷贝

int x = 27; // x是 int 类型
const int cx = x; // cx是 const int 类型
const int& crx = x; // crx是 const int& 类型

func(x); // ①T和ParamType都被推导为 int
func(cx); // ②T和ParamType都被推导为 int
func(rx); // ③T和ParamType都被推导为 int

const char* const ptr = "Fun with pointers"; // ptr是 const char* const 类型
func(ptr); // ④T和ParamType都被推导为 const char*

const char name[] = "J. P. Briggs"; // name是 const char[13] 类型
func(name); // ⑤T和ParamType都被推导为 const char*

void some_func(int, double); // some_func是 void(int, double) 类型函数
func(some_func); // ⑥T被推导为void(int, double), ParamType被推导为 void (*)(int, double)

此例中, 首先ParamType为非引用类型, 按值传递, 因此
①中实参x是int类型, T和ParamType都被推导为int.
②中实参cx是const int类型, T和ParamType都被推导为int, 常量性被忽略, param是一份拷贝, 是临时对象, 此处对临时对象添加const不合逻辑.
③中实参crx是const int&类型, T和ParamType都被推导为int, 自身引用性质被忽略, 常量性被忽略.
④中实参ptr是const char* const类型, T和ParamType都被推导为const char*, 指针自身按值传递其常量性(内层const)被忽略, 指针指向对象本身希望不被修改其常量性(外层const)被保留.
⑤中实参ptr是const char[13]类型数组, 按值传递, 此处数组被弱化为指针, 因此结果同④.
⑤中实参some_func是void(int, double)类型函数, 按值传递, 此处函数被弱化为指针, T被推导为void (*)(int, double), ParamType被推导为 void (*)(int, double).

auto类型推导

auto类型推导和模板类型推导大致是一样的, 但有两点需要注意.

  • 对初始化列表, auto类型推导成功, 而模板类型推导会失败!
auto x1 = 27; // x1被推导为 int 类型
auto x2 = { 27 }; // ①x2被推导为 std::initializer_list<int> 类型

template<typename T>
void func(T param);
func({27}); // ②error! 无法推导出T的参数类型

template<typename T>
void func(std::initializer_list<T> initList); // ③ 提供处理初始化列表的模板
func({27}); // 此时T被推导为 int

此例中
①中{27}通过auto类型推导, x2的类型为std::initializer_list<int>.
②中{27}通过模板类型推导, 失败!
③中提供了解决方案, 欲使模板类型推导能够处理初始化列表, 则需要如此处理.

  • 将auto作为函数返回值或lambda参数时(C++14支持), 应将其直接看做是模板类型推导, 而不是auto类型推导.
std::vector<int> v;
auto set_value = [&v](const auto& new_value) { v = new_value; }; // C++14, ok
set_value({27}); // error! 无法推导出auto的参数类型, 此时auto是模板类型推导

使用decltype获取类型声明

传给 decltype 的参数, 如果是变量名, 就返回其声明的类型; 如果是表达式,根据表达式的返回值类型来判断是否保留引用.


template<typename Container, typename Index>
auto get_value(Container& container, Index i) -> decltype(container[i]) // ①C++11, 使用auto和decltype声明尾置返回类型来推导模板函数返回值
{
    return c[i];
}

template<typename Container, typename Index>
auto get_value(Container&& container, Index i) -> decltype(std::forward<Container>(container)[i]) // ②欲使之支持移动语义, 添加万能引用和并完美转发
{
    return std::forward<Container>(container)[i];
}

std::vector<int> v{1, 2, 3};

template<typename Container, typename Index>
auto get_value(Container&& container, Index i) // ③C++14:可以去掉尾置返回类型, 此处存在错误
{
    return std::forward<Container>(container)[i];
}
get_value(v, 0) = 0; // error! get_value(v, 0)将返回右值, 无法对右值赋值

template<typename Container, typename Index>
decltype(auto) get_value(Container&& container, Index i) // ④使用decltype(auto)
{
    return std::forward<Container>(container)[i];
}
get_value(v, 0) = 0; // ok! get_value(v, 0)将返回左值, 可以对左值赋值

此例中, get_value通过访问容器下标运算符获取数据
①中为C++11的版本, 此处语法上需要auto和decltype声明尾置返回类型(trailing return type)才能得到模板函数的返回值, auto和尾置返回类型不需要多说, 重点在decltype(container[i]), 它返回的类型和container[i](下标运算符的返回类型)类型一致, 大多数容器的下标运算符的返回类型是引用类型.
②中继续完善, 使之支持移动语义, 于是引进万能引用和完美转发. 此处container若传入右值, 其内容将完全转移到函数内, 函数再以右值的形式将container[i]返回, 其他元素将被丢弃.
③中为C++14的版本, C++14支持模板类型推导返回值, 于是显示声明尾置返回类型可以省略了, 但省略后存在问题, auto的模板推导将忽略所有引用, 得到一个非引用类型的返回值, 最终返回值将按值拷贝返回.
④中decltype(auto)能够解决③的问题, decltype(auto)可视作decltype(std::forward<Container>(container)[i]), 它将返回我们想要的类型!

int x = 27; // x是 int 类型
const int& cx = x;
auto x1 = cx; // x1被推导为 int 类型

// C++14, decltype(auto)
decltype(auto) x2 = cx; // ①x2被推导为 const int& 类型, decltype(auto)可看做是decltype(cx)

// 关于decltype的反直觉的例子
decltype(x) x3 = x; // x作为变量传给decltype, x3的类型是 int
decltype((x)) x4 = x; // ②x作为表达式(因为添加了括号, (x)是一个左值表达式)传给decltype, x4最终的类型是int& !!

此例中
①中是使用decltype(auto)的一个例子, 用于对比, 加深理解.
②中是使用decltype的"特例", decltype对变量和表达式的处理结果可能不同! 但无需担心, 正常使用decltype返回的类型都是所期望的.

类型推导诊断

如果不确定所写模板将被推导出何种类型, 下面提供了一些方法来诊断.
若使用了类似visual studio这样的IDE, 那对于一些明显的错误, IDE通常足够智能的会给出一些错误提示.
否则就只能在编译阶段去发现模板实例化阶段的错误, 存在模板实例化错误可能会报出大量错误! 理解模板报错可能是枯燥和困难的, 因为它们实在太多了! 早些年是这样, 近几年到不曾遇到过了..
对于模板类型推导不确定的地方, 应该主动出击, 而不是被动的等待编译器抱怨.

下面这个例子即是获取编译期模板类型的一种方式, type_displayer没有实现, 若需要模板实例化则肯定报错, 不过这里的报错, 可以比较轻松的通过错误信息获取到推导出的模板类型.

template<typename T>
class type_displayer;

type_displayer<decltype((x))> xtype;
// 此处不用编译vs能直接告知 type_displayer<int&> 是不完整的类型
// 编译后报错: 使用未定义的 class type_displayer<int&> 
// 总之最后得到了decltype((x))的类型是 int&

发表回复

电子邮件地址不会被公开。必填项已用 * 标注