Effective Modern / Effective / More Effective C++ Notes

Effective Modern C++

Chap-1类型推导

Item 1 理解模板类型推导

要点

  1. 在模板推导过程中,具有引用类型的实参会被当成非引用类型来处理。换而言之,起引用性会被忽略,指针类型亦是如此
  2. 对万能引用形参进行推导时,左值实参会进行特殊处理,也就是形参对引用性会被保留
  3. 对按值传递的形参进行推导时,若实参类型中带有constvolatile饰词,则对它们还是会被当作不带constvolatile饰词的类型来处理,引用性也会被忽略
  4. 在模板类型推导过程中,数组或函数类型的实参会退化成对应的指针,除非它们被用开初始化引用
1
2
3
template<typename T>
void f(ParamType param); // ParamType包含一些饰词,包含const,引用,指针或者万能引用
f(expr); // 以某表达式调用f

情况1,ParamType是个引用或指针,但不是个万能引用

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 f(T& param);

int x = 27;
const int cx = x;
const int& rx = x; // rx的引用性会被忽略

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

void f(const T& param);

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

template<typename T>
void f(T* param);

const int *px = &x;

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

上述代码中T未被推导为引用或者指针,因为其引用性/指针性在推导过程中被忽略 – 要点1

情况2,ParamType是个万能引用

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

f(x); // x是左值,T的类型是int,param的类型是int&
f(cx); // cx是左值,T的类型是const int&,param类型时const int&
f(rx); // rx是左值,T的类型是const int&,param的类型是const int&
f(27); // 27是个右值,T的类型是int,param类型是int&&

上述代码中T得引用性被保留 – 要点2

情况3,ParamType既非指针也非引用

传值

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

f(x); // T和param的类型都是int
f(cx); // T和param的类型还都是int
f(rx); // T和param的类型还都是int

const char* const ptr = “Fun with pointers”
f(ptr); // param的类型是const char*

上述代码中传值方式的param是一个副本,忽略其const和引用性 – 要点3

数组实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const char name[] = "J. P. Brigss";	// name的类型是const char[13]
const char * ptrToName = name; // 数组退化为指针

template<typename T>
void f(T param);
f(name);

// 下面两个等价
void myFunc(int param[]);
void myFunc(int* param);

f(name); // name是数组,但是T的类型被推导为const char*

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

f(name); // 向f传递一个数组,此时T会被推导实际的数组类型,类型中包含数组size,即T的类型是const char [13]

数组引用的模板类型可以被推导成数组类型,包含数组size,利用该特性可以在编译时推导出数字个数 – 要点4

以下函数被声明为constexpr,以编译时常量形式返回数组size

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

int keyVals[] = {1, 2, 3, 4, 5, 6, 7};
int mappedVals[arrayzSize(keyVals)];

函数实参

1
2
3
4
5
6
7
8
9
10
void someFunc(int, double);

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

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

f1(someFunc); // param类型为函数指针,void (*)(int, double)
f2(someFunc); // param类型为函数引用,void (&)(int, double)

Item 2 理解auto类型推导

要点

  1. 一般情况下,auto类型推导和模板类型推导是一模一样的,但是auto类型推导会假定用大括号括起的初始化表达式代表一个std::initializer_list,但是模板类型推导却不会
  2. 在函数返回值或lambda式的形参中使用auto,意思是使用模板类型推导而非auto类型推导

auto类型推导类似模板参数推导,可以参照Item 1中的推导规则,也分3类情况

  • 情况 1: 类型饰词是指针或引用,但不是万能引用
  • 情况 2: 类型饰词是万能引用
  • 情况 3: 类型饰词既非指针也非引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto x = 27;		// 情况3,x类型是int
const auto cx = x; // 情况3,cx类型是int
const auto& rx = x; // 情况1,rx类型是int&

auto&& uref1 = x; // x类型是int,且是左值,所以uref1类型是int&
auto&& uref2 = cx; // cx类型是const int,且是左值,所以uref2类型是const int&
auto&& uref3 = 27; // 27的类型是int,且是右值,所以uref3的类型是int&&

