模板类型推导与auto

本文聊聊C++中的模板类型推导和auto。两者其实是一样的,前者推导T的类型,后者推导auto的类型。本文初创于公司内部博客,更适合于有基础的同学参考。

模板类型推导

对于模板函数来说,编译器需要根据实际传入的参数来推导模板类型T。例如,假设我们有下面这个模板函数:

 

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

那么调用模板函数时,编译器推导出的模板类型分别为:

 

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、是不是reference,类型推导的结果是不一样的。回到刚开始声明的模板函数,我们声明了参数类型为T&,如果声明为T,或者是T&&,又会得到怎样的结果呢?我们用表格来总结一下:

 

是不是有点眼花缭乱?死记硬背是记不住的,我们找一找其中的规律。

  • 一般情况下,param的类型是最完整的类型,继承了形参中声明的cr(const和reference)和实参中带过来的cr。但有两个特例:
    • 特例一:当形参是通用引用(T&&作为模板参数时称为通用引用)时,param根据具体的实参类型,推导为左值引用或者右值引用。
    • 特例二:当形参不是引用时,实参到形参为值传递,去除所有cr修饰符。
  • T中是否包含cr修饰符,取决于param的修饰符是否已在形参中声明过。也就是说,T中修饰符不会与形参中已声明的修饰符重复。

为什么我们需要知道这些规则呢?这是因为,有时候需要根据传入的param的类型构造与之相关联的其它类型的对象。比如我们想要在函数内部构造一个与param同类型但去除cr修饰符的对象,那么就应该把形参声明为const T& param,然后声明T obj这个对象。这是因为上表中第4行表明了,无论实参包含了什么修饰符,无论是左值还是右值,T都是不带任何修饰符的单纯类型。当然,声明为T param也是可以的,但这种情况下参数就是值传递了。所以说,根据实际情况选择合适的模板参数类型是很重要的。

最后,为了更容易实践,我们总结出下面三个推荐用法:

  • 想要按值传递,将模板函数参数声明为T param
  • 想要按引用传递,但不考虑右值时,将模板函数参数声明为const T& param
  • 想要按引用传递,但要区分左值和右值时,将模板函数参数声明为T&& param

auto类型推导

理解了上一节的模板类型推导后,auto类型推导就很简单了。除了极个别情况,auto类型推导与模板类型推导是完全一致的。比如,上一节表格中的四种模板函数调用都可以写出对应的auto语句(只写出对实参x调用的那一列):

 

auto& param = x;        // auto is int, param's type is int&
const auto& param = x;  // auto is int, param's type is const int&
auto&& param = x;       // auto is int&, param's type is int&
auto param = x;         // auto is int, param's type is int

decltype

decltype是一个运算符,用来获取一个变量或表达式的实际类型。之所以需要这个运算符,是因为在某些情况下,特别是模板函数中,我们事先根本不知道应该创建一个什么类型的变量。举个简单的例子,下面这个模板函数用来获取任意一个容器对象的第i个元素:

 

auto get(Container& c, Index i) -> decltype(c[i])
{
    return c[i];
}

函数声明用到了C++11中的trailing return type语法,即返回类型后置。该语法支持在原本书写返回类型的地方用auto占位,然后在参数列表后面加上“→返回类型”。这里之所以用到了这个语法,是因为c[i]必须在其声明的位置后面才可以访问到。

你可能会想,是否能够进一步简化呢?让编译器自动推导返回值类型不是更方便吗。正是为此,C++14提供了如下的写法:

 

template<typename Container, typename Index>
auto get(Container& c, Index i)
{
    return c[i];
}

返回值直接由c[i]的类型自动推导。但我们不能高兴地太早,根据auto类型推导的规则,现在这种写法相当于:

 

auto element = get(Container& c, Index i);

这是有问题的。这种形式对应于第1节表格的最后一行,返回值会按值传递。如果c[i]的实际类型是引用,函数返回类型会自动去掉引用这一修饰符,这在有些情况下不是我们想要的结果。我们想要的结果是,函数返回类型与c[i]完全一致,如果c[i]是引用,那么返回类型也是引用,如果c[i]不是引用,那么返回类型也不是引用。遗憾的是,第1节表格中提供的四种声明方式都不能满足我们的需求,auto&auto&&会把非引用变成引用,const auto&会添加额外的constauto会把引用变成非引用。

现在,唯一的方法就是借助decltype,将函数改写如下:

 

template<typename Container, typename Index>
decltype(auto) get(Container& c, Index i)
{
    return c[i];
}

现在,函数返回类型一定与c[i]的类型完全一致了。但别急,到这里还没完,因为这个函数仍有可改进的空间。注意到,我们把Container的参数设为了引用,这就注定它不能接收右值对象,比如临时创建的Container对象。另写一个重载函数可以解决该问题,但不优雅。借助通用引用和完美转发可以解决这个问题,请看最终版的代码:

 

decltype(auto) get(Container&& c, Index i)
{
    return std::forward<Container>(c)[i];
}

std::forward会根据实参的实际类型将c转换为左值引用或右值引用。关于通用引用和完美转发,可以参考《右值引用那些事儿》。这里,我们同时给出C++11版本的实现,以供对比:

 

