在读了大半本Effective C++时,受益于其中许多代码规则,虽说不能让coding能力突飞猛进,至少你会开始对代码安全性有所思考,对任何开始接触C++具体项目的人,这都是一本首推的书。但对2025年的今天来说,仍然使用boost库的智能指针举例的00年代的书还是略显落后。庆幸的是,同作者Scott Meyers也意识到这个问题,在10年前出版了包含C++ 11/14特性的《Effetive Modern C++》,虽然C++ 17/20甚至26已经开始进入快车道,但对大多数场景C++11和14的影响绝不能称为老旧。

译文项目来自EffectiveModernCpp_CN,持续更新。

第一章 类型推导

条款01:理解模板类型推导

决定读Effective Modern C++的另外一个原因也来自条款一,大半年前我看了几篇模板编程和类型萃取的资料,写下了一篇文章:C++ Generic Programming:SFINAF与类型萃取,其中有不少当时也不敢肯定的结论,完全是通过一些零碎的demo总结出来的,但竟然在这本书的第一章的第一节得到印证,缘分也好,伏笔也罢,至少能让我相信,这本书将节省我一些独自写demo验证新特性的时间。

看这样的模板函数:

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

//调用类似:
f(expr);
类型推导的地方有两个,一个是针对泛型T,另一个是针对形参param,这很重要,因为你的函数f内部可能就是针对这两种类型进行萃取。

这个问题看起来很抽象,实际上你肯定考虑过:

1
2
void func(int& c)
void func(int &c)
两种写法,你更喜欢哪种?这归咎于你喜欢把引用划给int类型还是变量c,这个答案没有对错之分,因为只是简单的类型写法,而在模板的推导中,划分给T还是划分给param是有严格的规则的,如果你喜欢看具体例子和讨论可以参考上述文章。

这里我们直接结合原书和之前的结论重新给出。

情况一:形参是非万能引用

结论1: 形参类型是左值引用、右值引用、指针类形式,那么T都是非引用,形参类型取决于匹配到什么类型;

如左值引用情况:

1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(T& param); // 左值引用

///左值情况:
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用
f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&
如果形参含const,就T就不含const了,这就是匹配的效果:const不会匹配到T里面
1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(const T& param); // 含const的左值引用

int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用

f(x); //T是int,param的类型是const int&
f(cx); //T是int,param的类型是const int&
f(rx); //T是int,param的类型是const int&

右值引用情况:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void f(const T&& param); // 右值引用(含const就不是万能引用)

int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用

test(27);
test(std::move(x)); // T是int,param的类型是const int&&
test(std::move(cx)); // T是int,param的类型是const int&&
test(std::move(rx)); // T是int,param的类型是const int&&

指针情况也类似,T不会被解释成指针:

1
2
3
4
5
6
7
8
template<typename T>
void f(T* param); //param现在是指针

int x = 27; //同之前一样
const int *px = &x; //px是指向作为const int的x的指针

f(&x); //T是int,param的类型是int*
f(px); //T是const int,param的类型是const int*

这容易理解,原书没有特别提到,但是如果传入的是指针且形参带const又如何呢,我们会发现:当形参带const时,T也不一定是非const特性的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T>
void ff(const T& param){
if(std::is_same<T, const char*>::value)
cout << "std::is_same<T, const char*>::value" << endl;
if(std::is_same<decltype(param), const char* const &>::value)
cout << "std::is_same<decltype(param), const char* const &>::value" << endl;
if(std::is_same<T, int*>::value)
cout << "std::is_same<T, int*>::value" << endl;
if(std::is_same<decltype(param), int* const &>::value)
cout << "std::is_same<decltype(param), int* const &>::value" << endl;
}

int main() {
const char* pname = "EdemMo";
ff(pname); //触发#1和#2

int name1 = 27;
int* pname1 = &name1;
ff(pname1); //触发#3和#4

cout << "done" << endl;
return 0;
}
对于const char*字符串,T推导也是const char*,而不是char*;而传入同为指针的int*,T推导的是int*;当然基础好的会立马反应过来这根本是庸人自扰,const char*可不是普通的char*,const char*表示的是一个字符常量,说明字符内容是不可更改的,而形参T的const,传入指针时匹配指的是指针指向不可修改(这是指针常量范畴),因此它不会去除你的常量指针特性。同理,如果传入的类是const特性的常量指针const Widget*,那么这个常量特性也不会去除,条款04我们会看到这样的例子。从param表现来看也可知,当对const T&传入一个指针时,指针会被赋予指针常量的特性,它与原来指针本身的常量指针特性互不干扰。总而言之,不能一概认为T一定是被匹配为非const特性。

