0%

auto能大大简化我们的代码,但用不好也会带来正确性和性能上的问题。本章覆盖了auto的方方面面,帮助我们避开陷阱,高高兴兴的把手动的类型声明替换成auto

Item5: 优先选用auto而不是显式类型声明

优点1: 避免忘记初始化

1
2
3
int x1;      // potentially uninitialized
auto x2; // error! initializer required
auto x3 = 0; // fine, x3 is well-defined

优点2: 方便声明冗长的,或只有编译器知道的类型

1
2
3
4
5
6
7
8
template <typename It>
void dwim(It b, It e) {
for (; b != e; ++b) {
typename std::iterator_traits<It>::value_type currValue = *b;
// or
auto currValue = *b;
}
}

以及:

1
2
3
4
auto derefUPLess = 
[](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; }

C++14中我们还可以写成:

1
auto derefLess = [](const auto& p1, const auto& p2) { return *p1 < *p2; }

这个例子中,我们根本没办法知道derefLess的类型了,但编译器知道,我们也就能通过auto拿到这个类型了。

优点3: 对函数来说,autostd::function体积更小,速度更快

上例中derefUPLess的类型应该bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&),但如果要手动声明的话,我们要用std::function

1
2
3
4
5
std::function<bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)> 
derefUPLess =
[](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{ return *p1 < *p2; }

一个重要的事实是std::functionauto是不同的。

std::function里面要hold我们传入的closure。相同函数签名的不同closure可能捕获了不同的元素,因此它们需要的体积也不同,但一个函数签名对应着一个确定的std::function特化类型,这个类型的体积是固定的,这说明std::function内部可能会根据不同的closure分配不同大小的堆上内存。这个机制还会影响函数的inline。

结果就是std::function几乎一定比auto体积大,调用慢,还可能会抛out-of-memory的异常。

而且auto还能比std::function少写很长一段代码。

优点4: 声明类型更准确

1
2
std::vector<int> v;
unsigned sz = v.size();

v.size()实际返回的是std::vector<int>::size_type,它是一个无符号整数类型,因此很多人习惯声明为unsigned,但这是不准确的。

32位环境下unsignedstd::vector<int>::size_type都是uint32_t,没问题。但64位环境下,前者还是32位的,后者却是64位的。

而用auto sz = v.size()就能避免这个问题。

另一个例子:

1
2
3
4
5
std::unordered_map<std::string, int> m;
...
for (const std::pair<std::string, int>& p: m) {
...
}

上面的代码有一个大问题:std::unordered_map的key是const的,因此p的类型应该声明为const std::pair<const std::string, int>&

问题还没结束,编译器会努力的为p找到一个从std::pair<const std::string, int>std::pair<std::string, int>的转换,而这样的转换是存在的:生成一个std::pair<std::string, int>的临时对象。

结果就是每次循环都会生成一个临时对象。

而用auto就没有这个问题了:

1
2
3
for (const auto& p: m) {
...
}

auto不光是效率上的问题,还有正确性的问题:如果我们取p的地址,我们能百分百确定它是在m中,而不用auto,我们可能取到一个临时对象的地址。

上面两个例子说明,合理的使用auto有助于写出显然正确的代码,而不需要我们小心翼翼的确定要声明的类型。

如何取舍auto与代码的可读性

auto只是一种选项,不是强制要求,如果显式声明类型能让代码更干净,更好维护,就继续用显式类型声明。

但根据其它语言中的经验,自动类型推断并没有阻碍我们对大型的工业级代码库的开发和维护。

有人担心auto略去了类型,会影响我们对代码的理解,但很多时间一个好的名字能解决这个问题,比如知道这个变量是容器、计数器,还是一个智能指针。

显式写出类型,往往只能引入微妙的问题,而没有提供很多信息。使用auto还能帮助我们做重构。比如一个变量的初始类型是int,有一天你想换成long,那么用了auto的地方自动就变掉了,但用了int去声明的地方则要你一个一个的找出来。

Item6: 在auto推断非预期时显式声明类型

一些场景下表达式的类型与我们想要的类型并不一致,我们依赖于隐式类型转换才能得到想要的类型。这个时候我们需要显式声明类型,如果用auto就会得到非预期的类型。

一种常见场景是表达式返回一个代理类型,比如std::vector<bool>::operator[]返回std::vector<bool>::reference,而不是我们预期的bool。类的设计者预期我们会把返回值的类型声明为bool,再通过reference::operator bool()来做隐式转换。

1
2
3
4
5
std::vector<bool> features();
...
bool highPriority = features()[5]; // reference -> bool, not bool&
...
processWidget(w, highPriority);

而如果声明变量为auto,那么变量的类型就是std::vector<bool>::reference

1
2
3
4
5
std::vector<bool> features();
...
auto highPriority = features()[5]; // std::vector<bool>::reference
...
processWidget(w, highPriority); // undefined behavior

更严重的是,features()返回了一个临时的std::vector<bool>对象,而highPriority中包含一个指向这个临时对象的指针,在这行结束时,这个临时对象就会析构,highPriority中的指针就变成了空悬指针,processWidget的调用就会成为未定义行为。

很多C++库都用到了一种叫做“表达式模板”的技术,也会导致上面的问题。

一个例子:

1
Matrix sum = m1 + m2 + m3 + m4;

通常来说这会产生3个临时的Matrix对象:每次operator+产生1个。如果我们定义一个代理类作为Matrix::operator+的返回值,这个类只会持有Matrix的引用,不做实际的运算,直到调用=时再去生成最终的Matrix,就能避免这几个临时对象的产生。

这个例子中我们也没办法直接声明auto sum = ...

怎么避免出现auto var = expression of "invisible" proxy class type;这种情况呢?

  1. 看文档,一般设计成这样的类会有特殊说明;
  2. 看头文件,看具体调用的返回值类型是不是符合预期;
  3. static_cast,保证返回值的类型符合预期。

static_cast的例子:

1
2
auto highPriority = static_cast<bool>(features()[5]);
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

一些依赖于基础类型的隐式转换的场景也可以用static_cast

1
2
3
double calcEpsilon();
float ep = calcEpsilon(); // implicitly convert double -> float
auto ep = static_cast<float>(calcEpsilon());

(我觉得static_cast只适合用于“确定了用auto”的场景,否则还是显式声明类型好一些)

目录

为什么unique_ptr的Deleter是模板类型参数,而shared_ptr的Deleter不是?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <class T, class D = default_delete<T>>
class unique_ptr {
public:
...
unique_ptr (pointer p,
typename conditional<is_reference<D>::value,D,const D&> del) noexcept;
...
};

template <class T>
class shared_ptr {
public:
...
template <class U, class D>
shared_ptr (U* p, D del);
...
};

上面的代码中能看到unique_ptr的第二个模板类型参数是Deleter,而shared_ptr的Delete则只是构造函数参数的一部分,并不是shared_ptr的类型的一部分。

为什么会有这个区别呢?

答案是效率。unique_ptr的设计目标之一是尽可能的高效,如果用户不指定Deleter,就要像原生指针一样高效。

Deleter作为对象的成员一般会有哪些额外开销?

  1. 通常要存起来,多占用空间。
  2. 调用时可能会有一次额外的跳转(相比deletedelete[])。

shared_ptr总是要分配一个ControlBlock的,多加一个Deleter的空间开销也不大,第一条pass;shared_ptr在析构时要先原子减RefCount,如果WeakCount也为0还要再析构ControlBlock,那么调用Deleter析构持有的对象时多一次跳转也不算什么,第二条pass。

既然shared_ptr并不担心Deleter带来的额外开销,同时把Deleter作为模板类型的一部分还会导致使用上变复杂,那么它只把Deleter作为构造函数的类型就是显然的事情了。

unique_ptr采用了“空基类”的技巧,将Deleter作为基类,在用户不指定Deleter时根本不占空间,第一条pass;用户不指定Deleter时默认的Deleter会是default_delete,它的operator()在类的定义内,会被inline掉,这样调用Deleter时也就没有额外的开销了,第二条pass。

因此unique_ptr通过上面两个技巧,成功的消除了默认Deleter可能带来的额外开销,保证了与原生指针完全相同的性能。代价就是Deleter需要是模板类型的一部分。

相关文档

unique_ptr是如何使用空基类技巧的

我们参考clang的实现来学习一下unique_ptr使用的技巧。

1
2
3
4
5
6
7
8
9
10
11
template <class _Tp, class _Dp = default_delete<_Tp> >
class unique_ptr
{
public:
typedef _Tp element_type;
typedef _Dp deleter_type;
typedef typename __pointer_type<_Tp, deleter_type>::type pointer;
private:
__compressed_pair<pointer, deleter_type> __ptr_;
...
};

忽略掉unique_ptr中的各种成员函数,我们看到它只有一个成员变量__ptr__,类型是__compressed_pair<pointer, deleter_type>。我们看看它是什么,是怎么省掉了Deleter的空间的。

1
2
3
4
5
template <class _T1, class _T2>
class __compressed_pair
: private __libcpp_compressed_pair_imp<_T1, _T2> {
...
};

__compressed_pair没有任何的成员变量,就说明它的秘密藏在了它的基类中,我们继续看。

1
2
template <class _T1, class _T2, unsigned = __libcpp_compressed_pair_switch<_T1, _T2>::value>
class __libcpp_compressed_pair_imp;

__libcpp_compressed_pair_imp有三个模板类型参数,前两个是传入的_T1_T2,第三个参数是一个无符号整数,它是什么?我们往下看,看到了它的若干个特化版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 0>
{
private:
_T1 __first_;
_T2 __second_;
...
};

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 1>
: private _T1
{
private:
_T2 __second_;
...
};

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 2>
: private _T2
{
private:
_T1 __first_;
...
};

template <class _T1, class _T2>
class __libcpp_compressed_pair_imp<_T1, _T2, 3>
: private _T1,
private _T2
{
...
};

看起来第三个参数有4种取值,分别是:

  • 0: 没有基类,两个成员变量。
  • 1: 有一个基类_T1,和一个_T2类型的成员变量。
  • 2: 有一个基类_T2,和一个_T1类型的成员变量。
  • 3: 有两个基类_T1_T2,没有成员变量。

__compressed_pair继承自__libcpp_compressed_pair_imp<_T1, _T2>,没有指定第三个参数的值,那么这个值应该来自__libcpp_compressed_pair_switch<_T1, _T2>::value。我们看一下__libcpp_compressed_pair_switch是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <class _T1, class _T2, bool = is_same<typename remove_cv<_T1>::type,
typename remove_cv<_T2>::type>::value,
bool = is_empty<_T1>::value
&& !__libcpp_is_final<_T1>::value,
bool = is_empty<_T2>::value
&& !__libcpp_is_final<_T2>::value
>
struct __libcpp_compressed_pair_switch;

template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, false, false> {enum {value = 0};};

template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, true, false> {enum {value = 1};};

template <class _T1, class _T2, bool IsSame>
struct __libcpp_compressed_pair_switch<_T1, _T2, IsSame, false, true> {enum {value = 2};};

template <class _T1, class _T2>
struct __libcpp_compressed_pair_switch<_T1, _T2, false, true, true> {enum {value = 3};};

template <class _T1, class _T2>
struct __libcpp_compressed_pair_switch<_T1, _T2, true, true, true> {enum {value = 1};};

__libcpp_compressed_pair_switch的三个bool模板参数的含义是:

  1. _T1_T2在去掉顶层的constvolatile后,是不是相同类型。
  2. _T1是不是空类型。
  3. _T2是不是空类型。

满足以下条件的类型就是空类型:

  1. 不是union;
  2. 除了size为0的位域之外,没有非static的成员变量;
  3. 没有虚函数;
  4. 没有虚基类;
  5. 没有非空的基类。

可以看到,在_T1_T2不同时,它们中的空类型就会被当作__compressed_pair的基类,就会利用到C++中的“空基类优化“。

那么在unique_ptr中,_T1_T2都是什么呢?看前面的代码,_T1就是__pointer_type<_Tp, deleter_type>::type,而_T2则是Deleter,在默认情况下是default_delete<_Tp>

我们先看__pointer_type是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace __pointer_type_imp
{

template <class _Tp, class _Dp, bool = __has_pointer_type<_Dp>::value>
struct __pointer_type
{
typedef typename _Dp::pointer type;
};

template <class _Tp, class _Dp>
struct __pointer_type<_Tp, _Dp, false>
{
typedef _Tp* type;
};

} // __pointer_type_imp

template <class _Tp, class _Dp>
struct __pointer_type
{
typedef typename __pointer_type_imp::__pointer_type<_Tp, typename remove_reference<_Dp>::type>::type type;
};

可以看到__pointer_type<_Tp, deleter_type>::type就是__pointer_type_imp::__pointer_type<_Tp, typename remove_reference<_Dp>::type>::type。这里我们看到了__has_pointer_type,它是什么?

1
2
3
4
5
namespace __has_pointer_type_imp
{
template <class _Up> static __two __test(...);
template <class _Up> static char __test(typename _Up::pointer* = 0);
}

简单来说__has_pointer_type就是:如果_Up有一个内部类型pointer,即_Up::pointer是一个类型,那么__has_pointer_type就返回true,例如pointer_traits::pointer,否则返回false

大多数场景下_Dp不会是pointer_traits,因此__has_pointer_type就是false__pointer_type<_Tp, deleter_type>::type就是_Tp*,我们终于看到熟悉的原生指针了!

_T1是什么我们已经清楚了,就是_Tp*,它不会是空基类。那么_T2呢?我们看default_delete<_Tp>

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class _Tp>
struct default_delete
{
template <class _Up>
default_delete(const default_delete<_Up>&,
typename enable_if<is_convertible<_Up*, _Tp*>::value>::type* = 0) _NOEXCEPT {}
void operator() (_Tp* __ptr) const _NOEXCEPT
{
static_assert(sizeof(_Tp) > 0, "default_delete can not delete incomplete type");
static_assert(!is_void<_Tp>::value, "default_delete can not delete incomplete type");
delete __ptr;
}
};

我们看到default_delete符合上面说的空类型的几个要求,因此_T2就是空类型,也是__compressed_pair的基类,在”空基类优化“后,_T2就完全不占空间了,只占一个原生指针的空间。

而且default_delete::operator()是定义在default_delete内部的,默认是inline的,它在调用上的开销也被省掉了!

遗留问题

  1. __libcpp_compressed_pair_switch_T1_T2类型相同,且都是空类型时,为什么只继承自_T1,而把_T2作为成员变量的类型?
  2. unique_ptrpointer_traits是如何交互的?

简单版

以下代码编译时会有warning:

1
2
3
4
5
class X;

void foo(X* x) {
delete x;
}

在GCC4.1.2下,编译出错信息是:

1
2
3
4
warning: possible problem detected in invocation of delete operator:
warning: ‘x’ has incomplete type
warning: forward declaration of ‘struct X’
note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined.

这是因为在foo里,编译器看不到X的完整类型,没办法确定两件事情:

  1. X有没有自定义的析构函数(准确的说,有没有non-trivial的析构函数)。
  2. X有没有自定义的operator delete函数。

在不确定这两件事情的情况下,编译器只能按最普通的方式去处理delete x

  1. 不调用任何析构函数。
  2. 调用全局的operator delete,通常来说就是直接释放内存。

日常版

有一个我们平常会遇到的场景,就会触发上面这个问题。

以下是由三个文件组成的一个工程,其中用到了’pImpl’方法来隐藏实现,因此在接口类中放了一个std::auto_ptr,很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// test.h
#include <memory>

class A {
class Impl;
public:
A();
void Func();
private:
std::auto_ptr<Impl> mImpl;
};

// test.cpp
#include "test.h"
#include <iostream>

class A::Impl {
public:
void Func() {
std::cout << "Func" << std::endl;
}
};

A::A(): mImpl(new Impl) {}

void A::Func() {
mImpl->Func();
}

// main.cpp

#include "test.h"

int main() {
A a;
a.Func();
}

看起来很正常,但编译时有warning:

1
2
3
4
5
6
7
$g++ test.cpp main.cpp
In destructor ‘std::auto_ptr<_Tp>::~auto_ptr() [with _Tp = A::Impl]’:
test.h:4: instantiated from here
warning: possible problem detected in invocation of delete operator:
warning: invalid use of undefined type ‘struct A::Impl’
test.h:5: warning: forward declaration of ‘struct A::Impl’
note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined.

和前面说的warning信息完全一致,看起来也是在调用delete时出的问题。但哪里调用了delete呢?

答案是std::auto_ptr

上面的代码中,我们没有给class A手动写一个析构函数,因为编译器自动生成的析构函数就是我们要的:析构时把mImpl析构掉。

那么自动生成的析构函数长什么样子呢?大概是:

1
2
3
A::~A() {
mImpl.~auto_ptr();
}

展开了基本就是一句delete

1
2
3
A::~A() {
delete mImpl._M_ptr;
}

这个析构函数的位置在哪呢?C++标准里说会把自动生成的类成员函数放在类的定义中,那么就是在test.h中。

问题清楚了:我们在编译main.cpp时,看不到A::Impl的完整定义,但却有一个自动生成的A::~A,其中delete了一个不完整的类对象!

解法

手动写一个A的析构函数,位置要在能看到A::Impl完整定义的地方,也就是test.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
```cpp
// test.h
#include <memory>

class A {
class Impl;
public:
A();
~A();
void Func();
private:
std::auto_ptr<Impl> mImpl;
};

// test.cpp
#include "test.h"
#include <iostream>

class A::Impl {
public:
void Func() {
std::cout << "Func" << std::endl;
}
};

A::A(): mImpl(new Impl) {}
A::~A() {}

void A::Func() {
mImpl->Func();
}

相关文献

C++98只有一组类型推断的规则:函数模板。C++11增加了两组:autodecltype。C++14则扩展了autodecltype的适用范围。

我们需要理解类型推断的规则,避免出了问题后脑海中一片白茫茫,Google都不知道该搜什么东西。

Item1: 理解模板类型推断

应用于函数模板的那套类型推断的规则很成功,数以百万的开发者都在用,即使大多数人都说不清楚这些规则。

但理解类型推断的规则非常重要,因为它是auto基础。但auto的有些规则反直觉,因此要好好学习下面的规则。

一个函数模板的例子:

1
2
template <typename T>
void f(ParamType param);

以及对它的调用:

1
f(expr);

expr要用来推断两个类型:TParamTypeParamType通常与T不同,比如带个const&

1
2
template <typename T>
void f(const T& param);

如果调用是:

1
2
int x = 0;
f(x); // ParamType is const T&

那么T被推断为int,而ParamType则是const int&

这里T就是expr的类型,符合大家的期望。但不总是这样。T的类型不仅与expr有关,也与ParamType的形式有关:

  • ParamType是指针或引用(不是universal reference即普适引用)。
  • ParamType是普适引用。
  • ParamType不是指针也不是引用。

Case1: ParamType是引用或指针,但不是普适引用

规则:

  1. 如果expr是引用,就忽略引用。
  2. 再用ParamType去模式匹配expr,来确定T

例如:

1
2
3
4
5
6
7
8
9
10
template <typename T>
void f(T& param); // param is a reference

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // T is int, param is int&
f(cx); // T is const int, param is const int&
f(rx); // T is const int, param is const int&

注意cxrx都令T被推断为const intParamTypeconst int&。这说明把const对象传给T&参数是安全的,不会令const对象被修改。

第三个调用中rx是引用,但T不是,是因为rx的引用属性在推断中被忽略了。

ParamType如果是const T&,那么三个调用中T都会被推断为int,而不再保留exprconst了。

如果ParamType是指针,那么类似:

1
2
3
4
5
6
7
8
template <typename T>
void f(T* param);

int x = 27;
const int* px = &x;

f(&x); // T is int, param is int*
f(px); // T is const int, param is const int*

总结起来就是,如果ParamType是引用或指针:

  1. expr的引用或指针性会被忽略。
  2. 如果exprconst,那么TParamType中需要且只需要有一个带const,来保证exprconst

Case2: ParamType是普适引用

如果ParamTypeT&&(即普适引用),但expr是左值引用,那么规则比较复杂:

  • 如果expr是左值,那么TParamType都被推断为左值引用。两个不寻常处:
    1. 这是模板类型推断中唯一的T被推断为引用的场景。
    2. 如果expr是右值,那么同Case1。

例如:

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
void f(T&& param);

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // x is lvalue, so T is int&, param is also int&
f(cx); // cx is lvalue, so T is const int&, param is also const int&
f(rx); // rx is lvalue, so T is const int&, param is also const int&
f(27); // 27 is rvalue, so T is int, param is int&&

Case3: ParamType既不是指针也不是引用

如果ParamType既不是指针也不是引用,那么f就是传值调用,那么param就是一个全新的对象,规则:

  1. 同上,如果expr是引用,就忽略引用性。
  2. 如果exprconstvolatile,也忽略。

例子:

1
2
3
4
5
6
7
8
9
10
template <typename T>
void f(T param);

int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int

f(x); // T and param are both int
f(cx); // T and param are both int
f(rx); // T and param are both int

注意expr如果是指向const对象的指针,那么这个const不能被忽略掉。

数组参数

注意:数组作为参数时与指针的行为不同。

传值调用中,数组会被推断为指针:

1
2
3
4
5
6
template <typename T>
void f(T param);

const char name[] = "J. P. Briggs"; // name is const char[13]

f(name); // T is const char*

但如果传引用,数组会被推断为数组:

1
2
3
4
template <typename T>
void f(T& param);

f(name); // T is const char [13] and ParamType is const char (&)[13]

我们可以利用这点在编译期拿到一个数组的长度:

1
2
3
4
5
template <typename T, size_t N>
constexpr size_t arraySize(T (&)[N]) noexcept
{
return N;
}

所以我们可以这么定义数组:

1
2
int keyVals[] = {1, 3, 7, 9, 11, 22, 35}; // keyVals has 7 elements
int mappedVals[arraySize(keyVals)]; // so does mappedVals

或者用更现代的方式:

1
std::array<int, arraySize(keyVals)> mappedVals;

函数参数

另一个会退化为指针的类型是函数。函数会退化为函数指针,且规则与数组相同。例如:

1
2
3
4
5
6
7
8
9
10
void someFunc(int, double);    // someFunc's type is void(int, double)

template <typename T>
void f1(T param);

template <typename T>
void f2(T& param);

f1(someFunc); // T and ParamType are both void (*)(int, double)
f2(someFunc); // T is void(int, double), ParamType is void (&)(int, double)

当然在实践上函数和函数指针几乎没有区别。

Item2: 理解auto类型推断规则

auto使用的类型推断规则与模板的规则几乎一样。

1
2
3
auto x = 27;
const auto cx = x;
const auto& rx = x;

auto就相当于上节中的T,而xcxrx的类型则是ParamType

回忆一下上节介绍的ParamType的三种情况,同样可以应用在auto上:

  • Case1: auto类型是指针或引用,但不是普适引用。
  • Case2: auto类型是普适引用。
  • Case3: auto类型既不是指针也不是引用。
1
2
3
4
5
6
auto x = 27;          // case3: int
const auto cx = x; // case3: const int
const auto&& rx = x; // case1: const int&&
auto&& uref1 = x; // case2: int&. x is lvalue
auto&& uref2 = cx; // case2: const int&. cx is lvalue
auto&& uref3 = 27; // case2: int&&. 27 is rvalue

以及针对数组和函数的规则:

1
2
3
4
5
6
7
8
9
const char name[] = "R. N. Briggs";    // name is const char[13]

auto arr1 = name; // arr1 is const char*
auto& arr2 = name; // arr2 is const char(&)[13]

void someFunc(int, double); // someFunc is void(int, double)

auto func1 = someFunc; // func1 is void (*)(int, double)
auto& func2 = someFunc; // func2 is void (&)(int, double)

例外:auto会把所有的统一初始化式(花括号初始化式)当作std::initializer_list<T>对待。

int的初始化中:

1
2
3
4
int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};

以上四种形式得到的x1x4都是一个值为27int

但如果换成auto,后两者的类型就有些出乎意料了:

1
2
3
4
auto x1 = 27;      // x1 is int
auto x2(27); // ditto
auto x3 = {27}; // x3 is std::initializer_list<int>
auto x4{27}; // ditto

即使这个初始化式根本没办法匹配成std::initializer_list<T>

1
auto x5 = {1, 2, 3.0}; // error! 

如果把std::initializer_list<T>传给一个函数模板,行为则不一样:

  • 如果param的类型为T,则报错:

    1
    2
    3
    template <typename T>
    void f(T param);
    f({11, 23, 9}); // error! can't deduce T
  • 如果param的类型为std::initializer_list<T>,则可以:

    1
    2
    3
    template <typename T>
    void f(std::initializer_list<T> param);
    f({11, 23, 9}); // T is int and param is std::initializer_list<int>

C++14允许auto作为函数的返回类型,及lambda函数的参数类型,但这两种情况下的auto实际应用的是模板的类型推断规则,而不是上面说的auto规则!

1
2
3
4
5
6
7
8
9
10
auto createInitList()
{
return {1, 2, 3}; // error! can't deduce type for {1, 2, 3}
}

std::vector<int> v;
...
auto resetV = [&v](const auto& newValue) { v = newValue; }
...
resetV({1, 2, 3}); // error! can't deduce type for {1, 2, 3}

Item3: 理解decltype

通常decltype返回表达式的精确类型(注意,与模板和auto不同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const int i = 0;             // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) is int
}; // decltype(Point::y) is int
Widget w; // decltype(w) is Widget
if (f(w)) ... // decltype(f(w)) is bool
template <typename T>
class vector {
public:
...
T& operator[](std::size_t index);
...
};
vector<int> v; // decltype(v) is vector<int>
...
if (v[0] == 0) ... // decltype(v[0]) is int&

C++11中,decltype还可以用来表示一个需要推断出来的类型返回类型:

1
2
3
4
5
template <typename Container, typename Index>   // requires refinement
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) {
authenticateUser();
return c[i];
}

一般容器的operator[]都会返回T&,但像vector<bool>就会返回一个中间类型,而用上面的decltype就能处理这种情况。

C++14中我们可以省掉尾部的返回类型:

1
2
3
4
5
template <typename Container, typename Index>   // C++14, not quite correct
auto authAndAccess(Container& c, Index i) {
authenticateUser();
return c[i];
}

上面的形式的问题在于:auto会抹去类型中的引用,导致我们想返回T&,但实际却返回了T

所以实际上C++14我们需要写成decltype(auto)

1
2
3
4
5
template <typename Container, typename Index>   // C++14, requires refinement
decltype(auto) authAndAccess(Container& c, Index i) {
authenticateUser();
return c[i];
}

我们可以把decltype(auto)中的auto替换成return后面的表达式,即decltype(c[i]),这样就能精确的表达我们的意图了。

decltype(auto)不仅能用于模板,同样能用于想拿到表达式精确类型的场合:

1
2
3
4
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // myWidget1 is Widget
decltype(auto) myWidget2 = cw; // myWidget2 is const Widget&

再回头看上面C++14版本的authAndAccess,它的问题是参数类型为左值引用,因此无法接受右值参数(const Container&可以,但返回类型带const)。

什么情况下我们会传入一个容器的右值引用?也许是想拿到一个临时容器的成员的拷贝吧。

我们当然可以用重载来实现同时支持左值和右值引用的目的。如果不用重载的话,就需要把参数类型改成普适引用了:Container&& c。为了能同时处理左值和右值引用,我们要引入std::forward<T>,它在传入右值时返回右值,传入左值时返回左值。

1
2
3
4
5
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
autherticateUser();
return std::forward<Container>(c)[i];
}

C++11中我们需要使用尾部返回类型:

1
2
3
4
5
6
7
template <typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
autherticateUser();
return std::forward<Container>(c)[i];
}

decltype一个容易忽视的特性是:如果它的括号中的表达式是左值,且不是一个变量的名字,那么decltype会保留引用。

因此decltype(x)返回int,但decltype((x))返回int&

因此在使用decltype(auto)时,改变return后面的表达式形式,可能会改变返回的类型:

1
2
3
4
5
6
7
8
9
10
decltype(auto) f1() {
int x = 0;
...
return x; // return int
}
decltype(auto) f2() {
int x = 0;
...
return (x); // return int& to a local variable!
}

Item4: 如何显示推断出来的类型

IDE

编译器诊断消息

可以通过编译器在出错时给出的诊断消息来显示推断出的类型。

首先声明一个模板类,但不给出任何定义:

1
2
template <typename T>
class TD;

然后用你想显示的类型来实例化这个模板:

1
2
TD<decltype(x)> xType;
TD<decltype(y)> yType;

显然编译会出错,错误消息中就有我们想看到的类型:

1
2
error: aggregate 'TD<int> xType' has incomplete type and cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined

不同的编译器可能会给出不同格式的诊断消息,但基本上都是能帮到我们的。

运行时输出

方法1: std::type_info::name

1
2
std::cout << typeid(x).name() << std::endl;
std::cout << typeid(y).name() << std::endl;

这种方法输出的类型可能不太好懂,比如在GNU和Clang中int会缩写为i,而const int*会缩写为PKi

但有的时候std::type_info::name的输出靠不住:

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
void f(const T& param) {
std::cout << typeid(T).name() << std::endl;
std::cout << typeid(param).name() << std::endl;
}

std::vector<Widget> createVec();
const auto vw = createVec();

if (!vw.empty()) {
f(&vw[0]);
}

在GNU下输出是:

1
2
PK6Widget
PK6Widget

意思是const Widget*

问题来了:根据Item1的规则,这里T应该是const Widget*,而param应该是const Widget* const&啊!

不幸的是,这就是std::type_info::name的要求:它要像传值调用一个模板函数一样对待类型,即“去掉引用、const、volatile”。

所以const Widget* const&最终变成了const Widget*

方法2: boost::typeindex::type_id_with_cvr

在方法1不奏效的时候,可以用boost::typeindex::type_id_with_cvr来查看类型。

1
2
3
4
5
6
7
#include <boost/type_index.hpp>

template <typename T>
void f(const T& param) {
std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
std::cout << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl;
}

GNU输出为:

1
2
Widget const*
Widget const* const&

而且,因为Boost是一个跨平台的开源库,因此在各个平台的各个编译器下,我们都能得到可用且正确的信息,尽管输出不完全一致。

目录