现代C++之理解decltype

decltype用于生成变量名或者表达式的类型,其生成的结果有的是显而易见的,可以预测的,容易理解,有些则不容易理解。大多数情况下,与使用模板和auto时进行的类型推断相比,decltype作用于变量名或者表达式只是重复了一次变量名或者表达式的确切类型:

const int i = 0;                         // decltype(i) 为 const int
bool f(const Widget& w);                 // decltype(w) 为 const Widget&
                                         // decltype(f) 为 bool(const Widget&)
struct Point {
    int x, y;                            // decltype(Point::x) 为 int
};                                       // decltype(Point::y) 为 int
Widget w;                                // decltype(w) 为 Widget
if (f(w)) …                              // decltype(f(w)) 为 bool

template<typename T>                     //  std::vector 的简易实现
class vector {
public:
…
T& operator[](std::size_t index);
…
};
vector<int> v;                           // decltype(v) 为 vector<int>
…
if (v[0] == 0) …                         // decltype(v[0]) 为 int&

上面的结果都在意料之中,很好理解。C++11中,decltype的主要用于声明模板函数,此模板函数的返回值类型依赖于其参数类型。例如,看一个例子:我们需要实现一个模板函数,此模板函数的参数包括一个支持方括号("[]")索引的容器加一个int索引值,中间需要做一些验证操作,最后函数返回类型应该同容器索引操作的返回类型相同。

一个元素类型为T的容器,operator []的返回值类型应该为T&。std::queue容器都满足这个要求,std::vector大部分情况下都满足(std::vector<bool>为一个例外,operator[]并不返回bool&,而是一个全新的对象),因此注意这里的容器操作符operator[]的返回值类型依赖于容器类型。

使用decltype可以很方便的实现此模板函数,此模板需要做一些改进,后面讨论:

template<typename Container, typename Index> // 此函数可以工作,但可以改进。
auto authAndAccess(Container& c, Index i)
-> decltype(c[i])
{
    authenticateUser();
    return c[i];
}

注意这里的auto并没有做任何类型推断,只是用来表明这里使用的是C++11 的拖尾返回类型(trailing return type)语法,也就是函数返回类型将在参数列表之后进行声明(在"->"之后),优点是可以使用函数参数来声明函数返回类型(如果将返回类型放置于函数之前,这里的参数c和i还没有被声明,因此不能被使用)。

C++14中可以忽略拖尾返回类型了,这样上面的实现就只剩下auto了。使用这种形式的声明就意味着要进行类型推断。编译器将会根据函数的实现来推断函数返回类型:

template<typename Container, typename Index> // C++14,但是不正确
auto authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i];//根据c[i]推断返回类型
}

上一边帖子的最后解释了,使用auto作为函数返回类型,编译器将会使用模板类型推断推断返回类型。这种情况下上面的函数就有问题了。对于大多数元素类型为T的容器,operator[]返回T&,但是模板类型推断
中解释了,用于初始化的表达式的引用属性会被忽略掉。看下面的代码:

std::deque<int> d;
…
authAndAccess(d, 5) = 10; // 返回 d[5],赋值10,编译会出错

这里的d[5]会返回int&,但是authAndAccess中的auto返回类型推断将会把引用剔除掉,最后的返回值类型为一个右值int。C++中禁止将10赋值给一个右值int,因此编译失败。

为了得到我们想要的,也就是不使用拖尾返回类型,我们需要对返回类型使用decltype类型推断,也就是要指定函数authAndAccess和表达式c[i]返回相同的类型。C++14中我们使用decltype(auto)标志符来达到目的。它的意义是:auto表明要进行类型推断,decltype说明推断过程中将会使用decltype推断规则。最后实实现autoAndAccess如下:

template<typename Container, typename Index> // C++14,正确的实现,仍然可以改进
decltype(auto) authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i];//根据c[i]推断返回类型
}

现在authAndAccess将会返回c[i]所返回的。如果c[i]返回一个T&,authAndAccess也会返回T&。如果c[i]返回一个对象,authAccess也会返回一个对象。