情况二:形参是万能引用

结论2:当形参是万能引用形式时,传入实参为左值、左值引用和右值引用,最终T和形参类型都会被推导为左值引用类型(这也是类型推导中T被推导成引用的唯一情况),而传入实参是右值时,适用结论1,即T为非引用,形参被推导为右值引用,我从之前的文章直接把demo抄过来:

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
template<typename T>
void func(T&& num){ //万能引用形式
if(std::is_lvalue_reference<T>::value)
cout<<"is_lvalue_reference<T>"<<endl;
if(std::is_rvalue_reference<T>::value)
cout<<"is_rvalue_reference<T>"<<endl;
if(std::is_lvalue_reference<decltype(num)>::value)
cout<<"decltype is_lvalue_reference"<<endl;
if(std::is_rvalue_reference<decltype(num)>::value)
cout<<"decltype is_rvalue_reference"<<endl;
}

//左值、左值引用、右值引用:输出is_lvalue_reference<T>和decltype is_lvalue_reference
int a = 5;
int& b = a;
int&& c = 10;
func(a);
func(b);
func(c);

//右值类型:仅输出decltype is_rvalue_reference
func(5);
func(std::move(a));
func(std::move(b));
func(std::move(c));

情况三:值传递

结论3: 形参是值传递形式时,传入实参的引用特性、const/volatile特性均被忽略。

这很容易理解,因为形参是实参的拷贝,实参的任何特性都不大可能影响拷贝的表现:

1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(T param); //以传值的方式处理param

int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样

f(x); //T和param的类型都是int
f(cx); //T和param的类型都是int
f(rx); //T和param的类型都是int
同理,const char*传入时const特性会被去掉,但一种特殊的表示是const char* const expr,这样的实参传递给param,第二个const特性(不能指向其他地址)会被忽略、第一个const特性(指向的为常量)会被保留(即形参类型变成const char*)。

情况四:传入实参是数组

结论4:形参是指针或数组形式,传入数组退化成指针;形参是引用形式,T是数组类型,形参是数组引用类型。

先看形参是指针或者数组的形式,这两种声明是完全等效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
void test(T* input){
if(std::is_same<T, const char>::value)
cout << "std::is_same<T, const char>::value" << endl;
if(std::is_same<decltype(input), const char*>::value)
cout << "std::is_same<decltype(input), const char*>::value" << endl;
}

template <typename T>
void ttest(T input[]){ //等效于指针声明
if(std::is_same<T, const char>::value)
cout << "std::is_same<T, const char>::value" << endl;
if(std::is_same<decltype(input), const char*>::value)
cout << "std::is_same<decltype(input), const char*>::value" << endl;
}

无论传入数组,还是指针,都退化成指针:即适用结论1,T将被推导成非引用,如const char,而形参自身被推导成指针,如const char*:

1
2
3
4
const char name[] = "EdenMo";
const char* pname = name;
test(name);
test(pname); //均输出std::is_same<T, const char>::value、std::is_same<decltype(input), const char*>::value

最有趣的事情来了,C语言存在数组退化指针机制,当你想完整保留数组特性,C++的引用可以满足你:

1
2
template <typename T>
void func(T& param)
当你传入一个数组时,T会被推导成数组类型,如const char[N],而形参的类型将被推导成const char(&)N;而当你传入一个指针时,T会被推导成指针类型,如const char,而形参类型被推导成指针的引用,如const char&(奇怪且无用),一个验证示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
void ttest(T& input){
///传入实参是数组:
if(std::is_same<T, const char[7]>::value) //#1
cout << "std::is_same<T, const char[7]>::value" << endl;
if(std::is_same<decltype(input), const char(&)[7]>::value) //#2
cout << "std::is_same<decltype(input), const char(&)[7]>::value" << endl;
///传入实参是指针:
if(std::is_same<T, const char*>::value) //#3
cout << "std::is_same<T, const char*>::value" << endl;
if(std::is_same<decltype(input),const char*&>::value) //#4
cout << "std::is_same<decltype(input),const char*&>::value" << endl;
}

