《Effective Modern C++》读书笔记(3) -- 明白decltype(understand decltype)

时间:2022-03-05 18:52:34

前言

decltype是一个神奇的创造。给它一个name或者expression,它将告诉你name或者expression的类型。正如书中写到的

decltype typically parrots back the exact type of the name or expression you give it

decltype的基本用法有以下两种:

decltype(entity )
decltype(expression )

基本用法

例如:

const int i = 0;            // decltype(i) is const int

bool f(const Widget& w); // decltype(f) is bool(const Widget& w)

struct Point{
int x, y; // decltype(Point::x) or decltype(Point::y) is int;
};

这里有一点值得注意的是,如果当我们使用decltype推导函数模板(function templates)的时候,推导返回的类型依赖于它的参数类型。例如

vector<int> v;          // decltype(v) is vector<int>

但如果是 decltype(v[0])呢?此时返回的类型是int&。这是由于vector容器中operator[]返回的是一个T&。这里还需要注意的一点是,对于

std::vector<bool> vb;

decltype(vb[0])并不返回bool&

decltype与auto

相信很多人都知道模板的强大,结合decltype我们可以让模板变得更加强大。如果我们想返回一个容器内的元素,利用decltype我们可以很轻松的推导出容器内元素的类型。例如

// C++11代码
template <typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
// Some operations
return c[i];
}

在上面的代码中,auto对类型推导没有任何作用。相反,它表示正在使用C++11尾置返回类型的语法特性。如果将上面代码改为:

template <typename Container, typename Index>
decltype(c[i]) authAndAccess(Container& c, Index i)
{
// Some operations
return c[i];
}

在C++11中无法编译通过,原因是C++的返回值是前置语法,在返回值定义的时候参数变量还不存在。C++11允许对单语句的lambda表达式进行类型推导。在C++14中,拓展为所有lambda和所有函数,包括多个语句(甚至多个返回,提供所有产出相同的推导类型)。这就意味着,在C++14中我们可以忽略尾置返回类型,单单只留下auto,例如:

// C++14中编译通过
template <typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
{
// Some operations
return c[i];
}

但也同样在C++14中会产生一个问题,这个问题是由auto引起的。在C++11的代码中,我们使用decltype配合auto使用,这时用auto推导出来的类型为T&,例如:

std::vector<int> v = {1, 2, 3};
authAndAccess(v, 0) = 2; // C++11代码编译通过

代码编译通过,这是由于decltype(v[0])的类型为int&,是一个右值引用,相当于

v[0] = 2

但在C++14的代码中

std::vector<int> v = {1, 2, 3};
authAndAccess(v, 0) = 2; // C++14代码编译出错

这是由于auto返回的类型推导会将引用剥离,使得返回的类型int,而不是int&,具体的auto类型推导请看上一节。这里函数返回的类型为int,是一个右值,而2是一个右值整形。对右值赋值,在C++中这是禁止的,代码编译不能通过。

要想在C++14中实现类似C++11的用法,应该将代码改为:

// C++14中编译通过
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i)
{
// Some operations
return c[i];
}
std::vector<int> v = {1, 2, 3};
authAndAccess(v, 0) = 2; // C++14代码编译通过

decltype(auto)用法

这里讲下为什么会产生这种用法。在C++14中,函数使用auto返回类型,会导致引用剥离的现象,为了指定一个函数(表达式,变量)准确的返回相同的类型(例如:表达式c[i]的类型)。C++标准的制定者,在C++14中通过使用decltype(auto)来实现这一目的。

由于不局限于函数,它也可以用在变量中,例如:

Widget w;

const Widget& cw = w;

auto myWidget1 = cw; // auto type deduction:
// myWidget1's type is Widget

decltype(auto) myWidget2 = cw // decltype type deduction:
// myWidget2's type is const Widget&

回过头来看看,C++14的代码:

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

容器由非常量左值引用(lvalue-reference-to-non-const)传递,但像第一节所讲的如果是一个右值传递呢?现在我们修改一下代码

template <typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i); // c is now a universal reference

c现在是一个通用引用。在这个模板中,我们不知道正在操作的容器的类型,这也意味着我们对它使用的索引对象的类型同样无知。为了使得它满足各种各样的情况,我们需要再次修改代码:

// C++14代码
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i)
{
// Some operations
return std::forward<Container>(c)[i];
}

// C++11代码
template <typename Container, typename Index>
auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
{
// Some operations
return std::forward<Container>(c)[i];
}

至于std::forward的用法,可以自行查找资料

decltype(x) 和 decltype((x))的区别

例如:

int x = 0;

如果使用:

decltype(x);

由于x是一个变量的名字,所以decltype(x)是一个int

如果使用:

decltype((x));

由于(x)是一个表达式,x是一个左值,在C++中,(c)也是一个左值。因此,decltype((x))是一个int&

decltype(auto) f1()
{
int x = 0;
// ...
return x; // decltype(x) is int, so f1 returns int
}

decltype(auto) f2()
{
int x = 0;
// ...
return (x); // decltype((x)) is int&, so f2 returns int&
}

这里要注意的是,f2和f1不单单是返回类型的不同,f2也返回了一个局部变量的引用,这将导致未定义的行为,要谨慎使用。