1 统一初始化/花括号初始化uniform initialization

1.1 特性一:可以在任何地方初始化

非静态成员变量可以用花括号初始化

cpp
1
2
3
4
5
6
7
8
class Widget()
{
private:
int x{ 0 }; //可以
int y = 0; //可以
//成员变量不可以用小括号初始化,有二义性,可能会被认为是函数声明
int z(0); //错误
}

对于不可拷贝的对象进行初始化,也可以用花括号初始化(原子对象不可拷贝)

cpp
1
2
3
4
std::atomic<int> ai1{ 0 }; //可以
std::atomic<int> ai2(0); //可以
//不可拷贝的对象不可以用=初始化
std::atomic<int> ai3 = 0; //错误

1.2 特性二:不允许内置类型间隐式的变窄转换

cpp
1
2
3
4
double x, y, z;
int sum1{ x + y + z }; //错误,不能编译通过
int sum2(x + y + z); //可以
int sum3 = x + y + z; //可以

1.3 特性三:防止默认构造函数被混淆为函数声明

cpp
1
2
3
4
5
6
//可能本意调用Widget的无参构造函数,去创建一个对象w1
//但会被解析为返回类型为Widget,函数名为w1的函数声明
Widget w1();

//使用花括号初始化就能避免这个问题
Widget w2{};

1.4 特性四:花括号返回类型为std::initializer_list导致构造函数被劫持

情况一:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
class Widget
{
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<bool> il);
}

//即使第三个构造函数无法被调用,也会调用第三个导致报错
//调用这个函数会尝试将int(10)和double(5.0)变窄转换为bool
//但花括号初始化拒绝变窄转换,因此无法编译通过
Widget w{10, 5.0};

情况二:

cpp
1
2
3
4
5
6
7
8
9
10
class Widget
{
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<string> il);
}

//这样才会正常调用第二个,因为没有隐式转换能把int和double转换为string
Widget w{10, 5.0};

情况三:

cpp
1
2
3
4
5
6
7
8
9
class Widget
{
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget (std::initializer_list<long, double> il);
}
Widget w{10, true};
//还是会调用第三个,int(10)会转换为long,bool(true)会转换为double

情况四:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Widget
{
public:
Widget();
Widget(std::initialr_list<int> il);
}

//空的花括号意味着没有实参,不是一个空的std::initializer_list
//所以会调用默认构造函数
Widget w1{};

//都会调用第二个构造函数,表示传入一个空的std::initializer_list
Widget w2{{}};
Widget w3({});

1.5 特性五:某些类库中小括号和花括号的初始化差异

cpp
1
2
std::vector<int> v1(10, 20); //创建一个vector,包含10个20
std::vector<int> v2{10, 20}; //创建一个vector,包含两个元素,10和20

2 nullptr

使用场景:

  • NULL在C++中一般被定义为0,而当其被用作指针时,会隐式转换为相应的指针类型,可能会掩盖一些的错误。

  • nullptr则专门用来表示空指针,更安全,可读性也更好。它的真正类型是std::nullptr_t,可以隐式转换为指向任何内置类型的指针。

例1: nullptr在函数重载时的好处

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
#include <iostream>

using namespace std;

void f(int)
{
cout << "exec int" << endl;
}

void f(bool)
{
cout << "exec bool" << endl;
}

void f(void*) //void*是通用指针类型
{
cout << "exec void*" << endl;
}

int main()
{
f(0); //调用f(int)
//f(NULL); //调用f(int)或编译不通过
f(nullptr); //调用f(void*)
return 0;
}

例2: nullptr表意明确

cpp
1
2
3
4
5
6
7
8
auto result = findRecord(); //当不确定函数返回什么时
//根据上下文推断代码作者的意图
//但result == 0就无法确定是拿一个指针类型还是拿一个整型和0比
//进而无法确定result是整型还是指针类型
if (result == 0) {}

//而如果下文是这样,就没有歧义,就可推断出result是一个指针类型
if (result == nullptr) {}

