07:在创建对象时注意区分()
和{}
- 初始化方式
1 2 3 4
int x1(1); int x2 = 2; int x3{3}; // 统一初始化(列表初始化) int x4 = {4}; // 和第三种方式相同
()
和=
初始化的限制()
不能用于non-static成员的初始化- 不能拷贝的对象不能使用
()
初始化
{}
初始化的优点- 禁止基本类型之间的隐式窄化类型转换:比如不能使用double初始化int型变量
- 避免了C++复杂的语法分析:C++’s most vexing parse
1 2 3
Widget w1(10); // 传入一个实参,构造出一个对象 Widget w2(); // 本来想调用无形参的构造函数构造一个对象,但是实际上声明了一个函数 Widget w3{}; // 调用无形参的构造函数,构造出一个对象
{}
的缺陷- auto类型推导中使用
{}
进行初始化,则auto被推断为initializer_list<T>
- 会优先使用形参为
initializer_list<T>
的构造函数,即使其他的构造函数更加匹配- 只有当
{}
中参数无法转换为initializer_list
中类型时,编译器才匹配普通函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class Widget{ public: Widget() {cout<<"0"<<endl;} Widget(int i, int d) {cout<<"1"<<endl;} Widget(int i, bool d) {cout<<"2"<<endl;} Widget(initializer_list<int> il) {cout<<"2"<<endl;} Widget(const Widget& w) {cout<<"copy ctor"<<endl;} Widget(Widget&& w) {cout<<"move copy ctor"<<endl;} operator int() const { cout<<"convert to int"<<endl; return 1; } }; Widget w1{1, true}; // 调用Widget(initializer_list<int> il),即使Widget(int i, bool d)更加匹配 Widget w2{1, 1.0}; // 编译报错,本来调用Widget(initializer_list<int> il),但是使用{}初始化禁止窄化类型转换(存在从double到int的转换) Widget w3{w1}; // 调用Widget(initializer_list<int> il)(中间先将w1转为int),即使Widget(const Widget& w)更加匹配(如果w1无法转换为int,则调用该构造函数) Widget w4{std::move(w1)}; // 调用Widget(initializer_list<int> il),即使Widget(Widget&& w)更加匹配 // 特殊情况: Widget w4{}; // 调用Widget(),而非调用Widget(initializer_list<int> il) Widget w5{{}}; // 调用Widget(initializer_list<int> il),而非调用Widget() Widget w6({}); // 调用Widget(initializer_list<int> il),而非调用Widget()
- 只有当
- auto类型推导中使用
- 使用模板创建对象时,仔细考虑使用
()
还是{}
进行初始化- 标准库函数
std::make_unique
和std::make_shared
也面临着这个问题,它们的解决方案是在内部使用小括号,并将这个决定写进文档中,作为其接口的组成部分。
1 2 3 4 5 6 7 8 9
template <typename T, typename... Ts> void f(Ts&&... params){ // 使用可变参数模板 T localVector1(std::forward<Ts>(params)...); T localVector2{std::forward<Ts>(params)...}; } f<vector<int>>(3,4); // 推断出T=vector<int>, Ts=int // localVector1: 4,4,4 // localVector2: 3,4
- 标准库函数
- 参考
08:优先选用nullptr,而非0或NULL
- 字面量0是一个int,NULL的实现为0L,可以转换为int,bool,
void*
nullptr
可以理解为任意类型的空指针- 使得重载函数的调用明确
- 提高代码的清晰度
- 使用类型推导时,
nullptr
可以隐式转换为任意类型指针
- 参考
09:优先选用别名声明,而非typedef
using
别名的优点:- 清晰,比
typedef
更容易理解 - 可以直接对模板起别名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
template <typename T> using MyAllocList = std::list<T, MyAlloc<T>>; // 如果非要使用typedef,需要包装一层 template <typename T> struct MyAllocList{ typedef std::list<T, MyAlloc<T>> type; }; template <typename T> class Widget{ MyAllocList<T> list1; // list1=std::list<T, MyAlloc<T>>,此时MyAllocList一定是一个别名 typename MyAllocList<T>::type list2; // list2=MyAllocList<T>中的std::list<T, MyAlloc<T>> // 需要使用typename显式说明MyAllocList<T>::type是一个类型,而非数据成员 }
- 清晰,比
- 应用:标准库的
<type_traits>
中提供了一整套用于类型转换的类模板- 虽然C++11中仍然是使用
typedef
实现的,但是C++14中是使用using
声明实现的
1 2
std::remove_const<T>::type // C++11中, 是一个内部包裹typedef的类模板,将T中的const属性移除 std::remove_const_t<T> // C++14中, 是一个类模板中typedef别名的别名,将T中的const属性移除
- 虽然C++11中仍然是使用
- 参考
10:优先选用限定作用域的枚举类型,而非不限作用域的枚举类型
- 无作用域限制的枚举(unscoped enums,C++98)
- 有时使用可能简便一点
1 2 3 4 5 6 7 8 9 10 11 12 13 14
using UserInfo = std::tuple<std::string, std::string, std::size_t> //name, email,age enum UserInfoFields {uiName, uiEmail, uiAge}; UserInfo uInfo; auto email = std::get<1>(uInfo); // 位置1为email auto email = std::get<uiEmail>(uInfo); // 发生隐式类型转换 auto email = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo); // 冗余 // C++14下的辅助类:既想使用有作用限制的枚举,又不想过于啰嗦 template <typename E> constexpr auto toUType(E enumerator) noexcept { return static_cast<std::underlying_type_t<E>>(enumerator); } auto email = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
- 有时使用可能简便一点
- 有作用域限制的枚举(scoped enums,C++11)
- 减少名称污染
1 2 3 4 5
enum unscopedColor{black, white}; auto black = false; // 无作用域限制的枚举,因此枚举类型暴露在{}之外 enum class scopedColor {red, blue}; auto red = false; // 有作用域限制的枚举,枚举类型限制在{}之内,因此减少名称污染
- 有强类型
1 2 3 4 5
enum unscopedColor{black, white}; double d1 = black; // 无作用域限制的枚举,可以发生隐式类型转换 enum class scopedColor {red, blue}; double d2 = static_cast<double>(scopedColor::red); // 有作用域限制的枚举,不会发生隐式类型转换,类型转换需要显式说明
- 可以前向声明:只有在指定底层类型后,才能进行前向声明
1 2
enum unscopedColor: std::uint8_t; // 没有提供默认底层类型 enum class; //默认底层类型为int
- 减少名称污染
- 参考
11:优先选用删除函数,而非private未定义函数
- 背景:编译期会自动生成某些函数,但是有时不需要这些函数;
- C++98的做法:声明为private的,且只声明不定义(effective C++中item6)
- 在private中声明但是不定义,使之在链接阶段因为没有定义而报错
- 在基类中声明为private的,会因为无法拷贝控制派生类中的基类部分,将报错从链接期提前到编译期
- C++11的做法:在声明中标记为
=delete
- 将删除的函数声明为public的,原因是编译器先检查访问权限,再检查delete状态。如果将删除的函数声明为private的,调用删除的函数时,可能报错原因提示是private的;但是更期望的更明确的含义是这些函数是删除的
=delete
可以在任意函数中进行标记,不仅仅局限于成员函数
- 应用:
- 比如可以阻止某些形参的隐式类型转换
1 2
void func(int a); void func(double) =delete; // 因此禁止double和float两种参数的调用(C++总是倾向于将 float 转换为 double)
- 阻止某些模板类型的实例化
1 2 3 4 5 6 7
template <typename T> void func(T* ptr); template <> void func<void>(void* ptr) =delete; struct Widget{ template<typename T> void g(T* ptr); }; template<> void Widget::g<void>(void* ptr) = delete; // 成员模板函数在类外阻止某些类型的实例化
- 比如可以阻止某些形参的隐式类型转换
- 参考
12:给意在改写的函数添加override声明
- 重写override需要满足的条件
- 基类的重写函数必须是虚函数
- 基类和派生类的重写函数
- 函数名(析构函数除外)、形参类型、函数常量性完全相同
- 函数引用限定符完全相同(C++11,函数引用限定符:该成员函数可以被左值对象还是右值对象调用)
- 返回值类型、异常规格说明兼容
- 将重写的函数标记为
override
,如果不满足重写条件则报错 - 参考
13:优先选用const_iterator
,而非iterator
- C++98在容器的成员函数中对
const_iterator
支持有限 - C++11在容器的成员函数中支持
const_iterator
,但是只提供了非成员的begin和end1 2 3 4
template <typename Container> // C++11实现cbegin的方法 auto cbegin(const Container& container) -> decltype(std::begin(container)){ // auto=const Container::iterator& return std::begin(container); }
- C++14提供了非成员的cbegin和cend
- 尽量使用非成员的cbegin和cend,因为某些数据结构(比如数组)没有成员函数cbegin和cend,非成员的cbegin和cend更加通用
|
|
14:只要函数不会抛出异常,就为其加上noexcept
声明
- noexcept 是函数接口的一部分,并且调用者可能会依赖这个接口。
- 相较于 non-noexcept 函数,noexcept 函数有被更好优化的机会。
- noexcept 对于 move 操作、swap、内存释放函数和析构函数是非常有价值的。
- 大部分函数是异常中立的而不是 noexcept。
- 背景:
- C++98中异常规范的局限性:接口的实现一旦被修改,其异常规范可能也变化
- 因此C++11只需要指明接口是否可能抛出异常
- 优点:一个
noexcept
函数有更多编译优化的机会- 不需要保持运行栈为解开的状态
- 不需要保证对象以构造顺序的逆序完成析构
- 应用:如果知道一个函数不会抛出异常,一定要加上
noexcept
noexcept
属性对于移动操作、swap、内存释放函数和析构函数最有价值。C++11 STL 中的大部分函数遵循 “能移动则移动,必须复制才复制” 策略- 默认
noexcept
函数:C++11内存释放函数和所有的析构函数都默认隐式地具备noexcept
属性- 析构函数未隐式地具备
noexcept
属性的唯一情况,就是所有类中有数据成员(包含递归的成员)的类型显式地将其析构函数声明为noexcept(false)
- 如果标准库使用了某个对象,其析构函数抛出了异常,则该行为是未定义的。
- 析构函数未隐式地具备
- 条件
noexcept
:一个函数是否为noexcept
,取决于noexcept
中的表达式是否为noexcept
- 只有被调用的低层次的函数是
noexcept
,高层次的调用方才是noexcept
的
1 2 3 4
template <typename T1, typename T2> struct myPair{ void swap(myPair& p) noexcept( noexcept(swap(first, p.first)) && noexcept(swap(second, p.second)) ); }
- 只有被调用的低层次的函数是
- 异常中立函数:本身不抛出异常,但是调用的函数可能抛出异常,因此不适合标记为
noexcept
- 但是允许
noexcept
函数中调用没有noexcept
保证的函数
- 但是允许
- 通常只为宽松规约提供
noexcept
声明- 宽松规约(wide contract,宽接口):不带前提条件,被调用时不需要关注程序的状态,传入的参数方面没有限制,宽接口的函数永远不会出现未定义的行为
- 狭隘规约(narrow contract,窄接口):带前提条件,如果违反前提条件,则结果是未定义的
- 调用者来保证调用时满足前提条件
- 如果调用时违反前提条件,则抛出异常;如果定义为
noexcept
的,违反前提条件结果是未定义的;相较而言,找出抛出异常的原因相对简单一些
- 参考
15:只要有可能使用constexpr
,就使用它
constexpr
对象:具备const属性,并且在编译期(和链接期)可以确定其值- const对象不能保证在编译期确定其值
constexpr
函数- 含义:
- 如果所有传入 constexpr 函数的参数都能在编译时知道,则结果将在编译时计算出来。
- 如果传入 constexpr 函数的参数有任何一个不能在编译期知道,则结果在运行时计算出来
- 使用
- C++11中,
constexpr
函数有且只能有一条return语句;C++14无此限制 - constexpr 函数被限制只能接受和返回 literal 类型(字面量,非指针和引用,自定义类型也可能是字面量类型的)
- C++11中,如果成员函数修改了操作的对象,或者成员函数的返回值是void的,则该成员函数无法成为
constexpr
的;C++14无此限制
1 2 3 4 5 6 7 8 9 10 11
class Point{ public: constexpr Point(double xVal=0, double yVal=0) noexcept: x(xVal), y(yVal) {} constexpr double getX() const noexcept {return x;} constexpr double getY() const noexcept {return y;} constexpr void setX(double newX) noexcept { x = newX;} // C++14中,移除了两条限制,因此可以设置为constexpr的 constexpr void setY(double newY) noexcept { y = newY;} private: double x, y; }; constexprt Point p1(1.0, 2.0);
- C++11中,
- 含义:
- 参考
16:保证const成员函数的线程安全性
- const成员的好处:不会修改成员变量,而且可以区分重载(const对象和非const对象调用)
- 保证const成员函数的线程安全性
- 使用
std::mutex
,进入临界区锁对象获取互斥量,出临界区析构锁(释放互斥量) - 使用
std::atomic
,但是只能同步单一变量或者内存单元 std::mutex
和std::atomic
都是move-only的
- 使用
- 参考
17:理解特殊成员函数的生成机制
- 特殊成员函数(special member function):
- 一般是public、inline和novirtual的
- 例外:如果基类中的析构函数是virtual的,派生类中的析构函数也是virtual的
- 拷贝构造和拷贝赋值是两个独立的操作
- 移动构造和移动赋值不是独立的操作,如果声明了其中一个,编译器会阻止生成另外一个
- 如果显式申明一个拷贝操作,则两个移动操作不会自动生成
- 一般是public、inline和novirtual的
- 三法则(The Rule of Three):如果声明了{拷贝构造函数、拷贝赋值操作、析构函数}中任意一个,则应该声明所有这三个函数,因为往往意味着类要管理某些资源
- 因此,如果只声明了一个析构函数,编译器应该不会自动生成拷贝操作
- 但实际上编译器还是可能自动生成拷贝操作(历史遗留原因,以及C++11为了兼容历史代码)
- 因此,只有当类中没有声明析构函数、拷贝操作、移动操作,而且需要时,编译器才会生成移动操作
- 如果想让编译器自动生成相关函数(即使违背了这些限制),添加
=default
进行标记 - C++11中对特殊成员函数的生成规则:
- 默认构造函数:同C++98
- 析构函数:本质同C++98,只是默认声明为
noexcept
- 拷贝构造函数:运行期行为同C++98(memberwise 拷贝构造 non-static 成员变量)
- 如果类中声明了一个移动操作,则拷贝构造函数和拷贝赋值运算符被标记为
=delete
的 - 如果类中自定义拷贝赋值运算符或析构函数,可以生成拷贝构造函数,但是已经成为被废弃的方法
- 如果类中声明了一个移动操作,则拷贝构造函数和拷贝赋值运算符被标记为
- 拷贝赋值运算符:规则同拷贝构造函数
- 移动构造函数和移动赋值运算符:仅当类中不包含用户声明的拷贝操作、移动操作和析构函数时才生成
- 特殊情况:成员模板函数不会抑制特殊成员函数的自动生成
1 2 3 4 5
class Widget{ public: template <typename T> Widget(const T &rhs); template <typename T> Widget& operator=(const T& rhs); }; // 编译器仍然会生成copy和move操作,即使可以实例化得到
- 参考