稳定性与兼容性
断言
1 |
|
异常
如果noexcept修饰的函数抛出了异常,编译器可用选择直接调用std::terminate()函数来终止程序的运行,如析构函数不应该抛出异常,所以默认是noexcept(true)。在c++98中,使用throw()来声明不抛出异常的函数。1
2template <class T>
void fun() noexcept(noexcept(T())) {} // 后面的noexcept是操作符
初始化
初始化列表的效果总是优先于就地初始化的。
非常量的静态成员变量,需要在头文件以外定义,这会保证编译时,类静态成员的定义最后只存在于一个目标文件中。
sizeof
1 | struct People { |
友元
1 | class Poly; |
final/override
如果不想成员函数被重载,可以直接将成员函数定义为非虚的。final通常只在继承关系的中途终止派生类的重载有意义。
派生类在虚函数声明中使用了override描述符,那么该函数必须重载其基类中的同名函数,否则编译不过。
模板函数的默认模板参数
1 | void DefParm(int m = 3) {} // c++98 c++11 pass |
不按照从右往左定义默认类模板参数的模板类都无法通过编译,而函数模板默认模板参数位置则比较随意。
外部模板
可以使用强制实例化进行外部声明。
不使用外部模板声明并不会导致问题,因为只是代码重复,不是数据重复,这是一种对编译时间和空间的优化手段。
通用特性
继承构造函数
如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。1
2
3
4
5
6
7
8
9
10
11struct A {
A(int i) {}
A(double d, int i) {}
A(float f, int i, const char * c) {}
// ...
};
struct B : A {
using A::A; // 继承构造函数,透传构造函数就不需要了
virtual void ExtraInterface() {}
};
参数默认值会导致多个构造函数版本的产生。1
2
3
4
5
6
7
8
9
10
11
12struct A {
A (int a = 3, double b = 2.4) {}
};
struct B : A {
using A::A;
// 类似于:
// B (int, double) {}
// B (int) {}
// B (const B &) {}
// B () {}
};
可以通过显示定义继承类的冲突的构造函数,阻止隐式生成相应的继承构造函数来解决冲突。1
2
3
4
5
6
7
8struct A { A(int) {} };
struct B { B(int) {} };
struct C : A, B {
using A::A;
using B::B;
C(int) {} // 这句可以解决冲突
};
一旦使用了继承构造函数,编译器就不会再为派生类生成默认构造函数了。1
2
3
4struct A { A(int) {} };
struct B : A { using A::A; };
B b; // B没有默认构造函数
委派构造函数
黑客版:1
2
3Info() { InitRest(); }
Info(int i) { new (this) Info(); type = i; } // placement new 强制在本对象上再次调用类的构造函数,但很危险。
Info(char e) { new (this) Info(); name = e; }
所谓委派构造,指的是委派函数将构造的任务委派给了目标构造函数来完成的类构造方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Info {
public:
Info() { InitRest(); }
Info(int i) : Info() { type = i; } // 类似于基类的构造函数,但初始化代码必须放在函数体中
Info(char e) : Info() { name = e; }
private:
void InitRest() { }
int type {1};
char name {'a'};
};
// 优化:
class Info {
public:
Info() : Info(1, 'a') { }
Info(int i) : Info(i, 'a') { }
Info(char e) : Info(i, e) { }
private:
Info(int i, char e) : type(i), name(e) { }
int type;
char name;
};
目标构造函数的执行总是先于委派构造函数,如果委派构造函数中使用try,从目标构造函数中产生的异常,都可以在委派构造函数中被捕捉到。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class DCExcept {
public:
DCExcept(double d)
try : DCExcept(1, d) {
cout << "Run the body." << endl;
}
catch (...) {
cout << "caught exception" << endl;
}
private:
DCExcept(int i, double d) {
cout << "going to throw" << endl;
throw 0;
}
int type;
double data;
};
范型编程版:1
2
3
4
5
6
7
8
9
10class TDConstructed {
template<class T> TDConstructed(T first, T last) : l(first, last) {}
list<int> l;
public:
TDConstructed(vector<short> &v):
TDConstructed(v.begin(), v.end()) {}
TDConstructed(deque<int> &d):
TDConstructed(d.begin(), d.end()) {}
};
右值引用
移动语义
1 |
|
移动构造函数:1
2
3
4HasPtrMem(HasPtrMem &&h) : d(h.d) {
h.d = nullptr; // 将临时值的指针成员置空,否则临时对象会析构掉本是我们“偷”来的堆内存
cout << "Move construct: " << ++n_mvtr << endl;
}
由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。1
2
3T && a = ReturnRvalue(); //通过右值引用的声明,该右值重获新生,只要a活着,右值临时值会一直存活
T b = ReturnRvalue(); // 会多一次析构和构造的开销
const T &f = ReturnRvalue(); // 常量左值引用在C++98标准中就是个“万能”引用,可以指向右值,但“余生”中只能只读
std::move并不移动任何东西,只是将一个左值强制转化为右值引用,继而通过右值引用使用该值,以用于移动语义。1
static_cast<T&&>(lvalue)
移动语义一定是要修改临时变量的值,以下会使得临时变量常量化,成为一个常量右值,使得临时变量的引用也无法修改,导致无法实现移动语义。1
2Moveable(const Moveable&&)
const Moveable ReturnVal();
应该尽量编写不抛出异常的移动构造函数。std::move_if_noexcept在类的构造函数没有noexcept关键字修饰时返回一个左值引用使变量可以实现拷贝语义。1
2
3
4
5
6
7
8
9
10
11
12struct Nothow {
Nothow() {}
Nothow(Nothow&&) noexcept {
std::cout << "Nothow move constructor" << std::endl;
}
Nothow(const Nothow&) {
std::cout << "Nothow move constructor" << std::endl;
}
};
Nothrow n;
Nothrow nt = move_if_noexcept(n);
完美转发
引用折叠规则:一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。1
2
3typedef const int T;
typedef T& TR;
TR& v = 1;
完美转发实现过程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void IamForwording(X& &&t) {
IrunCodeActually(static_cast<X& &&>(t));
}
// 转化为==>
void IamForwording(X& t) {
IrunCodeActually(static_cast<X&>(t));
}
void IamForwording(X&& &&t) {
IrunCodeActually(static_cast<X&& &&>(t));
}
// 转化为==>
void IamForwording(X&& t) {
IrunCodeActually(static_cast<X&&>(t));
}
// forward就是一个static_cast
template<typename T>
void IamForwording(T&& t) {
IrunCodeActually(forward(t));
}
列表初始化
只要#include了<initializer_list>头文件,并且声明了一个以initialize_list1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using namespace std;
enum Gender { boy, girl };
class People {
public:
People(initializer_list<pair<string, Gender>> l) {
auto i = l.begin();
for (; i != l.end(); i++) {
data.push_back(*i);
}
}
private:
vector<pair<string, Gender>> data;
};
People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}};
列表初始化是唯一一种可以防止类型收窄的初始化方式。1
2
3float f{7}; // 可以通过编译
int g{2.0f}; // 收窄,无法通过编译
A obj2{}; // 此时编译器不会将obj2解析为函数声明,而是默认调用A的默认构造函数
POD类型的好处
- 字节赋值,可以使用memset和memcpy对POD类型进行初始化和拷贝操作。
- 提供对C内存布局兼容。
- 保证了静态初始化的安全有效,比如放入目标文件的.bss段,在初始化中直接被赋为0。
易用易学
auto
1 | float radius = 1.7e10; |
如果要使得auto声明的变量是另一个变量的引用,必须使用auto &。1
2
3const double a;
auto d = a; // d: double
auto & e = a; // e: const double &, 可以带走const
声明为引用或指针的auto变量可以带走其对象的相同属性,包括const、volatile.
auto可以用来声明多个变量类型,不过这些变量的类型必须相同,否则编译报错。1
auto o = 1, &p = o, *q = &p; // 从左往右推导为int
不能推导的情况:
- auto不能为函数形参类型。
- 结构体非静态成员变量类型不能是auto。
- 不能声明auto数据。
- vector
v 不行。
decltype
与auto相同,decltype类型推导也是在编译时进行的。
decltype一个最大的用途就是用在追踪返回类型的函数中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15template<typename T1, typename T2>
void Sum(T1 &t1, T2 &t2, decltype(t1 + t2) &s) {
s = t1 + t2;
}
void Sum(int a[], int b[], int c[]) {
// 数据版本
}
int main() {
int a[5], b[5], c[5];
Sum(a, b, c); // 选择数组版本
int d, e, f;
Sum(d, e, f); // 选择模板的实例化版本
}
1 |
|
自动追踪返回值类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21template <typename T1, typename T1>
auto Sum(T1 &t1, T2 &t2) -> decltype(t1 + t2) {
return t1 + t2;
}
int (*(*pf())())() {
return nullptr;
}
// auto (*)() -> int(*)() 一个返回函数指针的函数(假设为a函数)
// auto pf1() -> auto(*)() -> int(*)() 一个返回a函数的指针的函数
auto pf1() -> auto(*)() -> int(*) {
return nullptr;
}
// is_same<decltype(pf), decltype(pf1)>::value
// 用于转发函数
template <typename T>
auto Forward(T t) -> decltype(foo(t)) {
return foo(t);
}
类型安全
强类型枚举
1 |
|