Effective Modern C++ Item 3

Understand decltype

给定名称或表达式, decltype就会告诉你这个名字或者表达式的类型。decltype只是简单的返回名字或者表达式的类型,很正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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中,auto 不能被用作函数模板的返回类型。因此decltype最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。

[!NOTE]

在 C++14 中,auto 不能用作函数的返回类型,除非使用的是 C++14 中的泛型 lambda 表达式。此外,使用 auto 作为函数返回类型还有一些限制,主要涉及到函数模板和非静态成员函数的情况。

  1. 函数模板中的auto返回类型:在函数模板中,如果使用 auto 作为返回类型,编译器无法推导出实际类型,因为模板的参数类型可能在调用时才能确定。因此,在函数模板中使用 auto 作为返回类型是不合法的。

    1
    2
    3
    4
    5
    // 不合法的例子
    template<typename T>
    auto foo(T x) {
    return x;
    }
  2. 非静态成员函数中的auto返回类型:C++ 中,非静态成员函数必须与类的实例相关联才能被调用。因此,在类定义时,非静态成员函数的声明只是描述了函数的接口,而不涉及任何特定实例的情况。返回类型的推导需要考虑类的实例。这些信息直到使用对象调用该函数时才会完全可见,因此在函数声明处无法推导出完整的返回类型,非静态成员函数不能使用 auto 作为返回类型。

    1
    2
    3
    4
    5
    6
    7
    // 不合法的例子
    class MyClass {
    public:
    auto myFunc() {
    return 42;
    }
    };

C++11 中的第一个版本是:

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

函数名称前面的auto不会做任何的类型推导工作。相反的,他使用了C++11的尾置返回类型语法,它允许使用入参作为返回类型推导的一部分。

C++11允许自动推导单一语句的lambda表达式的返回类型, C++14扩展到允许自动推导所有的lambda表达式和函数。因此在 C++14 中, 我们可以省略尾置返回类型而只使用 auto。先贡献一个错误版本:

1
2
3
4
5
6
7
8
9
template<typename Container, typename Index>    //C++14版本,
auto authAndAccess(Container& c, Index i) //不正确!!!
{
authenticateUser();
return c[i]; //从c[i]中推导返回类型
}
std::deque<int> d;
authAndAccess(d, 5) = 10; //认证用户,返回d[5],然后把10赋值给它
//无法通过编译器!

在这里, d[5] 理应返回一个 int&, 但是auto类型推导(此处实际是模板类型推导的那套规则)去除了了引用, 因此推导出了 int 的返回类型,是个右值

所以我们需要使用decltype类型推导来推导它的返回值。decltype(auto) 的规则: auto 指定推导类型, 但推导时应该使用decltype 规则。

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

decltype(auto)的使用不仅仅局限于函数返回类型,也可以对初始化表达式使用decltype推导的规则:

1
2
3
4
5
6
7
8
Widget w;

const Widget& cw = w;

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

目前容器是通过左值引入一个非const传递的, 如果也可以传递右值就好了。我们可以为右值容器定义一个重载版本, 但维护起来太麻烦了。这时我们需要通用引用

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename Container, typename Index>    //最终的C++14版本
decltype(auto) authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}

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

decltype有一些特殊情况:对于单纯的变量名,decltype只会返回变量的声明类型。然而,对于比单纯的变量名更复杂的左值表达式,decltype报告的类型始终是左值引用。

1
2
3
4
5
6
7
8
9
10
11
12
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 总是不加修改的产生变量或者表达式的类型。
  • 对于名称以外的类型 T 的左值表达式, decltype 总是返回类型 T&.
  • C++14 支持 decltype(auto), 它与 auto 一样, 从其初始值设定项中推导类型, 但它使用 decltype 规则执行类型推导。