decltype(auto)的使用并不限制于函数返回类型,也能够用于变量的声明:

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto 类型推断,myWidget1的类型为 Widget,引用和const属性被忽略掉了
decltype(auto) myWidget2 = cw;//myWidget2的类型为const Widget& ,因为这里使用了decltype推断推着

在authAndAccess的最后一个版本中,我们提到了此函数仍然可以改进,如何做呢?再看一眼函数声明:

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

这里函数参数为按指向非const左值的引用进行传递,返回容器中元素的引用到客户端就允许客户端对其进行修改。既然是左值引用我们就不能够向这个函数传递右值。但是传递右值到函数中可能是有意义的,客户端可能只想获得容器中元素的一份拷贝,看下面的例子:

std::deque<std::string> makeStringDeque(); // 工厂函数
// 获取从makeStringDeque中返回的deque中第五个元素的拷贝
auto s = authAndAccess(makeStringDeque(), 5);

因此我们需要对函数进行修订,使此函数即能够接受左值,也能接受右值。可以使用重载(一个函数声明一个左值引用参数,一个函数声明一个右值引用参数),但是需要维护两个函数。我们可以使用universal reference参数类型来避免这种情况,因为 此参数类型即可以绑定到右值,也可以绑定到左值,最后authAndAccess可以声明成下面这个样子:

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

在这个模板函数中,我们不知道需要操作的容器类型,也当然不知道容器内的元素类型,对一个不了解其类型的对象采用按值传递,可能会带来不必要的拷贝造成的性能问题,还有可能有对象切片问题,但是这里我们使用容器索引获取函数返回值,仿照标准模板库中的实例来实现看上去是合理的(例如,std::string,std::vector,std::deque,),因此我们坚持使用按值传递。

为了从返回值中传递右值属性,我们需要对univversal reference使用std::forward:

template<typename Container, typename Index> // C++14,最终版本
decltype(auto) authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return  std::forward<Container>(c)[i];
}

上面的函数需要使用C++ 14的编译器,如果没有,也可以使用C++11中的模板版本,与C++ 14不同的是需要你自己指定返回类型:

template<typename Container, typename Index> // C++14,最终版本
decltype(auto) authAndAccess(Container&& c, Index i) ->decltype(std::forward<Container>(c)[i])
{
    authenticateUser();
    return  std::forward<Container>(c)[i];
}

我们还需要说明另外一个问题,文章开始提及了,decltype大多数情况下会返回你所期望的类型,但是还有一些例外,为了更好的理解decltype,我们也需要熟悉这些情况。

将decltype应用于变量名会生成同此变量名相同的类型。这种情况下没有例外。但是对于左值表达式来说情况就有些复杂了,decltype会确保其作用于左值表达式时,生成的类型为一个左值引用。也就是说如果一个左值表达式(而非变量名)的类型为T,那么decltype(左值表达式)的类型就是T&。大多数情况下这不会有任何影响,因为大多数左值表达式都会显示的包含一个左值引用标识符。例如,函数返回左值时,通常会返回左值引用,也就包含一个&标识符。

但是有一种情况需要注意:

int x =0;

x是变量名,因此decltype(x)的类型为int。但是用括号()将x括起来将会生成一个表达式,表达式(x)也为左值,因此decltype((x))为int&。

进一步考虑c++14中的decltype(auto):

decltype(auto) f1()
{
    int x = 0;
    …
    return x; // decltype(x) 为int,f1返回int
}
decltype(auto) f2()
{
    int x = 0;
    …
    return (x); // decltype((x)) 为 int&,  f2 返回 int&
}

注意第二种情况不仅仅返回值发生了变化,而且返回的是指向本地变量的引用。因此要警觉这种错误的发生。

最后总结一下:

  • 大多数情况下decltype为变量名或者表达式生成的类型是不会发生变化的。
  • decltype作用于左值表达式时,生成的类型为T&。
  • 采用C++14中的decltype(audo)进行类型推断时,使用decltype推断规则进行推断。
04-06 20:06