const char name[] = "EdenMo";
const char* pname = name;
ttest(name); //触发#1和#2
ttest(pname); //触发#3和#4
这里我们至少了解多了两种类型:const char(&)[N]和const char*&,后者基本没什么用,但是前者可以结合constexpr和模板,获取数组长度:
1
2
3
4
5
6
7
8
9
10
template <typename T, std::size_t N>
constexpr std::size_t getArraySize(T(&)[N]) noexcept{ //条款14会知:noexcept使得编译的代码更好
return N;
}

//调用:
int arr[] = {1,2,3,4,5};
int arr1[getArraySize(arr)]; //编译期定长
std::array<int,getArraySize(arr)> arr2;
cout << sizeof(arr1)/sizeof(int) << arr2.size() << endl; //5 5

情况五:传入实参是函数指针

函数和数组一样,都会退化成指针,因此没有什么新理论了,总结为:

模板形式 推导T类型 推导形参类型
void func(T param) void(*)(int,int) void(*)(int,int)
void func(T& param) void(int,int) void(&)(int,int)
void func(T* param) void(int,int) void(*)(int,int)

条款02:理解auto类型推导

把大量复杂的类型推导写在条款01是有缘由的,过去我一直错误认为模板推导仅用于泛型编程中,直到读完条款02,另一种理由是让你更胸有成竹地使用“auto”。

auto的推导原理和模板推导是一致的,当你简单地写下auto赋值时,相当于使用了三种模板推导:

1
2
3
4
5
6
7
8
9
10
11
template <typename T>   
void func(T param)
func(27);                   //相当于 auto x = 27;

template <typename T>  
void func1(const T param)
func1(x)                    //相当于 const auto cx = x;

template <typename T>
void func2(const T& param)
func2(x)                    //相当于 const auto& rx = x;
是的,auto推导的类型就对应T的类型,变量自身的类型,相当于模板形参的类型,而赋值的来源值,就相当于传入的实参值。

同理,auto像条款01的情况二一样调用万能引用:

1
2
3
auto&& uref1 = x;
auto&& uref2 = cx;  //实参为左值,auto和uref1、uref2均是左值引用类型(int&)
auto&& uref3 = 27//实参为右值,auto是int类型,uref3是右值引用类型(int&&)

数组、函数指针表现也与模板推导一致:

1
2
3
4
5
6
7
const char name[] = "R. N. Briggs";
auto arr1 = name;               //值传递:指针退化,arr1的类型是const char*
auto& arr2 = name;              //引用传递:arr2类型是const char (&)[13] 

void func(int, double);
auto func1 = func;      //值传递:func1类型是void(*)(int,double)
auto& func2 = func;     //引用传递,func2类型是void(&)(int,double)
题外话,函数引用和函数指针不同的是,函数引用不允许置空,也不允许二次赋值:
1
2
3
4
5
6
void func(int, double);
auto& func2 = func;     //引用传递,func2类型是void(&)(int,double)

//错误:不允许置空或二次赋值!
func2 = nullptr;
func2 = func_else;

C++11的统一初始化

auto和模板推导存在一个例外的区别:auto支持统一类型的花括号推导,而模板推导必须显式指明std::initializer_list才能成功推导类型T;

C++ 11引入了花括号初始化方式,称为统一初始化(uniform initialization):

1
2
3
4
5
6
7
//C++98语法:
int x = 27;
int x(27);

//C++11 支持:
int x = {27};
int x{27};
而如果在统一初始化中使用auto,推导的类型是std::initializer_list,而不是int(其中auto x{27};N3922后才修正为int):
1
2
3
4
5
auto x = 27
auto x(27);   //都是int

auto x{27};   //修正为int
auto x = {27};  //注意,为std::initializer_list<int>

auto可以使用列表推导,但是要求列表元素必须同一种数据类型:

1
2
auto list = {1, 2, 3, 4};    //ok,推导为std::initializer_list<int>
auto list1 = {1, 2, 3.3f};   //无法编译

而在模板中,无论列表元素类型相不相同,都无法直接使用列表推导,只能显式使用std::initializer_list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename T>
void test(T param){
    for(int x:param){
        cout << x << ", ";
    }
    cout << endl;
}


template <typename T>
void ttest(std::initializer_list<T> param){
    for(int x:param){
        cout << x << ", ";
    }
    cout << endl;
}