例3: 在模板推导时避免NULL和0被推导为整型的错误

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int f1(std::shared_ptr<Widget> spw);
int f2(std::unique_ptr<Widget> upw);
bool f3(Widget* pw);

template<typename FuncType, typename PtrType>
decltype(auto) callFun(FuncType func, PtrType ptr)
{
return func(ptr);
}

//这三个都能编译通过,前两个分别传0和NULL作为空指针
auto result1 = f1(0);
auto result2 = f2(NULL);
auto result3 = f3(nullptr);

//错误,0被推导为int传给std::shared_ptr<Widget>
auto result11 = callFun(f1, 0);
//错误,NULL被推导为long int传给std::unique_ptr<Widget>
auto result22 = callFun(f2, NULL);
auto result33 = callFun(f3, nullptr);

3 别名声明alias declaration

3.1 基本用法:和typedef一样

假如想定义一个std::unique_ptr<std::unordered_map<std::string>, <std::string>>类型的对象,每次都要写一遍太麻烦了。

解决办法如下:

cpp
1
2
3
4
5
6
7
8
//这两行作用是一样的,用UPtrMapSS这个名字代替一大串
typedef std::unique_ptr<std::unordered_map<std::string>, <std::string>> UPtrMapSS;
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string>, <std::string>>;

//后面再定义对象,直接如下
UPtrMapSS x;
//等价于
std::unique_ptr<std::unordered_map<std::string>, <std::string>> x;

3.2 对比typedef的优势

  1. 在某些情况语义更加明确
cpp
1
2
3
//以下意思都是:FP是一个指向函数的指针的同义项,它指向的函数带有int和const std::string&形参,不返回任何东西
typedef void (*FP)(int, const std::string&);
using FP = void (*)(int, const std::string&);
  1. 别名声明可以被模板化(别名模板alias templates),但typedef不能

要想使MyAllocList<T> lt等价于std::list<T, MyAlloc<T>> lt,别名声明可以直接这样使用:

cpp
1
2
3
4
5
template <typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;

//调用
MyAllocList<int> li;

而typedef要想用MyAllocList<T>表示需要模板的std::list<T, MyAlloc<T>>,必须将其包装在一个结构体中,并且调用也更加麻烦

cpp
1
2
3
4
5
6
7
8
template<typename T>
struct MyAllocList
{
typedef std::list<T, MyAlloc<T>> type;
};

//调用
MyAllocList<int>::type li;

在类模板等场景使用时,由于MyAllocList<T>::type是一个数据类型,且依赖模板参数T,所以是一个依赖数据类型,还需要在前面加typename,更加麻烦了。

而使用using时,编译器知道MyAllocList是一个别名模板,所以知道它一定是一个类型,所以MyAllocList<T>就是一个非依赖类型。

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
//using
template <typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;

//typedef
template<typename T>
struct MyAllocList
{
typedef std::list<T, MyAlloc<T>> type;
};

//在类模板中使用
//typedef
template<typename T>
class Widget
{
private:
typename MyAllocList<T>::type list;
};

//using
template<typename T>
class Widget
{
private:
MyAllocList<T> list;
};

3.3 在C++11中实现C++14中的模板元编程(TMP)

  • 模板元编程:修改类型的常量修饰或引用修饰
cpp
1
2
3
4
5
6
std::remove_const<T>::type //C++11: const T → T
std::remove_const_t<T> //C++14 等价形式
std::remove_reference<T>::type //C++11: T&/T&& → T
std::remove_reference_t<T> //C++14 等价形式
std::add_lvalue_reference<T>::type //C++11: T → T&
std::add_lvalue_reference_t<T> //C++14 等价形式
  • 通过以下别名声明,在C++11中调用std::remove_const_t<T>就等价于std::remove_const<T>::type
cpp
1
2
3
4
5
6
7
8
template <class T>
using remove_const_t = typename remove_const<T>::type;

template <class T>
using remove_reference_t = typename remove_reference<T>::type;

template <class T>
using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;