template<typename Container, typename Index>
auto get(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
{
    return std::forward<Container>(c)[i];
}

如何查看类型推导结果?

编程过程中,我们经常需要知道模板类T或者auto的推导结果到底是什么,是否符合我的预期。这里介绍四种方式。

  1. 使用IDE的自动提示。像Clion这种IDE,把鼠标放上去就会有自动提示的。但不可尽信,因为IDE内置的语法分析并不总是那么可靠。

  2. 让编译器告诉我们。编译器一定是最准的,但让编译器输出Tauto的类型却并不容易。我们需要手动构造一个编译错误,它才会把错误部分的类型打印出来。比如,想要查看x的类型,需要先声明一个空的模板类

 

template<typename T>
class TD;               // TD means "Type Displayer"

然后实例化对象

 

int y = 0;
const int& x = y;
TD<decltype(x)> xType;  // Compiler will report error at this line, which contains x's type

在我的计算机上,输出的错误信息是

 

error: aggregate 'TD<const int&> xType' has incomplete type and cannot be defined

这样就可以看到,x的实际类型是const int&

  1. 运行时输出。这是我们通常最容易想到的方法。C++提供了typeid操作符,用法如下:

 

std::cout << typeid(x).name() << std::endl;

在我的计算机上,输出结果为i

itypeinfo类中定义的int的简称。很显然,这里的结果是错误的,因为x的实际类型是const int&,而typeinfo却认为它是int。之所以会出现这种情况,是因为x传入typeid时采用了值传递,所有cr修饰符都被丢弃了。所以,typeinfo只适合检查变量的基础类型,不能用来查看其完整类型。

  1. 使用boost库中的typeindex。这种方式需要引入第三方库,但可以保证结果绝对准确,代码示例如下:

 

std::cout << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name() << std::endl;

type_id_with_cvr中的cvr指的是constvolatilereference三种修饰符。使用前先#include <boost/type_index.hpp>。在我的计算机上,输出结果为int const&,完全正确。

这四种用法各有其适用场景,总结一下:

  • 最方便快捷:IDE提示和typeid操作符。
  • 最准确:编译器提示和boost::typeindex

用auto代替显式类型声明

auto有显而易见的优点,比如,它可以使你的代码更简洁,避免手写类型出错等等。不过你可能并不觉得这是多大的优点,但看看下面这个例子,就会发现不用auto的坏处了。

 

std::unordered_map<std::string, int> m;
// ...
for (const std::pair<std::string, int>& p : m)
{
    // do something with p
}

在range-for循环中,我们用const std::pair<std::string, int>&类型的变量绑定m中的每个键值对,看起来天衣无缝。但事实是,m中的每个键值对并非该类型,正确的类型应该是const std::pair<const std::string, int>&,键是const类型的。上面的写法导致创建了临时变量以及隐式类型转换,不知不觉中就造成了性能损失。如果使用auto,这种问题就能完全避免。

再来举一个例子,下面展示了两种保存lambda对象的方式:

 

auto my_function1 = [](const std::vector<int>& v1, const std::vector<int>& v2) {return v1.size() + v2.size();};
std::function<std::vector<int>::size_type(const std::vector<int>&, const std::vector<int>&)> my_function2 = [](const std::vector<int>& v1, const std::vector<int>& v2) {return v1.size() + v2.size();};

前者直接保存为lambda对象,后者相当于转换成了std::function对象后再保存。虽然用的时候并无区别,但如果我们看一下这两个对象的大小,就会发现前者比后者小的多:

 

std::cout << sizeof(my_function1) << std::endl;     // output: 1
std::cout << sizeof(my_function2) << std::endl;     // output: 32

这是因为,std::function是一个功能完善的标准化类,提供了额外的功能,而lambda表达式生成的对象仅仅包含一个operator(),连数据成员都不需要,所以占用空间非常小。因此在这个例子中,auto无论是在简洁性上,还是在性能上,都完全优于std::function

当auto出错时,使用static_cast显式指定类型

最后一节来说说auto不行的场景。有时候,自动推导出的类型反而不是我们想要的类型,这种情况在invisible proxy这种设计模式中可能遇到。举个最常见的例子,我们经常用Eigen这个矩阵库,比如下面的代码:

 

Eigen::MatrixXd m1, m2;
// ...
Eigen::MatrixXd m = m1 + m2;

如果把第二句换成

 

auto m = m1 + m2;

这是会出问题的。因为m1 + m2得到的并不是一个Eigen::MatrixXd类型的对象,而是const CwiseBinaryOp< sum <Scalar>, const Derived, const OtherDerived>类型,见Eigen文档Eigen::MatrixBase::operator+。Eigen使用了lazy-evaluation技术,只有当把一个表达式最终赋值给另一个Matrix对象时,才会真正计算表达式的值。也就是说,在调用Eigen::Matrix::operator=时,才会真正执行加法运算。一旦我们用auto替代了Eigen::MatrixXd,这个表达式就仍然保持其表达式的状态,而不进行运算。关于Eigen的内部实现机制,可以参考What happens inside Eigen, on a simple example,保证干货满满。

还有一种情况,如果我们想要进行隐式类型转换,比如:

 

float float_number = getDoubleNumber();

这种情况也是无法用auto替代float的。但此处有更好的解决方式。像上面这样的隐式类型转换是不推荐的,因为这句代码没有体现出开发者的主观意愿,不知道开发者是忘记了等号两边类型不一致,还是知道但故意这样写。更好的做法是:

 

auto float_number = static_cast<float>(getDoubleNumber());

这就没有任何歧义了。



作者:金戈大王
链接:https://www.jianshu.com/p/d9067403d0c5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值