///调用:
    //test({1,2,3,4});   //错误:不存在模板
    ttest({1,2,3,4});    //ok

同样的,在C++14中,允许使用auto作为函数返回值类型,但是仍然使用模板推导规则进行,所以auto仍然不支持推导返回列表的类型:

1
2
3
4
//error code:
auto getArray(){
    return {1,2,3};
}
匿名函数也是同理:
1
2
3
4
5
6
7
    std::vector<int> vp;
    auto resetVp = [&vp](const auto& newvp){
        vp = newvp;
    };

    resetVp(std::initializer_list<int>{1,2,3,4});  //ok
    resetVp({1,2,3,4});                            //错误:不存在模板

std::initializer_list

再插点题外拓展,std::initializer_list是一种很轻量的结构,支持遍历打印,但不支持修改元素(增删改),适宜局部存储一些列表值、作为函数参数等,如:

1
2
3
4
5
void doSomething(std::initializer_list<int> lst){
    for(int x : lst){
        ...
    }
}
作函数参数时,C++甚至会建议你直接使用它的拷贝语义;当然它也可以存储自定义的类,但是要求对应的类必须允许拷贝:
1
2
3
4
5
void doSomething(std::initializer_list<Person> lst){
    for(const auto& p : Person){
        ...
    }
}
将初始化列表作为返回值极度危险,离开函数域,初始化列表会被析构,确实需要应该使用vector等STL:
1
2
3
4
5
6
7
auto getArray1() {
    return std::vector<int>{1, 2, 3};     //good
}

auto getArray1() {
    return std::initializer_list<int>{1, 2, 3};   //补药这么干啊......
}

条款03:理解decltype

decltype最常用的用法是推导变量的类型,相比于前两个条款的模板推导和auto推导,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
25
26
27
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&
    ......
}              

其中值得注意的是,对于T类型的STL容器,使用operator[]往往会返回一个T&,但是例外是std::vector<bool>,其operator[]返回的不是bool&,在MSVC平台,其返回的是std::_Vb_reference<std::_Wrap_alloc<std::allocator<unsigned int>>>,而在类gcc平台,返回的是std::_Bit_reference&&,详见条款06;

C++11 允许自动推导单一lambda语句的返回类型,如:

1
2
3
4
5
//这并不完美
template <typename Container, typename Index>
auto authAndAccess_decltype(Container& c, Index i)->decltype(c[i]){
    return c[i];
}
但是这种写法高度依赖decltype推导,其推导对象必须是个变量,不能是表达式(算术表达式或者条件表达式),也不能是复杂的类型等;

C++ 14起则支持直接使用auto推导,如:

1
2
3
4
5
//这也不完美
template <typename Container, typename Index>
auto authAndAccess_auto(Container& c, Index i){
    return c[i];
}
但是这种用法存比authAndAccess_decltype更严重缺陷,即推导引用丢失。

所以你不能给返回值赋值,根据auto和模板推导可知,返回的operator[]大概一个T&,而赋予一个auto值,相当于值递,忽略引用,因此auto和形参接受实参c[i]后,实际上都被推导成T,这是一种右值,不能被赋值:

1
2
3
std::deque<int>dp{1};
authAndAccess_decltype(dp, 0) = 27; //ok
authAndAccess_auto(dp,0) = 27; //无法编译
原书没有说明的疑问是为什么T是右值,int显然不是右值,但是这里的写法类似:
1
2
3
4
5
6
int getx(){
    int x = 27;
    return x;
}

getx() = 27; //绝对错误!
返回的就是一种int右值,因此authAndAccess_auto不能通过编译,如果你需要真正返回一个可赋值对象,按模板编译,你需要使用auto&,此时auto推导的是int,但是会产生一个T&左值对象:
1
2
3
4
5
template <typename Container, typename Index>
auto& authAndAccess_auto_c(Container& c, Index i){     //引用返回
    return c[i];
}
authAndAccess_auto_c(dp,0) = 27; //ok

decltype(auto)

C++14有另外一种写法,满足了我们所有期待,可以这样:

1
2
3
4
5
//这接近完美
template<typename Container, typename Index>
decltype(auto) authAndAccess_decltypeauto(Container& c, Index i){
    return c[i];
}
离谱之间又带着一丝合理,因为auto恰好能完美推导结果类型(无论它是表达式还是条件关系式),而decltype恰好根据该类型返回一个T&(如同在->decltype(c[i])做的那样)。