auto arr1 = name; // arr1的类型是const char*
auto& arr2 = name; // arr2的类型是const char (&)[13]

auto func1 = someFunc; // func1的类型是void (*)(int, double),函数指针
auto& func2 = someFunc; // func2的类型是void (&)(int, double),函数引用

auto x3 = { 27 }; // 类型是std::initializer_list<int>
auto x = {11, 23, 9}; // x类型是std::initializer_list<int>

模板推导无法推导出{}的类型,auto类型推导可以,但是auto是返回值或lambda形参时使用模板推导

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

f({11, 23, 9}); // 错误!无法推导出T的类型,

auto createList() {
return {1, 2, 3};
}

std::vector<int> v;
auto resetVec = [&v](const auto& newValue) { v = newValue; };
resetVec({1, 2, 3}); //错误!无非为{1, 2, 3}完成类型推导

Chap-3 转向现代C++

Item 7 在创建对象时注意区分()和{}

要点

  1. 大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化型转换,还对C++最令人苦恼的解析语法(most vexing parse)免疫
  2. 在构造函数重载决议期间,只要有任何可能,大括号初始化就会与带有std::initializer_list型别的形参相匹配,即使其他重载版本有着貌似更加匹配的形参表
  3. 使用小括号还是大括号,会造成结果大相径庭的一个例子是:使用两个实参来创建一个std::vector< 数值类型 >对象。
  4. 在模板内容进行对象创建时,到底应该使用小括号还是大括号会成为一个棘手问题。

C++11引入了统一初始化,概念上是可以用于一切场合,表达一切意思的初始化。它的基础是大括号形式,“统一初始化”是为其里,“大括号初始化”是为其表。

1
2
3
4
5
6
7
8
int x(0);	// 初始化物在小括号内
int y = 0; // 初始化物在等号之后
int z{ 0 }; // 初始化物在大括号内
Widget w1; // 调用的是默认构造函数
Widget w2 = w1; // 并非赋值,调用的是复制构造函数
w1 = w2; // 并非赋值,调用的是复制赋值运算符
std::vector<int> v{1, 2, 3, 4, 5}; // 初始化v的内容为1 2 3 4 5,不能使用小括号
std::atomic<int> ai1 = 0; // ERROR!不可复制对象可以采用{}, ()初始化,但是不能使用“=”

使用大括号会进行窄化检查,避免苦恼解析,参考要点1

1
2
3
4
5
6
double x, y, x;
int sum1{x + y + z}; // Error! double无法用int表达

Widget w1(10); // 调用Widget的构造函数,传入行参10
Widget w2(); // 这个语句声明了一个名为w2、返回一个Widget类型对象的函数
Widget w3{}; // 调用没有形参的Widget构造函数

大括号初始化的缺点是会伴随意外行为,要点2,3

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
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
};

Widget w1(10, true); // 调用第一个构造函数
Widget w2{10, true}; // 调用第一个构造函数
Widget w3(10, 5.0); // 调用第二个构造函数
Widget w4{10, 5.0}; // 调用第二个构造函数

// 有构造函数带有std::initializer_list形参,使用大括号会优先选用带有std::initializer_list形参的重载版本
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<long double> il);

operator floag() const;
};
Widget w1(10, true); // 调用第一个构造函数
Widget w2{10, true}; // 使用大括号,调用的是带有std::initializer_list形参的构造函数,10和true被强制转换成long double

/// 执行Copy或Move的构造函数也可能被带有std::initializer_list形参对构造函数劫持
Widget w5(w4); // 使用小括号,调用复制构造函数
Widget w6{w4}; // 使用大括号,调用带有std::initializer_list形参的构造函数,w4被强制转换成float然后被强制转换成long double
Widget w7(std::move(w4)); // 使用小括号,调用移动构造函数
Widget w8{std::move(w4)}; // 使用大括号,同w6

Item 8 优先选用nullptr,而非0或NULL

要点

  1. 相对于0或NULL,优先选用nullptr。
  2. 避免在整型和指针类型之间重载。