声明:作者其实对他讲的东西什么都不懂,所以如果哪个地方有误请指出并痛骂作者。
auto
C++11给我们带来了auto
这种偷懒便利的东西,可以让编译器帮我们自动地推断出我们想要的类型 (这样我们就可以不用费工夫写特别长的类型名了,懒人必备)。
int& f();
std::unordered_map<std::string::size_type, std::pair<std::string, int>>::const_local_iterator g();
auto x = 0; // Type of x deduced as int
auto y = f(); // Type of y deduced as int
auto z = g(); // Type of z deduced as whatever nonsense it should be
不过这样就能看到一个坑了,这里y
被复制了一份,而不是像预期的那样是一个引用。当然如果这样写就能既是引用又能让编译器推断类型了。
int& f();
auto& y = f(); // auto deduced as int, so y is int&
不过这样写的话我们需要先知道这个返回类型确实是个左值引用,如果不是的话就会出错:
int&& f();
auto& y = f(); // Deduced as int&, but cannot bind rvalue to non-const lvalue reference
这个时候我们就可以用到auto&&
这种东西:
int& f1();
int&& f2();
auto&& y1 = f1(); // auto deduced as int&, y1 is int&
auto&& y2 = f2(); // auto deduced as int, y2 is int&&
这种写法之所以能使用,是因为有引用坍缩这么一回事。只有两个右值引用才坍缩成右值引用,只要出现左值引用则坍缩成左值引用。T& &
,T& &&
,T&& &
都会坍缩成T&
,只有T&& &&
会变成T&&
。所以上面y1
的auto
被推断为int&
意味着y1
的类型是int&
。
不过这种写法仍然有问题。如果函数返回的是纯右值(prvalue),那么这种写法会导致悬垂引用的出现。 回看自己之前写的东西的时候发现这里我写错了。这个例子里面 f()
的结果赋值给右值引用并不会悬垂,由于对象生存期延长的规则这里 f()
返回值的生存期会延长到引用 y
的作用域。不过因为这里 y
被推导成 int&&
如果之后再用 y
的类型做进一步推导的话就会丢失 f()
返回的不是右值引用这一信息,向后传递这个值的时候就有可能出现真·悬垂引用了。
int f();
auto&& y = f(); // Lifetime extended, but error-prone
至于如何处理这种问题,我们接下来再探讨。
decltype
既然编译器可以在变量初始化时帮我们推断出来一个表达式的类型,那我们怎样直接获得表达式的类型呢?答案就是decltype
。这个关键字可以像auto
那样帮我们推断出表达式的类型。
static_assert(std::is_same_v<int, decltype(0)>);
int x = 0;
static_assert(std::is_same_v<int, decltype(x)>);
const int y = 0;
static_assert(std::is_same_v<const int, decltype(y)>);
这里需要注意decltype(x)
与decltype((x))
的区别,具体请见cppreference上decltype
的说明。简而言之,若decltype
中是无括号的标识或者类成员访问,那么decltype
直接返回其定义类型;如果是带括号的表达式,那这个表达式就会被看做是普通的左值表达式。比如下面的例子:
int x;
// Not parenthesized, thus decltype(x) is simply the type of x
static_assert(std::is_same_v<int, decltype(x)>);
// When parenthesized, (x) is considered an lvalue expression
static_assert(std::is_same_v<int&, decltype((x))>);
struct S { int x; } const s;
// Declaration type of s.x is int
static_assert(std::is_same_v<int, decltype(s.x)>);
// (s.x) is an lvalue expression, since s is const,
// (s.x) is a constant lvalue expression
static_assert(std::is_same_v<const int&, decltype((s.x))>);
auto&&
和decltype(auto)
decltype(auto)
会像decltype
那样推断出表达式的类型,这样就可以包括各种不同的值类型了。
auto x1 = 0; // x1 deduced as int
decltype(auto) x2 = 0; // decltype(0) is int, thus x2 is int
auto y1 = x1; // y1 deduced as int
decltype(auto) y2 = x2; // decltype(x2) is int, thus y2 is int
auto z1 = (x1); // z1 deduced as int
decltype(auto) z2 = (x2); // decltype((x2)) is int&, thus z2 is int&
auto w1 = { 1, 2 }; // w1 deduced as std::initializer_list<int>
// decltype(auto) w2 = { 1, 2 }; // Error, { 1, 2 } isn't an expression
还记得前面auto&&
悬垂引用的问题吗?有了decltype(auto)
,这个问题就可以比较简单地解决了。
int f1();
int& f2();
int&& f3();
decltype(auto) x1 = f1(); // x1 is int
decltype(auto) x2 = f2(); // x2 is int&
decltype(auto) x3 = f3(); // x3 is int&&
C++14加入了函数返回值自动推导的语法,同样也可以用decltype(auto)
。
int f1();
int& f2();
int&& f3();
decltype(auto) g1() { return f1(); } // Returns int
decltype(auto) g2() { return f2(); } // Returns int&
decltype(auto) g3() { return f3(); } // Returns int&&
这样我们就有了转发函数返回值的方法。(这里可以对比一下转发参数所用到的std::forward
。)
如果有关于更复杂的返回值转发情况,请看一看2018年的CppCon上Ezra Chung的talk:Forwarding Values… and Backwarding Them Too?。
std::declval<T>
虽然这个函数跟前面讲的东西关系不大,(但是它的名字也有decl) 但是它经常出现在decltype
类似的不求值环境内,所以这里也一并说明一下。
想象一下这个场景,在写泛型算法的时候,我们想要获得一个容器类对应的迭代器类型。也就是说,给定一个容器类T
,我们想知道类型为T
的对象value
对应的value.begin()
的返回值类型。
因为begin
不是一个静态函数,所以你不能直接用decltype(T::begin())
来获取返回类型,并且decltype(&T::begin)
返回的是成员函数指针的类型,也不是我们想要的。我们需要一个T
类型的对象来调用这个函数。
初步的想法即是采用默认构造函数构造一个T
:
decltype(T{}.begin())
但是T
不一定有默认构造函数,甚至都不一定有公开的构造函数。这里就要用到std::declval
了。这个函数没有函数体,只有一个声明,返回一个T
类型的右值引用。它的声明大概是这样的:
namespace std
{
template <typename T>
add_rvalue_reference_t<T> declval() noexcept;
}
虽然它没有函数体,不能在正常的地方调用,但是它可以用在decltype
这种不求值环境中只供类型推导使用。于是我们无法调用构造函数的问题就可以用std::declval
这样绕过去了:
decltype(std::declval<T>().begin())