Effective Modern C++ Notes.1
中文版:https://cntransgroup.github.io/EffectiveModernCppChinese/Introduction.html
1. 类型推导
条款1:理解模板类型推导
- 在模板类型推导的时候,参数的引用特性会被忽略
template<class T>
void f(T& param);
int x = 27; f(x); // T = int
int& rx = x; f(rx); // T = int
- 在推导通用引用时,左值会被特殊处理(引用折叠)
template<class T>
void f(T&& param);
int x = 27; f(x); // T = int&
int& rx = x; f(rx); // T = int&
f(27); // T = int
- 在推导按值传递时,const/volatile参数会被视为非const/volatile
template<class T>
void f(T param);
const int a = 27; f(a); // T = int;
const int& cra = a; f(cra); // T = int
- 在模板类型推导时,参数如果是数组或函数名称,则会退化为指针;如果形参是引用,则会是推导出数组或函数类型
template<class T>
void f(T param);
void func(int); f(func); // T = void(*)(int)
const char name[] = "name"; f(name); // T = const char*
template<class T>
void f(T& param);
void func(int); f(func); // T = void()(int)
const char name[] = "name"; f(name); // T = const char[5]
- 不允许改变函数地址
void f1(int);
void f2(int);
auto& f = f1; // f是f1的引用
f = f2; // 编译出错,f是只读的
auto fptr = f1; // fptr是一个函数指针,指向f1
fptr = f2; // 正常,fptr指向f2
条款2:理解auto类型推导
- auto与模板类型推导类似,唯一区别是对待花括号的行为;当auto声明变量被使用一对花括号初始化时,推导类型是std::initializer_list,模板类型则失败。
auto x = {1,2}; // x的类型是std::initializer_list<int>
template<class T>
void f(T param);
f({1,2}); // 编译失败
- auto在函数返回值或者lambda参数里执行模板类型推导的法则,而不是通常意义的auto类型推导
std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; }
resetV({1,2}); // 编译错误,无法推导出{1,2}的类型
auto x = {1,2}; v = x; // 编译正常, x的类型是std::initializer_list<int>
条款3:理解decltype
- decltype几乎总是得到一个变量或表达式的类型而没有任何修改
const int a = 0; // decltype(a) 的类型就是const int
vector<int> v; // decltype(v[0])的类型就是int&
int x = 1; // decltype(x)的类型是int
- 对于类型为T的左值表达式,decltype总是返回T&
int x = 1; // decltype((x))的类型是int&
- C++14支持decltype(auto),推导类型时使用decltype的规则
decltype(auto) f2() {
int x = 0;
return (x); // decltype((x)) is int&, so f2 returns int&
}
条款4:知道如何查看类型推导
- 可以使用IDE、编译报错信息和boost typeindex库查看,但IDE和编译报错信息可能不准确,建议使用boost typeindex库
#include <boost/type_index.hpp>
template<class T>
void f(T& param) {
cout << " T = " << boost::type_index::type_id_with_cvr<T>().pretty_name() << endl;
cout << " param = "
<< boost::type_index::type_id_with_cvr<decltype(param)>().pretty_name()
<< endl;
}
2. auto
条款5:优先考虑auto而非显示类型声明
- auto可以避免未初始化的错误,以及不清楚正确类型时带来的移植性和效率的问题,也使重构更方便
auto x; // 这是错误的,必须初始化, auto x = 0;
vector<int> v;
unsigned int l = v.size(); // v.size()返回的是vector<int>::size_type而非unsigned int
auto l = v.size(); // auto 可以很好的避免这样的错误
std::unordered_map<std::string, int> m;
for(const std::pair<std::string, int>& p : m) // p的类型应该是pair<const string, int>
{} // 这样会使得拷贝一份,再传给p
for(const auto& p : m) // 这样则不会产生拷贝
条款6:若auto推导类型非期望类型,则使用显式类型初始化
- 不可见的代理类可能会使auto推导出“错误”类型,此时应显式类型初始化
vector<bool> v(2);
auto b = v[0]; // vector<bool> 的[]返回的是vector<bool>::reference类型,而非bool&
auto b = static_cast<bool>(v[0]); // 此时返回bool类型
3. 移步现代C++
条款7:区别使用()和{}创建对象
- 在构造函数重载决议中,编译器会尽最大努力将括号初始化与initializer_list参数匹配,即便其他构造函数看起来是更好的选择
class Widget {
public:
Widget(int i, bool b);
Widget(int i, double d);
Widget(initializer_list<double> il);
};
Widget w1(10,true); // 调用第一个构造函数
Widget w2{10,true}; // 调用initializer_list的构造函数
Widget w3(10,5.0); // 调用第二个构造函数
Widget w4{10,5.0}; // 调用initializer_list的构造函数
- 花括号初始化防止变窄变换,并且对C++的声明解析具有免疫性
vector<int> v{1.0,2}; // 编译错误,narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’
Widget w1; // 调用默认或无参数的构造函数
Widget w2(); // 会被认为是函数声明
Widget w3{}; // 调用默认或无参数的构造函数
条款8:优先考虑nullptr而非0和NULL
- 优先考虑nullptr而非0和NULL
template<class FuncType, class PtrType>
decltype(auto) call(FuncType func, PtrType ptr) {
return func(ptr);
}
int f1(shared_ptr<Widget> spw);
double f2(unique_ptr<Widget> upw);
bool f3(Widget* pw);
f1(0); f2(NULL); f3(nullptr); // 编译均正常
call(f1, 0); // 编译错误,PtrType = int, int类型无法转换为shared_ptr<Widget>
call(f2, NULL); // 编译错误, PtrType是整形,也无法转换为unique_ptr<Widget>
call(f3, nullptr); // 编译正常
- 避免重载整形和指针
void f(int);
void f(void*);
f(0); //调用f(int)
f(NULL); // 调用f(int)
条款9:优先考虑别名声明而非typedef
- typedef不支持模板化,而别名声明支持
template<class T>
using myList = list<T, MyAlloc<T>>;
myList<Widget> lw;
-
别名模板避免使用“::type”后缀,而且在模板中使用typedef还需要在前面加上typename
-
C++14提供了C++11所有type traits转换的别名声明版本
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>::type // C++14
条款10:优先考虑限域enum而非未限域enum
- 限域enum的枚举名仅在enum内可见,要转换为其他类型只能使用cast
enum Color {red, gray, blue}; // 非限域
auto red = 0; // 编译错误,red已经被声明
auto c = red; // 正常
enum class Color {red, gray, blue}; // 限域
auto red = 0; // 编译正常,red是int类型,和Color::red无关
auto c = Color::red; // red仅在Color域内可见
int i = static_cast<int>(Color::blue); // 必须显式转换
- 非限域/限域enum都支持底层类型说明语法,限域enum的底层类型默认是int,非限域enum没有默认底层类型
enum class Color: std::uint32_t {red, gray, black}; // 指定Color的底层类型是uint32_t
enum Color: std::char {red, gray, black}; // 指定非限域Color的底层类型是char
- 因此限域enum可以前置声明,而非限域则不可以前置声明,除非指定底层类型
enum class Color; // 编译正常
enum Color; // 编译错误
enum Color: std::int; // 编译正常
- underlying_type_t获取enum的底层类型
enum class Color: char {red, blue};
using UnderlyingType = std::underlying_type_t<Color>; // UnderlyingType = char
条款11:优先考虑使用delete函数而非使用未定义的私有声明
- 任何函数都能被删除(= delete),包括非成员函数和模板实例
class Widget {
public:
Widget(const Widget& other) = delete;
Widget& operator=(const Widget& rhv) = delete;
};
template<typename T>
void processPtr(T* ptr) { }
template<>
void processPtr<void>(void*) = delete;
条款12:使用override声明重写函数
-
为重写函数加上override,有助于发现错误
-
成员函数引用限定让我们可以区别对待左值对象和右值对象
class Widget {
public:
using DataType = vector<double>;
DataType& data() & // 对于左值Widget, 返回左值
{ return values; }
DataType data() && // 对于右值Widget,返回右值
{ return move(values); }
private:
DataType values;
};
auto v1 = w.data(); // 调用左值重载版本,a是一个Widget对象
auto v2 = factory().data(); // 调用右值重载版本, factory()返回一个Widget对象
条款13:优先考虑const_iterator而非iterator
-
只要它有意义就加上const
-
在最大程度通用的代码中,优先考虑非成员函数版本的begin,end,rbegin等,而非同名成员函数
template<class C, class V>
void findAndInsert(C& container, const V& targetVal, const V& insertVal) {
auto it = std::find(std::cbegin(container), std::cend(container), targetVal);
container.insert(it, insertVal);
}
条款14:如果函数不抛出异常请使用noexcept
-
noexcept函数较之于non-noexcept函数更容易优化
-
noexcept对于移动语义,swap,内存释放函数和析构函数非常有用
-
大多数函数是异常中立的,而不是noexcept
条款15:尽可能的使用constexpr
-
加上constexpr相当于宣称“我能被用于C++要求常量表达式的地方,用于编译期常量的地方”
-
constexpr对象是const,它被在编译期可知的值初始化
-
constexpr函数:如果实参是编译期常量,函数将产出编译期常量;如果实参是运行时才知道的值,函数将产出运行的值
constexpr int pow(int base, int exp) noexcept {
auto ret = 1;
for(int i=0; i<exp; ++i) ret *= base;
return ret;
}
std::array<int, pow(3,5)> nums;
int a,b; ...
int cnt = pow(a,b);
条款16:让const成员函数线程安全
- 确保const成员函数线程安全
条款17:理解特殊成员函数的生成
- 特殊函数:析构函数、构造函数、拷贝操作、移动操作
class Base {
public:
virtual ~Base() = default;
Base() = default;
Base(const Base&) = default; // 支持拷贝
Base& operator=(const Base&) = default;
Base(Base&&) = default; // 支持移动
Base operator=(Base&&) = default;
}
- 拷贝构造函数、拷贝赋值函数仅当类没有显式声明时才自动生成,并且如果声明了移动操作,则拷贝构造函数、拷贝赋值函数就是delete
class A {
public:
using DataType = vector<int>;
A(A&& other): values(move(other.values)) {}
private:
DataType values;
};
A a;
auto b = a; // 编译错误, error: use of deleted function ‘A::A(const A&)’
// ‘A::A(const A&)’ is implicitly declared as deleted
// because ‘A’ declares a move constructor or move assignment operator
auto c;
c = a; // 编译错误,同上
// ‘A& A::operator=(const A&)’ is implicitly declared as deleted
// because ‘A’ declares a move constructor or move assignment operator
- 移动操作仅当类没有显式声明移动操作(移动构造,移动赋值),拷贝操作,析构函数时才自动生成
class A {
public:
using DataType = vector<int>;
A(const A& a): values(a.values) {}
A(A&& other) = default;
private:
DataType values;
};
A a;
A b = move(a); // 如果注释A(A&& other) = default,则调用的是拷贝构造函数
// 不注释则调用移动构造函数
4. 智能指针
条款18:对于独占资源使用unique_ptr
-
unique_ptr是轻量级、快速的、只可移动的管理专有所有权语义资源的智能指针
-
默认情况,资源销毁通过delete,但是支持自定义删除器。有状态的删除器和函数指针会增加unique_ptr的大小,推荐使用lambda表达式
class Investment {
public:
virtual ~Investment();
};
class Stock: public Investment {};
class Bond: public Investment {};
class RealEstate: public Investment {};
template<class... T>
auto makeInvestment(T&&... params) {
auto delInvmt = [](Investment* pInvestment) {
makeLogEntry(pInvestment);
delete pInvestment;
};
unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
if (...) {
pInv.reset(new Stock(forward<T>(params)...));
} else if (...) {
pInv.reset(new Bond(forward<T>(params)...));
} else if (...) {
pInv.reset(new RealEstate(forward<T>(params)...));
}
return pInv;
}
- unique_ptr可以转换为shared_ptr
shared_ptr<int> sptr = unique_ptr<int>(new int(1)); // 正确用法
unique_ptr<int> up(new int(1));
shared_ptr<int> sp = up; // 编译出错,无法赋值
shared_ptr<int> sp = move(up); // 编译正常,不推荐使用
// up的指针将指向nullptr,再通过up访问将会产生未定义行为
条款19:对于共享资源使用shared_ptr
- 较之于unique_ptr,shared_ptr对象通常大两倍(两个指针分别指向资源和控制块),控制块会产生开销,需要原子性的引用计数修改操作
- 默认资源销毁是通过delete,但是也支持自定义删除器,删除器的类型对shared_ptr的类型没有影响
shared_ptr<Widget> pw1(new Widget, [](Widget* pw){...}); // lambda表达式内容不同
shared_ptr<Widget> pw2(new Widget, [](Widget* pw){...}); // 即删除器不同
vector<shared_ptr<Widget>> vpw{pw1, pw2}; // 删除器不同,但shared_ptr对象的类型相同
// 可以被放入同一vector中
- 不要使用原始指针创建shared_ptr,尽量使用make_shared();必须使用原始指针时,应直接传new出来的结果,而非指针变量
auto ptr = new Widget(); // 这种写法错误,ptr可以控制资源
shared_ptr<Widget> pw1(ptr); // pw1可以控制资源
shared_ptr<Widget> pw2(ptr); // pw2可以控制资源,且控制快与pw1的不同,互不影响,会造成多次销毁
shared_ptr<Widget> pw1 = make_shared<Widget>(); // 调用无参的构造函数创建对象,并创建智能指针管理该对象
shared_ptr<Widget> pw2(new Widget); // 直接使用new出来的结果
auto pw3 = pw1; // pw3和pw1指向同一资源
- shared_ptr无法转换为unique_ptr
条款20:shared_ptr可能悬空时使用weak_ptr
-
用weak_ptr替代可能会悬空的shared_ptr
-
weak_ptr的潜在使用场景包括:缓存、观察者列表、打破shared_ptr环状结构
shared_ptr<Widget> fastLoadWidget(WidgetID id) {
static unordered_map<WidgetID, weak_ptr<Widget>> cache;
auto objPtr = cache[id].lock();
if(objPtr == nullptr) { // 不在缓存中
objPtr = loadWidget(id); // 加载它
cache[id] = objPtr;
}
return objPtr;
}
条款21:优先考虑使用make_unique和make_shared,而非直接使用new
-
和直接使用new相比,make函数消除了代码重复,提高了异常安全性。对于make_shared生成的代码更小更快
-
不适合使用make函数的情况包括需要指定自定义删除器和希望用花括号初始化
-
对于shared_ptr,其他不建议使用make函数的情况包括(1)有自定义内存管理的类;(2)特别关注内存的系统,非常大的对象,以及weak_ptr比对应的shared_ptr活得更久
条款22:当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
- 该建议只适用于unique_ptr,不适用于shared_ptr