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对象通常大两倍(两个指针分别指向资源和控制块),控制块会产生开销,需要原子性的引用计数修改操作

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
  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值