除了用作函数返回值,decltype(auto)可以作为类别声明使用,如:

1
2
3
4
Widget w;
const Widget& cw = w;
auto myWidget1 = cw;                    //myWidget1的类型为Widget
decltype(auto) myWidget2 = cw;          //myWidget2的类型是const Widget&
当然,在此例中,auto&也能够保留cw的const和引用特性,但是你传入一个原始类型时,你必须又换成auto,而传入一个右值,或者一个来自完美转发的右值返回,你又得换成auto&&,否则你甚至无法完成编译,显然decltype(auto)比较强大且无脑。

对C++ 11,最后比较完美的右值容器模板应该为:

1
2
3
4
template<typename Container, typename Index>
decltype(auto) authAndAccess_decltypeauto_uni(Container&& c, Index i){
    return std::forward<Container>(c)[i];
}
在C++ 11,它必须写成:
1
2
3
4
template<typename Container, typename Index>
auto authAndAccess_decltypeauto_uni(Container&& c, Index i)->decltype(std::forward<Container>(c)[i]){
    return std::forward<Container>(c)[i];
}

decltype古怪细节

最后是decltype的一点古怪细节,正如上文所述,decltype大部分场景下返回简单的类型,但是可能会被一个括号干扰:

1
2
3
4
5
6
7
8
9
10
11
12
13
int x = 27;
decltype(x);   //int
decltype((x));  //int&

decltype(auto) f1(){          //ok
    int x = 0;
    return x;                 //decltype(x)是int,所以f1返回int
}

decltype(auto) f2(){          //极其危险!
    int x = 0;
    return (x);               //decltype((x))是int&,所以f2返回int&
}
C++11认为(x)写法是一个左值,因此如果结合了decltype(auto),那么f2将返回一个局部变量的引用,应该警惕。

条款04:学会查看类型推导的结果

结论1:注意编译器、IDE提供的类型推导有可能是错误的。

对于简单类型,typeid返回类型名称的经过编译器处理的mangled name(经修饰的名称),例如下面代码gcc会分别返回i和PKi,而MSVC会直白地返回int和int const*:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

using namespace std;

int main() {
const int x = 27;
auto y = &x;

cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;

cout << "done" << endl;
return 0;
}
C/C++/Qt 修炼手册的__cxa_demangle类型名转换一节我就阐述过如何将mangled name转换成正常类型名,遗憾的是这只对大部分常用类型有效。

但是如果是一些比较复杂的类型代码耦合产生的对象,如运行这样的代码:

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
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <vector>

using namespace std;

class Widget{
public:
Widget(int num){
_num = num;
}
~Widget(){}
private:
int _num;
};

std::vector<Widget> produce(std::vector<int>& vp){ //通过工厂模式返回一组对象
std::vector<Widget> res;
for(const auto& pos : vp){
Widget* temp = new Widget(pos);
res.emplace_back(*temp);
delete temp;
}
return res;
}

template<typename T>
void f(const T& param){
cout << "T type: " << typeid(T).name() << endl;
cout << "param type: " << typeid(param).name() << endl;
}

int main() {
std::vector<int> vp{1,2,3};
const auto widgetVp = produce(vp);

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

Widget* wid = new Widget(10);
f(wid);
delete wid;

cout << "done" << endl;
return 0;
}
得到输出:
1
2
3
4
T type: PK6Widget
param type: PK6Widget
T type: P6Widget
param type: P6Widget
其中使用工厂模式时,PK6Widget指的是Pointer to const Widget(const Widget*),而使用正常堆区构造,P6Widget指的是Pointer to Widget(Widget*),无论哪一个,两个结果和模板类型推导理论是相悖的,至少T和param的推导结果不应该相同。对于工厂模式,向左值引用传入一个左值常量指针即const Widget,T应该是const Widget,对应的param应该是const Widget* const&(此处为什么不是Widget和const Widget&,在条款01我已经讲过了)。可见,编译器的输出并非是完全可信的。

使用boost库能推导比较准确的结果:

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

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

输出结果:

1
2
3
4
5
T type: Widget const*
param type: Widget const* const&

T type: Widget*
param type: Widget* const&
这个结果是完美符合模板推导理论的。

第二章 auto

本章会覆盖描述关于auto必要的若干使用场景。

条款05:优先考虑auto而非显式类型声明

必须初始化

