C++基础知识小讲堂(1)——简单的类型推断

迫真基础知识,迫真讲堂


声明:作者其实对他讲的东西什么都不懂,所以如果哪个地方有误请指出并痛骂作者

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&&。所以上面y1auto被推断为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())