读书笔记:Effective Modern C++
在读了大半本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
5template<typename T>
void f(ParamType param);
//调用类似:
f(expr);
这个问题看起来很抽象,实际上你肯定考虑过: 1
2void func(int& c)
void func(int &c)
这里我们直接结合原书和之前的结论重新给出。
情况一:形参是非万能引用
结论1: 形参类型是左值引用、右值引用、指针类形式,那么T都是非引用,形参类型取决于匹配到什么类型;
如左值引用情况: 1
2
3
4
5
6
7
8
9
10template<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&1
2
3
4
5
6
7
8
9
10template<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
11template<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
8template<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*
情况二:形参是万能引用
结论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
25template<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
10template<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的类型都是intconst 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
15template <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
4const 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
2template <typename T>
void func(T& param)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template <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和#41
2
3
4
5
6
7
8
9
10template <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
11template <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像条款01的情况二一样调用万能引用: 1
2
3auto&& uref1 = x;
auto&& uref2 = cx; //实参为左值,auto和uref1、uref2均是左值引用类型(int&)
auto&& uref3 = 27; //实参为右值,auto是int类型,uref3是右值引用类型(int&&)
数组、函数指针表现也与模板推导一致: 1
2
3
4
5
6
7const 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
6void func(int, double);
auto& func2 = func; //引用传递,func2类型是void(&)(int,double)
//错误:不允许置空或二次赋值!
func2 = nullptr;
func2 = func_else;
C++11的统一初始化
auto和模板推导存在一个例外的区别:auto支持统一类型的花括号推导,而模板推导必须显式指明std::initializer_list
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};1
2
3
4
5auto x = 27;
auto x(27); //都是int
auto x = {27};
auto x{27}; //都是std::initializer_list<int>
auto可以使用列表推导,但是要求列表元素必须同一种数据类型:
1
2auto list = {1, 2, 3, 4}; //ok,推导为std::initializer_list<int>
auto list1 = {1, 2, 3.3f}; //无法编译
而在模板中,无论列表元素类型相不相同,都无法直接使用列表推导,只能显式使用std::initializer_list1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20template <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
5void doSomething(std::initializer_list<int> lst){
for(int x : lst){
...
}
}1
2
3
4
5void doSomething(std::initializer_list<Person> lst){
for(const auto& p : Person){
...
}
}1
2
3
4
5
6
7auto 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
27const 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::vectorstd::_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];
}
C++ 14起则支持直接使用auto推导,如: 1
2
3
4
5//这也不完美
template <typename Container, typename Index>
auto authAndAccess_auto(Container& c, Index i){
return c[i];
}
所以你不能给返回值赋值,根据auto和模板推导可知,返回的operator[]大概一个T&,而赋予一个auto值,相当于值递,忽略引用,因此auto和形参接受实参c[i]后,实际上都被推导成T,这是一种右值,不能被赋值:
1
2
3std::deque<int>dp{1};
authAndAccess_decltype(dp, 0) = 27; //ok
authAndAccess_auto(dp,0) = 27; //无法编译1
2
3
4
5
6int getx(){
int x = 27;
return x;
}
getx() = 27; //绝对错误!1
2
3
4
5template <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];
}
除了用作函数返回值,decltype(auto)可以作为类别声明使用,如:
1
2
3
4
5
6
7Widget w;
const Widget& cw = w;
auto myWidget1 = cw; //myWidget1的类型为Widget
decltype(auto) myWidget2 = cw; //myWidget2的类型是const Widget&
对C++ 11,最后比较完美的右值容器模板应该为: 1
2
3
4template<typename Container, typename Index>
decltype(auto) authAndAccess_decltypeauto_uni(Container&& c, Index i){
return std::forward<Container>(c)[i];
}1
2
3
4template<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
13int 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&
}