1
2
int x;   //可能声明一个未定义变量
auto x; //错误,不可能这样声明

闭包

闭包是一个抽象的概念,通常可以理解为Lambda函数,其特点就是能够捕获函数内的其他变量和环境,但是它的返回类型只有编译器才知道,这些特点使得我们专注于函数的功能,而无需太过纠结我们究竟想返回什么样的类型才算正确,例如:

1
2
3
4
5
6
7
8
template <typename It>
void dwim(It b, It e){
while(b != e){
typename std::iterator_traits<It>::value curValue = *b; //curValue的类型来自迭代器指向的元素类型
//who cares?I choose auto:
auto curValue = *b;
}
}

另一方面,因为闭包不会显式说明要返回的类型,auto符合这种惰性习惯,如:

1
2
3
4
5
6
7
8
9
//C++ 11:
auto derefUPLess = [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2){
return *p1 < *p2;
}

//C++ 14中更离谱:
auto derefUPLess = [](const auto& p1, const auto& p2){
return *p1 < *p2;
}
现在你收到一个任务,将所有Lambda函数换成普通函数形式,你会发现束手无策,但C++ 11t提供了std::function和std::bind,是的,这两种形式允许保存或者生成一个闭包(或者任何可调用对象),例如使用前者,你必须写成:
1
2
3
std::function<bool(const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2)> func = 
[](const std::unique_ptr<Widget> &p1, const std::unique_ptr<Widget> &p2){
return *p1 < *p2; };
这种写法导致闭包占用空间更大(有可能使用额外堆区空间)、速度更慢、且可能发生内存泄漏,与auto方法对比毫无优势。尽管是std::bind,在条款34也会发现根本比不上一个Lambda。

意料外的拷贝

auto还可以防止因为某些类型认识的错误,导致冗余的对象创建和销毁,如遍历一个哈希表:

1
2
3
4
std::unordered_map<std::string, int> m;
for(const std::pair<std::string, int>&p : m){
...
}
咋一看好像很勤奋,但是哈希表的pair的键根本不是std::string,而是const std::string,所以为了完成这个类型转换,会特意地拷贝m中的对象作为临时对象,将引用绑定到该临时对象上,当使用完会被销毁,而这本来可以通过使用auto避免。

条款06:auto推导若非己愿,使用显式类型初始化惯用法

前几个条款怂恿开发者无脑使用auto,这个条款是悬崖勒马,一些情况下不掌握推导具体情况而使用有可能踩坑,其中一个坑来自std::vector<bool>;

std::vector<bool>不同于其他std::vector<T>,在C++98时期就被认为一种设计失误。开始时他们希望std::vector<bool>实际占据更少的空间,事实上也做到了,容器内的每个bool对象只占据1比特,而不是1字节。但是代价却很大,因为不能对其取地址,其引用返回的也不是纯粹的bool&,这样的代码是一种错误:

1
2
3
4
//这无法编译:
std::vector<bool> bvp(8,false);
bool* bp = &bvp[0];
bool& bpr = bvp[0];
所以也注定了它不能转换为C风格数组:
1
2
//error:
bool* cbp = bvp.data();

但是C++ 11引入auto后,使得这样的代码通过称为可能:

1
2
std::vector<bool> bvp(8,false);
auto a = bvp[4];
总所周知,一般STL的operator[]返回对象的引用,std::vector<bool>是一种例外,其返回的是一个std::vector<bool>::reference对象(这个类定义于std::vector<bool>中),为了模拟bool&特性,这个reference类中必然含有一个指针以及记录bit的偏移量,这个指针会在语句声明完后被销毁(成为一个悬垂指针),所以任何函数再引用这个auto定义的a变量,会导致UB。

所以首先应该使用std::deque<bool>或其他类型STL替代std::vector<bool>,再者使用显式声明:

1
2
std::vector<bool> bvp(8,false);
bool a = bvp[4]; //还可以

std::vector<bool>不是唯一,std::bitset的std::bitset::reference亦是如此,这种引入代理模范原来特性的,称为代理类,这两个例子的代理都是不可见的(基本不可能看到显式声明一个std::vector<bool>::reference),另外一些代理类是可见的,比如智能指针就完全模拟了指针特性,而且代理类往往可以作为原类的初始化条件,但要注意这些隐含的代理类结合auto都可能导致UB,应该避免。

第三章 拥抱现代C++

未完待续