理解decltype类型推导

108 阅读8分钟

理解decltype类型推导

item3原文

参考blog(【深度C++】之“decltype”)

decltype是类型说明符,这个关键字返回操作数的数据类型。相较于模板与auto类型推导,decltype只是简单的返回名字或者表达式的类型。

decltype + 变量,所有的信息都会被保留,数组和函数也不会退化为指针

    int a = 10;
    decltype(a) b;      //b被推导为int
    int &aref = a;
    decltype(aref) bref = a;//bref被推导为int &
    const int &arefc = a;
    decltype(arefc) brefc = a;//brefc被推导为const int &
    const int *const aptr = &a;
    decltype(aptr) bptr = &a;//bptr被推导为const int const *
    const int &&carref = std::move(a);
    decltype(carref) cbrref;//cbrref被推导为const int &&
    int array[2] = {1, 2};
    decltype(array)arr;     //arr被推导为int [2]

decltype + 表达式,会返回表达式结果的类型(表达式不是左值就是右值)

如果表达式是左值就得到该类型的左值引用,如果表达式是右值就得到该类型。为什么有这样的区别? 因为decltype单独作用于对象,没有使用对象的表达式属性,而是直接得到该变量的类型,如果想使用对象的表达式属性,可以加括号。

    int *aptr = &a;
    decltype(*aptr) b = a;//int&
    decltype(a) b1; //int
    decltype((a)) b2;   //int&

    int *const acptr = &a;
    decltype(*acptr) aaa;   //int &
    const int *captr = &a;
    decltype(*captr) bbb;   //const int &
    const int *const cacptr = &a;
    decltype(*cacptr) ccc;  //const int &
    //引用没有顶层const
    decltype(10) ddd;       //int

    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(expr)的目的是完整保留表达式的语义信息,而不仅是类型。

下面给出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&

decltype并不会实际计算表达式的值,编译器会分析表达式并且得到表达式类型。函数调用也算一种表达式,因此不必担心在使用decltype时真正的执行了函数。当使用decltype(func_name)的形式时,decltype会返回对应的函数类型,不会自动转换成相应的函数指针。

在C++11中,decltype最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。假如我们现在要写一个模板函数,这个函数的作用是对容器内某一索引的元素进行认证用户操作,然后将这个元素返回,函数的返回类型应该和索引操作返回的类型相同。

使用decltype使得我们很容易去实现它,这是我们写的第一个版本,使用decltype计算返回类型,这个模板需要改良,我们把这个推迟到后面:

template<typename Container, typename Index>    //可以工作,
auto authAndAccess(Container& c, Index i)       //但是需要改良
    ->decltype(c[i])
{
    authenticateUser();
    return c[i];
}

这里需要注意,函数名前面的auto不会做任何的类型推导,它只是暗示使用了C++11的尾置返回类型语法,即在函数形参列表后面使用一个”->“符号指出函数的返回类型,尾置返回类型的好处是我们可以在函数返回类型中使用函数形参相关的信息。

authAndAccess函数中,我们使用ci指定返回类型。如果我们按照传统语法把函数返回类型放在函数名称之前,ci就未被声明所以不能使用。在这种声明中,authAndAccess函数返回operator[]应用到容器中返回的对象的类型,正是我们所期望的。

C++11允许自动推导单一语句的lambda表达式的返回类型, 在C++11中,auto只能用于变量的类型推导,函数返回类型必须提前确定,不能仅靠函数体内的 return 语句来推导函数返回类型。C++14扩展到允许自动推导所有的lambda表达式和函数,C++14 支持直接使用 auto 作为函数返回类型,通过编译器自动根据函数体内的return推断返回值类型。所以在C++14中我们可以这样写:

template<typename Container, typename Index>    //C++14版本,
auto authAndAccess(Container& c, Index i)       //不那么正确
{
    authenticateUser();
    return c[i];                                //从c[i]中推导返回类型
}

之前的文章解释过,函数返回类型中使用auto,编译器实际上是使用的模板类型推导的那套规则。正如我们之前讨论的,operator[]对于大多数T类型的容器会返回一个T&,但是在模板类型推导期间,表达式的引用性(reference-ness)会被忽略。由于这样的规则,它会对下面用户的代码有影响:

std::deque<int> d;
…
authAndAccess(d, 5) = 10;               //认证用户,返回d[5],
                                        //然后把10赋值给它
                                        //无法通过编译!

在这里d[5]本该返回一个int&,但是模板类型推导会剥去引用的部分,因此产生了int返回类型。函数返回的那个int是一个右值,上面的代码尝试把10赋值给右值int,C++11禁止这样做,所以代码无法编译。那我们现在应该怎么办呢,命名有了更优的写法,但是模板类型推导的规则会忽略引用性。

在这里,我们需要使用decltype类型推导来推导它的返回值,即指定authAndAccess应该返回一个和c[i]表达式类型一样的类型。代码如下:

template<typename Container, typename Index>    //C++14版本,
decltype(auto)                                  //可以工作,
authAndAccess(Container& c, Index i)            //但是还需要
{                                               //改良
    authenticateUser();
    return c[i];
}

decltype(auto)这个语法看起来十分令人困惑,既有decltype保留类型又有auto类型推导。其实这个语法结合了两件事:

  • auto:用来自动推导类型,但它会 丢掉引用性和值类别(左值/右值)。
  • decltype(expr):不仅能推导类型,还能 保留引用和值类别

所以decltype(auto) 表示用decltype的方式去推导类型,但是推导对象是初始化表达式。下面是原文中的解释:auto说明符表示这个类型将会被推导,decltype说明decltype的规则将会被用到这个推导过程中。现在authAndAccess将会真正的返回c[i]的类型。

一般情况下c[i]返回T&authAndAccess也会返回T&,特殊情况下c[i]返回一个对象,authAndAccess也会返回一个对象。decltype(auto)的使用不仅仅局限于函数返回类型,当你想对初始化表达式使用decltype推导的规则,你也可以使用:

Widget w;

const Widget& cw = w;

auto myWidget1 = cw;                    //auto类型推导
                                        //myWidget1的类型为Widget
decltype(auto) myWidget2 = cw;          //decltype类型推导
                                        //myWidget2的类型是const Widget&

我们再回过头看一下C++14版本的authAndAccess声明:

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

容器通过传引用的方式传递非常量左值引用,因为返回一个引用允许用户可以修改容器。但是这意味着在不能给这个函数传递右值容器,右值不能被绑定到左值引用上。但是向这个函数传递一个右值容器是一个十分罕见的情况。一个右值容器,是一个临时对象,通常会在authAndAccess调用结束被销毁,这意味着authAndAccess返回的引用将会成为一个悬置的(dangle)引用。但是使用向authAndAccess传递一个临时变量也并不是没有意义,有时候用户可能只是想简单的获得临时容器中的一个元素的拷贝,比如这样:

std::deque<std::string> makeStringDeque();      //工厂函数

//从makeStringDeque中获得第五个元素的拷贝并返回
auto s = authAndAccess(makeStringDeque(), 5);

要想支持这样使用authAndAccess我们就得修改一下当前的声明使得它支持左值和右值。我们可以选择重载函数,一个函数重载声明为左值引用,另一个声明为右值引用,但是这样我们就不得不维护两个函数。另一个方法是使authAndAccess的引用可以绑定左值和右值,这就是通用引用:

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

在这个模板中,我们不知道我们操纵的容器的类型是什么,那意味着我们同样不知道它使用的索引对象的类型,对一个未知类型的对象使用传值通常会造成不必要的拷贝,对程序的性能有极大的影响。