Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Effictive Modern C++

3.理解decltype

C++14 支持decltype(auto),和auto一样,他会从其初始化表达式 出发来推导类型,但是会使用decltype的规则。

c++14:

template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container&& c, Index i)
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

c++11:

template<typename Container, typename Index>
auto
authAndAccess(Container&& c, Index i)
    -> decltype(std::forward<Container>(c)[i])
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

使用decltype需要格外的小心:


int x = 0;
decltype(x)     // 结果是int
decltype((x))   // 结果是int&

/* "(x)" 也是一个lvalue */

decltype(auto) f1()
{
    int x = 0;
}

decltype(auto) f2()
{
    int x = 0;
    ...
    return (x); // decltype((x)) 是int&, 所以f2返回int&
}


4.掌握查看类型推导结果的方法

Boost.TypeIndex 了解下

5.优先使用auto

  • 刀耕火种的时代:

// 使用迭代器的结果来初始化局部变量
template<typename It>
void dwid(It b, It e)
{
    while (b != e) {
        typename std::iterator_traits<It>::value_type
            currValue = *b;
    }
}

  • 使用auto后:
template<typename It>
void dwid(Itb, It e)
{
    while (b != e){
        auto currValue = *b;
    }
}

告别初始化变量的烦恼:

int x1;         // 潜在为初始化的风险

auto x2;        // 编译不同过,必须有初始化物

auto x3 = 0;    // 妙哉

auto 还可以掌握只有编译器能掌握的类型:

// 冷的处理方法
auto derefUPLess =
    [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2)
    { return *p1 < *p2; }

// 最冷的处理方法 in c++14
auto derefUPLess =
    [](auto &p1, auto &p2)
    { return *p1 < *p2; }

类型捷径问题:

32位Windows中std::vector<int>::size_typeunsigned尺寸一样

64位Windows中std::vector<int>::size_type 是64位 unsigned 是32位

使用 auto sz = v.size() 可以降低代码移植成本

std::vector<int> v;
...
unsigned sz = v.size();

规避无心之错引起的类型不匹配

std::unordered_map 的键值部分是const, 方案一原本因该使用const std::pair<const string, int>. 编译器会复制m中的每个对象用以构造一个std::pair<string, int> 临时对象,每次循环迭代结束时,临时对象会被析构一次, 非常浪费资源。

std::unordered_map<std::string, int> m;
...
// 方案一, 灰头土脸且引入'错误'
for (const std::pair<std::string, int>& p : m)
{
    ...
}
// 方案二, 使用auto化解
for (const auto& p : m)
{
    ...
}

6.使用显式类型的初始化习惯用法

features()返回的std::vector<bool> 第五个bit。 代表的意思是: Widget 是否具有优先级, 使用auto无法满足要求,反而会产生一个 悬垂指针,从而导致UB行为。 features(w)返回了一个std::vector<bool>对象,而之后对该对象 进行operator[]操作,返回的是一个std::vector<bool>::reference 类型的对象,auto会把highPriority推导成该对象的类型。 完全不可能是std::vector<bool>第五个比特的值。


std::vector<bool> features(const Widget& w);

Widget w;
...
bool highPriority = features(w)[5];     // w具有高优先级吗?
processWidget(w, highPriority);         // 按照优先级来处理

// 简单替换
auto highPriority = features(w)[5];     // 错误做法

// 正确处理方法
auto highPriority = static_cast<bool>(features(w)[5]);

// 其他显式转换的管用表达方式
double calcEpsilon();
float ep = calcEpsilon()                    // 隐式转换,无法表达“我故意降低了函数返回值精度”
auto ep = static_cast<float>(calcEpsilon()) // 显式转换,表明意图

double d = 27;
int index = d * c.size()                    // 右侧double转换成int的事实含含糊糊
auto index = static_cast<int>(d * c.size()) // 这就显而易见了

7.创建对象是区分()和

统一的大括号的初始化形式

非静态成员变量的默认值可以用={}赋值,不能用() 不可复制对象(例如:std::atomic对象)可以使用{}()而不能使用=

故而使用统一的{}赋值是妙级了的选择??? ({}={}是完全相同的复制方式) 大括号的初始化方式是禁止内建内行的隐式窄化类型转化的

大括号初始化形式并非万能

C++ Most Vexing Parse

Widget w1(10);  // 调用构造函数,传入形参10
Widget w2();    // MVP(most vexing paser) 现身;声明了一个w2函数;
Widget w3{};    // 调用没有形参的构造函数

/* ---------------------------- */
/*
 * 当构造函数中没有任何一个具备
 * std::initialzer_list 类型时
 * 大小括号的意义没有任何区别
 */
/* ---------------------------- */

class Widget{
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    ...
};

Widget w1(10, true);
Widget w2{10, true};
Widget w3(10, 5.0);
Widget w4{10, 5.0};

/* ---------------------------- */
/* 只要任意一个或者多个构造函数声明了
 * std::initialzer_list 类型的形参
 * 采用大括号初始化的语句会强烈地
 * 优先选用带有std::initialzer_list
 * 的重载版本
 */
/* ---------------------------- */

class Widget{
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initialzer_list<long, double> il);  // 增加的构造函数
    ...
};

Widget w1(10, true);

// 调用带有std::initialzer_list的构造函数(10和true被强制转换成long double)
Widget w2{10, true};

Widget w3(10, 5.0);

// 调用带有std::initialzer_list的构造函数(10和5.0被强制转换成long double)
Widget w4{10, 5.0};

/* ---------------------------- */
/* 即使是平常会执行复制或者移动的构造函数也可能被
 * std::initialzer_list类型的构造函数劫持:
 */
/* ---------------------------- */

class Widget{
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initialzer_list<long, double> il);  // 增加的构造函数

    operator float() const;                         // 强制转换成float类型
    ...
};

Widget w5(w4)                                       // 复制构造函数

// 使用大括号
// w4的返回值被强制转换为float
// 随后float又被强制转换为long double(大括号永远的神)
Widget w6{w4}

Widget w7(std::move(w4));                           // 调用移动构造函数

// 调用带有std::initialzer_list的构造函数
// 和w6理由相同
Widget w8{std::move(w4)};

/* ---------------------------- */
/* 只有找不到任何办法把初始化函数形参转换成
 * std::initialzer_list模板中的类型时,
 * 编译器才会退而求其次去检查普通的重载构造函数;
 */
/* ---------------------------- */

class Widget{
public:
    Widget(int i, bool b);
    Widget(int i, double d);

    // std::initialzer_list 模板的元素类型现在没有隐式强制转换了
    Widget(std::initialzer_list<std::string> il);
    ...
};

Widget w1(10, true); // 调用第一个构造函数
Widget w2{10, true};
Widget w3(10, 5.0);
Widget w4{10, 5.0};

另一个用例:

class Widget{
public:
    Widget();

    Widget(std::initialzer_list<std::string> il);
    ...
};

Widget w1;              // 调用默认构造函数
Widget w2{};            // 仍然是默认构造函数
Widget w3();            // MVP 重现江湖!变成函数声明语句了

/*
 * 想调用带有std::initialzer_list的构造函数,
 * 并传如一个空的std::initialzer_list的话,可以这样写:
 */

Widget w4({});
Widget w5{{}};

使用模板时,使用大括号还是小括号是一个棘手的问题

8.优先使用nullptr, 而非0,NULL

避免在整型和指针类型之间重载

9.优先使用别名声明,而不是typedef

STL 好

std::unique_ptr; 好

typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS; 不好

using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

using定义函数指针:

typedef void (*FP)(int, const std::string&);       // 使用typedef
using FP = void (*)(int, const std::string&);      // 使用别名声明

别名可以模板话,而typedef则不能:

/* MyAllocList<T> 是 std::list<T, MyAlloc<T>> 的同义词; */

template<typename T>
using MyAllocList = std::list<T, MyAllocList>;

MyAllocList<Widget> lw;


/* typedef 几乎肯定要自己从头动手 */
/* MyAllocList<T>::type 是std::list<T, MyAlloc<T>> 的同义词; */

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

MyAllocList<Widget> lw;

别名模板可以让人避免写::type后缀,并且在模板内, 对于内嵌typedef的引用经常也要求加上typename前缀。

10.优先使用限定作用域的枚举,而非未限定作用域的枚举

/*
 *  Redefinition of 'black' as different kind of symbol
 *  [clang: redefinition_different_kind]
 */

enum Color { black, white, red };
auto white = false;

/*
 * 由于限定作用域的枚举使用 enum class 声明的,
 * 所以有时候他们也被成为枚举类
 * 同时,枚举类的类型是强制性的,无法转换成整型
 */

enum class Color { black, white, red };
auto white = false;   // correct

/*
 * 枚举类指定底层类型
 */

enum class Status: std::unit32_t        // std::uint32_t 在 <cstdint> 中
enum Status: std::unit32_t              // 不限范围的枚举的前置声明,底层类型为std::uint32_t

enum class Status: std::uint32_t {
    good = 0;
    failed = 10;
    incomplete = 100;
    corrupt = 200;
    audited = 500;
    indeterminate = 0xFFFFFFFF;
}

11.优先使用删除函数而不是private为定义函数

/* c++98  不当的使用只能在链接阶段才能检查出来 */
template <class CharT, class traits = char_traits<charT> >
class basic_ios: public ios_base {
public:
    ...
private:
    basic_ios(const basic_ios&);
    basic_ios& operator=(const basic_ios);

}

/* c++11 当客户尝试访问某个成员函数时
   C++会首先检查其可访问行 */
template <class CharT, class traits = char_traits<charT> >
class basic_ios: public ios_base {
public:
    basic_ios(const basic_ios&) = delete;
    basic_ios& operator=(const basic_ios) = delete;
    ...
}

/* 使用删除函数阻止一些隐式转换 */
bool isLucky(int num);                   // 原始版本
bool isLucky(char) = delete;             // 拒绝char
bool isLucky(bool) = delete;             // 原始版本
bool isLucky(double) = delete;           // 拒绝double 和 float

/* delete函数可以阻止不应该进行的模板具现, private函数则不行 */

template<typename T>
void processPointer(T* ptr);

// void* 与 char* 是两类特殊的指针
template<>
void processPointer<void>(void*) = delete;

template<>
void processPointer<char>(char*) = delete;
// const void*, const char* 也是可能是非法的
template<>
void processPointer<const void>(const void*) = delete;
template<>
void processPointer<const char>(const char*) = delete;

/* 模板特化必须在命名空间作用域而非类作用域撰写 */

// 无法实现的做法
class Widget{
public:
    ...
    template<typename T>
    void processPointer(T* ptr) { ... };
private:
    template<>
    void processPointer<void>(void*)     // 错误, 无法编译
}

// 使用delete函数则可以达到要求
class Widget{
public:
    ...
    template<typename T>
    void processPointer(T* ptr) { ... };
    ...
}

template<>
void Widget::processPointer<void>(void*) = delete;
// 仍然具有public的访问层级,但被删除了

12.为改写的函数添加override声明

派生类如果定义了一个函数和基类中的虚函数名称相同, 但是形参不同,这也是合法行为。编译器会认为新定义的 这个函数与基类原有的函数时相互独立的。这时, 派生类的函数没有覆盖掉基类中的版本,这实际上是 发生了错误,因为我们向覆盖掉基类中的版本。

想要调试发现这种错误非常困难。 但时c++11标准使得我们可以在派生类中使用override 来说明派生类中的虚函数。这样编译器就能发现这类错误。

struct B {
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};

struct C : B {
    void f1(int) const override; // 正确:f1与基类中的f1匹配
    void f2(int) override;       // 错误:B没有形如f2(int)的函数
    void f3() override;          // 错误:f3不是虚函数
    void f4() override;          // 错误: B没有名为f4的函数
}
class Widget {
public:
    using DataType = std::vector<double>;
    DataType& data() &        // 对于左值Widgets类型返回左值;
    { return values; }
    DataType data() &&        // 对于右值Widgets类型,返回右值;
    { return std::move(values); }

private:
    DataType values;
}

auto vals1 = w.data();                 // vals1 采用复制构造完成初始化
auto vals2 = makeWidget().data();      // vals2 采用移动构造完成初始化

13.优先使用const_iterator, 而非iteratoee

c++14: findAndInsert:

template<typename C, typename V>
void findAndInsert(C& container,
                   const V& targetVal,
                   const V& insertVal)
{
    using std::cbegin;
    using std::cend;

    auto it = std::find(cbegin(container),
                        cend(container),
                        targetVal);
    container.insert(it, insertVal);
}

c++11: 一个非成员函数的cbegin实现:

template <class C>
auto cbegin(const C& container) -> decltype(std::begin(container))
{
    return std::begin(container);
}

14.noexcept

  • noexcept 声明是函数接口的一部分,这意味着调用方可能对它有依赖。
  • 相对与不带noexcept 声明的函数,带noexcept的函数有更多机会得到优化。
  • noexcept 性质对于移动操作、swap、内存释放和析构函数最有价值。
  • 大多数函数都是异常中立的,不具备noexcept的性质。

15.constexpr

// constexpr 对象

int sz;                               // 非constexpr 变量
...
constexpr auto arraySize = sz;        // 错误!zs的值在编译期未知
std::array<int, sz> data1;            // 错误!一样的原因
constexpr auto arraySize2 = 10;       // 10 是编译期常量
std::array<int, arraySize2> data2;    // arraySize2 是个 constexpr 对象

int sz;
...
const auto arraySize = sz;            // 没问题, arraySize 是sz的一个const副本
std::array<int, arraySize> data;      // 错误! arraySize 的的值非编译期可知

16.保证const成员函数的线程安全性

  • 保证const成员函数的线程安全性,除非确定他们不会出现在并发语境中;
  • 运用std::atomic类型会比运用互斥量提供更好的性能 , 但仅适用于单个变量或内粗区域的情况。
class Point {
public:
    ...
    double distanceFromOrigin() const noexcept
    {
        ++callCount;       // 带原子性的自增操作;
        return std::sqrt((x*x) + (y * y));
    }
private:
    mutable std::atomic<unsigned> callCount{ 0 };
    double x, y;
}

/*
 * 如果有两个或者更多变量或者内存区域
 * 需要整体作为一个单位操作时,就需要动用互斥量了。
 */

calss Widget{
public:
    ...
    int magicValue() const
    {
        std::lock_guard<std::mutex> guard(m);

        if (cachedValid) return cachedValue;
        else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();
            cachedValue = val1 + val2;
            cachedValid = true;
            return cachedValue;
        }
    }

private:
    mutable std::mutex m;
    mutable int cachedValue;
    mutable bool cachedValid{ false }
}

17.特种成员函数的生成方式

  • Widget(Widget&& rhs) move constructor
  • Widget& operator=(Widget&& rhs) move assignment operator
  • Widget(const Widget& rhs) copy constructor
  • Widget& operator=(Widget& rhs) copy assignment operator

18.std::unique_ptr

std::unique_ptr 可以认为默认情况下和裸指针尺寸相同, 而且与裸指针执行了相同的指令。

std::unique_ptr是只能移动的类型, 不可复制。

std::unique_ptr以两种形式提供std::unique_ptr<T>std::unique_ptr<T[]>

  • std::unique_ptr<T> 不提供索引运算符operator[]

  • std::unique_ptr<T[]> 不提供deferencing运算符: operator*operator->

std::unique_ptr可以方便高效的转换为std::shared_ptr: std::shared_ptr<investment> sp = makeInvestment( arguments )

std::unique_ptr 应用于工厂模式:

/* classes */
class Investment { ... };

class Stock:
    public Investment { ... };

class Bond:
    public Investment { ... };

class RealEstate:
    public Investment { ... };

/* maker */
template<typename... Ts>
std::unique_ptr<investment>
makeInvestment(Ts&&... params);

/* use maker */
{
    ...
    auto pInvestment =
        makeInvestment( arguments );
    ...
}               // destroy *pInvestment

/* custom deleter */
auto delInvmt = [](Investment* pInvestment)
                {
                    makeLogEntry(pInvestment); // write a log
                    delete pInvestment;
                }

template <typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&... params)
{
    std::unique_ptr<Investment, decltype(delInvmt)>
        pInv(nullptr, delInvmt);

    if ( /* a Stock object should be created */ )
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if ( /* a Bond object should be created */ )
    {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if ( /* a RealEstate object should be created */ )
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;
}

/*
 * 当使用自定义析构器时,其返回类型必须制定为std::unique_ptr的
 * 第二个参数。本例为delInvmt的类型。

 * makeInvestment首先创建了一个空的std::unique_ptr指针,
 * 使其指涉到适当的的对象然后返回该指针。

 * 将一个裸指针赋值给std::unique_ptr是被禁止的
 * 这种隐式转换有大问题。
 * 这就是为什上例rest来指定pInv获取使用new产生的对象的所有权。

 * 自定义deleter通过一个基类的指针来删除一个派生类的对象,
 * 因此,基类Investment必须具备一个虚析构函数。

*/

class Investment {
public:
    virtual ~Investment();
}

/* c++14 具有函数返回类型推导 */
template<typename... Ts>
auto makeInvestment(Ts&&... params)
{
    // 现在自定义析构器位于makeInvestment内部了
    auto delInvmt = [](Investment* pInvestment)
                    {
                        makeLogEntry(pInvestment);
                        delete pInvestment;
                    }
    std::unique_ptr<Investment, decltype(delInvmt)>
        pInv(nullptr, delInvmt);
    // 同前
    ...
}

使用自定义析构器之后std::unique_ptr的尺寸:

  • 若析构器是函数指针那么尺寸会增加一到两个字长(word)

  • 若析构器是函数对象,则取决于该函数对象存储了多少状态, 无状态的函数对象(例如无捕获的lambda表达式)不浪费任何尺寸。

// 无状态的lambda表达式作为自定义的析构器
auto delInvmt1 = [](Investment* pInvestment)
                {
                    makeLogEntry(pInvestment);
                    delete pInvestment;
                }

// 使用函数作为自定义析构器
void delInvmt2(Investment* pInvestment)
{
    makeLogEntry(pInvestment);
    delete pInvestment;
}

// 返回尺寸 = Investment*的尺寸 + 至少函数指针的尺寸
template<typename... Ts>
std::unique_ptr<Investment, void(*)(Investment*)>
makeInvestment(Ts&&... params);

18.std::shared_ptr

  • std::shared_ptr的尺寸是裸指针的两倍

    内部包含一个裸指针+一个指涉到该资源的引用计数器

  • 引用计数器的内存必须是动态分配

  • 引用计数器的递增和递减操作必须是原子操作

    原子操作一般都比非原子操作慢,所以即使是引用计数器通常 只有一个字长,也应该假设读写他们的成本是比较高昂的。

  • 自定义析构器不会改变std::shared_ptr的尺寸:

    因为shared_ptr管理的对象有一个控制块除了包含因用计数器外 该控制块还包含了自定义析构器的一个复制。如果指定了自定义 内存分配气,那么控制块也会包含一份它的复制。

    1. std::make_shared 总是创建一个控制块

    2. 从具备专属所有权的指针(std::unique_ptrstd::auto_ptr) 出发构造一个std::shared_ptr时,会创建一个控制块。

    3. std::shared_ptr构造函数使用裸指针作为实参来调用时,会创建一个控制块

    从裸指针构造不只一个std::shared_ptr的话被指涉对象就会有多重控制块。

    多重控制块意味着多重引用计数,进而意味着多重析构。

    UB发动机。

    auto pw = new Widget;                           // 裸指针
    ...
    std::shared_ptr<Widget> spw1(pw, loggingDel);   // 第一个控制块
    ...
    std::shared_ptr<Widget> spw2(pw, loggingDel);   // 第二个控制块
    
    /*
       *pw 有了两个引用计数器,最终引用计数器会清零
       而*pw会被析构两次,第二次是引发未定意行为
    */
    

    应尽量避免将裸指针传递给shared_ptr的构造函数, 使用make_shared

    如果必须将裸指针传递给它的构造函数,就直接传递new运算符的结果,而非裸指针变量

    假设Widge有个成元函数来做这种处理:

    std::vector<std::shared_ptr<Widget>> processWidgets;
    
    class Widget {
    public:
        ...
        void process();
        ...
    };
    
    
    void Widget::process()
    {
        ...
        processWidgets.emplace_back(this);
    }
    
    

    这种将处理完成的Widget加入链表的做法大错特错, 错误的地方在于this指针的使用:把一个裸指针传递 给了一个std::shared_ptr容器。由此构造的std::shared_ptr 将为其所指涉的Widget类型的对象(*this)创建一个新的控制块。 已经指涉到该Widget类型的对象的成员函数外部再套了一层std::shared_ptr。 未定义行为取得了彻头彻尾的胜利.

    std::shared_ptr为这种情况提供了一种基础设施:std::enable_shared_from_this

    // 奇妙递归模板模式CRTP (The Curiously Recurring Template Pattern)
    // 一个派生类的基类是用该派生类作为模板形参具现的
    class Widget: public std::enable_shared_from_this<Widget> {
    public:
        ...
        void process();
        ...
    }
    
    void Widget::process()
    {
        ...
        processWidgets.emplace_back(shared_from_this());
    }
    

自定义析构器:

auto loggingDel = [](Widget* pw)
                {
                    makeLogEntry(pw);
                    delete pw;
                }

// 析构器类型是智能指针类型的一部分
std::unique_ptr<Widget, decltype(loggingDel)>
    upw(new Widget, loggingDel);

// 析构器类型不是智能指针类型的一部分
std::shared_ptr<Widget>
    spw(new Widget, loggingDel);

// shared_ptr 的设计更具弹性
auto customDeleter1 = [](Widget *pw) { ... };
auto customDeleter2 = [](Widget *pw) { ... };

std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);

// 类型相同,可以放到容器中
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };

20.std::weak_ptr

std::weak_ptr不能dereferencing,也不能检查是否为空。

std::weak_ptr不是一种独立的智能指针,而是std::shared_ptr的补充。

std::weak_ptr一般是通过std::shared_ptr来创建的。 当使用std::shared_ptr来完成初始化std::weak_ptr时, 两者就指涉到了相同的位置, 但std::weak_ptr不影响所指涉的对象的引用计数器。

auto spw = std::make_shared<Widget>();
...
/*
   wpw和spw指涉到同一个Widget
   引用计数器保持为1
*/
std::weak_ptr<Widget> wpw(spw);
...

/*
   引用计数器变为0
   Widget对象被析构
   wpw空悬(dangles 又称为expired)
*/
spw = nullptr;

/* 测试spw是否为空悬 */
if (wpw.expired()) ...

/* 需要一个原子操作来完成std::weak_ptr是否失效的检验
   以及未失效的情况下提供对所指涉对象的访问。
   这个操作可以由std::weak_ptr构造std::shared_ptr来完成
*/

std::shared_ptr<Widget> spw1 = wpw.lock(); // 若wpw失效,则spw1为空
auto spw2 = wpw.lock();

std::shared_ptr<Widget> spw3(wpw)          // 如wpw失效,抛出
                                           // std::bad_weak_ptr 异常



/*
    带缓存的工厂模式
*/

std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
    static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;

    auto objPtr = cache[id].lock(); // 如果对象不存在objPtr = nullptr
                                    // 然后loadWidget并加入缓存

    if (!objPtr) {
        objPtr = loadWidget(id);
        cache[id] = objPtr;
    }

    return objPtr;
}

总结:

  • 使用std::weak_ptr代替可能空悬的std::shared_ptr

  • std::weak_ptr可能的用武之地包括缓存,观察者模式, 以及避免std::shared_ptr指针环路

21.优先选用std::make_uniquestd::make_shared,而非直接使用new

  • 省时省力:
auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);
auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

  • 内存安全
void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();

/* processWidget 按值传递,始终会构造一个widget副本
 * (也许是故意设计成这样的) */

// 潜在资源泄漏风险
processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

/* 传递个函数的实参必须在函数调用被发起之前完成求值:
 * 1 "new Widget" 必须完成求值,一个Widget必须先在堆上创建;
 * 2 由"new" 产生的裸指针的托管对象"std::shared_ptr<Widget>"
 *   的构造函数必须执行。
 * 3 computePriority必须执行。
 *
 * 但是编译器不是必须按照上述顺序生成代码,可能生成:
 * 1. new Widget
 * 2. computePriority
 * 3. std::shared_ptr<Widget> 构造函数
 * 此时运行期间computePriority产生了异常,那么第一步new出来的Widget
 * 将会泄漏。
 */

// 使用std::make_shared可以避免这些问题
processWidget(std::make_shared<Widget>(), computePriority());

/* 无论make::shared_ptr与computePriority那个先调用都能避免泄漏
   1. 如果make_shared先调用,指涉widget的裸指针会在computePriority
      调用之前安全的存储在std::shared_ptr中,若computePriority
      出现异常,则widget对象可以被安全的析构。
   2. 如果computePriority先调用并产生了异常,那么make_shared根本
      不会调用。
*/

  • 避免多次分配内存,提升性能
/* 分配了两次内存new Widget分配一次,
   还要为与其关联的控制块分配一次。
*/
std::shared_ptr<Widget> spw(new Widget);

/* 分配单块(single chunck)内存,
   即保存Widget对象,又保存与其关联的控制块。
*/

  • 谨慎选择使用make系列函数的情况

make系列函数不能使用自定义析构器:

auto widgetDeleter = [](Widget* pw) { ... };

std::unique_ptr<Widget, decltype(widgetDeleter)>
 upw(new Widget, widgetDeleter);

std::shared_ptr<Widget>(new Widget, widgetDeleter);

make系列函数会向对象的构造函数完美转发其形参:

auto upv std::make_unique<std::vector<int>>(10, 20);
auto spv std::make_shared<std::vector<int>>(10, 20);
// 都会创建10个元素每个元素都是20的vector,而非{10, 20}

// make系列函数能完美转发大括号初始化的能力,
// 但不能完美转发大括号初始化物

// 一种解决方案
// 利用std::initialzer_list类型的构造函数创建std::vector
auto initList = { 10, 20 };
auto spv = std::make_shared<std::vector<int>>(initList);

std::make_unique仅在上述情况出现问题

std::make_shared还有两种场景

  1. 自定义内存管理的类

  2. 内存紧张的系统、非常大的对象、以及涉及指涉到相同的对象 std::shared_ptr生存期更久的std::weak_ptr

22.使用Pimal习惯用法时,了特殊成员函数的定义放到实现文件中

Pimal (pointer to implementation);

把类的数据成员用一个指涉到具体实现类(或结构体)的指针替代, 而后把原来在主类中的数据成员放置到实现类中,并通过指针 间接访问这些数据成员。

std::unique_ptr:

class Widget {                       // 位于"widget.h"
public:
    Widget();
    ~Widget();
    Widget(Widget&& rhs);            // 仅能声明
    Widget& operator=(Widget&& rhs);

    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs);
    ...
private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};


#include <string>                  // 位于"widget.cpp"

struct Widget::Impl { ... };
Widget Widget(): Pimal(std::make_unique<Impl>())
{};

Widget::~Widget() = default;

Widget::Widget(Widget&& rhs) = default;                 // 不能写在.h文件中,会出现非非完整类型的问题
Widget& Widget::operator=(Widget&& rhs) = default;      // 同上

Widget::Widget(const Widget& rhs)
    : pImpl(std::make_unique<Impl>(*rhs.pImpl))
{}
Widget& Widget::operator=(const Widget& rhs)
{
    *pImpl = *rhs.pImpl;
    return *this;
}

std::shared_ptr:

class Widget {                       // 位于"widget.h"
public:
    Widget();
    ...

private:
    struct Impl;
    std::shared_ptr<Impl> pImpl;
};

// 用户代码
Widget w1;
auto w2(std::move(w1));
w1 = std::move(w2);

总结:

  1. Pimpl降低类和客户和类实现者之间的依赖性,减少了构造次数。

  2. 对于采用std::unique_ptr实现的pImpl指针,须在类的头文件中 声明特种成员函数,但在实现文件中实现它们。即使默认函数实现 有着正确的行为,也必须这样做。

  3. 上述建议仅适用于std::unique_ptr, 并不适用于std::shared_ptr

23.std::movestd::forward

std::move不移动

std::forward不转发

std::move:

  • 如果想取得某个对象执行移动操作的能力,则不要将其声明成常量, 因为针对指针常量对象执行的移动操作将一声不吭的转换成复制操作;

  • std::move 不仅不实际移动任何东西,甚至不保证经过其强制类型转换 后的操作对象具备可移动的能力。唯一保证的时std::move的结果是一个右值;

/*
   text 被声明为const string, 而强制转换的结果时一个右值const std::string,
   经过这番折腾之后常量性质被保留下来了
*/

class Annotation {
public:
    explicit Annotation(const std::string text)
        :value(std::move(text))
    { ... }
private:
    std::string value;
}

std::forward:

  • std::forward是有条件的强制类型转换;

  • 仅当传入的实参被绑定到右值时,std::forward 才针对该实参实施 向右值类型的强制类型转换。

/*
   当且仅当用来初始化param的实参(即传入logAndProcess的实参)
   是个右值的情况下,把param强制转换成右值
*/

void process(const Widget& lvalArg);
void process(Widget&& rvalArg);

template<typename T>
void logAndProcess(T&& param)
{
    auto now = std::chrono::system_clock::now();
    makeLogEntry("Calling 'process'", now);
    process(std::forward<T>(param));
}

24.区分万能引用和右值引用


void f(Widget&& param);            // rvalue reference

Widget&& var1 = Widget();          // rvalue reference
                                   // 不涉及类型推导,var1是一个右值引用

auto&& var2 = var1;                // not rvalue reference

template<typename T>
void f(std::vector<T>&& param);    // rvalue reference
                                   // f被调用时,类型T将被推导,但param
std::vector<int> v;                // 的类型声明不是"T&&", 这就排除了其
f(v);  // 报错                     // 是一个万能引用的可能性。因此param
       // 不能给一个右值引用绑定一个左值

template<typename T>               // not rvalue reference
void f(T&& param);

template<typename T>
void f(const T&& param)            // param 是一个右值引用
                                   // 一个const也足以剥夺T&&
                                   // 成为万能引用的资格

T&&:

  1. 理所当然: 是右值引用。它仅仅会绑定到右值,而其主要存在的理由 在于是被出可移动对象。

  2. 表示既可以是右值引用,亦可以表示时左值引用,二者局一。 带有这种意义的引用在代码中形如右值引用T&&, 但时可以如 左值引用T& 一样运作。这种双重特性使之既可以绑定至右值, 亦可以绑定至左值。甚至可以绑定至const亦或非const对象, 以及volatile或者非volatile对象。 可以绑定万事万物,故而赐名“万能引用“

万能引用现身于两种场景:

  • 函数模板形参

  • auto类型推导

万能引用至出现在类型推导的过程中。哪怕时“位于模板内部” 不涉及类型推导,那就不可能是一个万能引用; 但是具体分析时还要看调用者给定的类型的边界情况分析。

25.针对右值引用实施std::move,针对万能引用实施std::forward

右值引用:

应清楚的知道右值引用绑定的对象可移动。

class Widget {
public:
    Widget(Widget&& rhs)
        :name(std::move(rhs.name)),
        p(std::move(rhs.p))
    { ... }
    ...
private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};


万能引用:

只是可能绑定到可移动的对象上, 只有在使用右值初始化来时才会强制转换成右值引用。

这正是std::forward的用武之地:

class Widget {
public:
    template<typename T>
    void setName(T&& newName)
    { name = std::forward<T>(newName); }
    ...
};

若局部对象可能适用于返回值优化(return value optimization, RVO), 则切勿对其实施std::movestd::forward

RVO的两个前提条件:

  1. 局部对象的类型和函数返回类型相同

  2. 返回的就是局部对象本身

26.避免依万能引用的类型进行重载

  • 形参时万能引用的函数是c++中最贪婪的,几乎和任何实参类型精准匹配

  • 完美转发构造函数问题最为严重,因为对于非常量的左值类型而言, 它们一般都会形成相对于复制构造函数更佳的匹配, 而且它们会劫持对基类的复制和移动构造函数的调用。

class Person {
public:
    template<typename T>
    explicit Person(T&& n)
    : name(std::forward<T>(n)) {}

    explicit Person(int idx);

    // Person(const Person& rhs)  复制构造函数,由编译器生成
    // Person(Person&& rhs);      移动构造函数,由编译器生成
}

Person p("Nancy");
auto p(p);
/* 由p出发创建新的Person类型对象,无法通过编译
   this code won’t call the copy constructor. It will call the perfect-
   forwarding constructor. That function will then try to initialize Person’s
   std::string data member with a Person object (p). std::string having no con‐
   structor taking a Person */


27.熟悉依万能引用类型进行重载的替代方案

  1. 舍弃重载

  2. 传递const T& 类型的形参

  3. 传值

  4. 标签分发

    template<typename T>
    void logAndAdd(T&& name)
    {
        logAndAddImpl(std::forward<T>(name),
                      std::is_integral<typename std::remove_reference<T>::type>());
    }
    
    template<typename T>
    void logAndAddImpl(T&& name, std::false_type)
    {
        auto now = std::chrono::system_clock::now();
        log(now, "logAndAdd");
        names.emplace_back(std::forward<T>(name));
    }
    
    std::string nameFromIndex(int idx);
    void logAndAddImpl(int idx, std::true_type)
    {
        logAndAdd(nameFromIndex(idx));
    }
    
    

    标签分发发挥作用的关键在于,存在一个无需重载的函数 作为客户端api。

  5. 对接受万能引用的模板施加限制 …

滚去搜索:

  • std::enable_if_t

  • std::decay_t

  • SFINAE

经由std::enable_if对模板施加限制,就可以将万能引用和重载一起使用, 不过这种技术控制了编译器可以调用到接受万能引用的重载版本的条件。 万能引用形参通常在性能方面具备优势,但在易用性方面一般会有劣势。

28.理解引用折叠

如果引用的引用出现在允许的语境(例如:模板实例化的过程中), 该双重引用会折叠成单个引用:

如果任一引用为左值引用,结果为左值引用。 否则(即两个引用都是右值引用),结果为右值引用。

std::forward运作的机理:

template<typename T>
void f(T&& fParam)
{
    ...
    someFunc(std::forward<T>(fParam));
}

// std::forward的一种实现:
template<T>
T&& forward(typename remove_reference<T>::type& param)
{
    return static_cast<T&&>(param);
}

/* 加入传递给f的是左值Widget,
   则T会被推导为Widget&类型.
   std::forward调用的实例化版本为:
   std::forward<Widget&> */

// Widget& 插入forward后产生的结果
Widget& && forward(typename remove_reference<Widget&>::type& param)
{
    return static_cast<Widget& &&>(param);
}

// remove_reference后产生的结果
Widget& && forward(Widget& param) { ... }

// 根据引用折叠规则,最终结果
Widget& forward(Widget& param)
{
    return static_cast<Widget&>(param);
}

  • 引用折叠会发生在四种语境中:

    1. 模板实例化

    2. auto自动推导,与模板实例化并无区别

    3. typedef 创建或者评估求值阶段出现引用的引用,引用折叠则会出手消灭之

    4. decltype

  • 万能引用就是在类型推导过程中会区别左值和右值, 以及会发生引用折叠的语境中的右值引用。

29.假定移动操作不存在、成本高、未使用

std::array实际上带有stl接口的内建数组。

std::string,许多实现都采用了小型字符串优化 (small string optimization, SSO)。SSO的“小型“ 字符串会存储在一个std::string对象的某个缓冲区内, 而不去使用堆分配内存。SSO的小型字符串移动并不快于复制。

  • 对于移动类型或者移动语义已知的代码则无需作上述假定。

30.熟悉完美转发的失败情形

template<typename... Ts>
void fwd(Ts&&... params)
{
    f(std::forward<Ts>(params)...)
}

fwd( expression );
f( expression );

给定目标函数f和转发函数fwd,当某特定的实参调用f 会执行某一操作,而用同一实参调用fwd会执行不同的操作, 则称为完美转发失败。

  1. 大括号初始化

    void f(const std::vector<int>& v);
    
    f({1, 2, 3});   // 没问题,会隐式转化为std::vector<int>
    fwd({1, 2, 3}); // 错误,无法编译通过
    
    auto il = {1, 2, 3};  // il 推导类型为std::initialzer_list<int>
    fwd(il);              // 没问题, 将il完美转发给f
    

    fwd({1, 2, 3})问题在于向未声明为std::initialzer_list 类型的函数模板传递了大括号初始化物。称之为“非推导语境”。 通俗解释:fwd的形参未声明为std::initialzer_list, 编译器就会被禁止在fwd调用过程中从表达式{1, 2, 3} 出发来推导类型。

  2. 0和NULL用作空指针

    0或者NULL用作空指针传递给模板 则会推导成整型,而非指针类型。 所以0和NULL都不能用作空指针以 进行完美转发。可用nullptr修正。

  3. 仅有声明的整型static const成员变量

    class Widget {
    public:
        static const std::size_t MinVals = 28;   // 给定了MinVals的声明
        ...
    };
                                                 // 为给定MinVals的定义
    std::vector<int> widgeData;
    widgeData.reserve(Widget::MinVals);          // 此处用到了MinVals
    

    有意个普适的规定: 不需要给出类中的static const 成员变量的定义, 仅需要声明就行。 编译器会根据这些成员的值进行常数传播, 从而不必再为它们保留内存。

    以MinVals直接调用f没问题,编译器会直接用她的值替代参数。

    void f(Widget::MinVals)

    以MinVals由fwd来调用f就出问题了。 对MinVals实施取地址的过程失败。

  4. 重载函数名和模板函数名

  5. 位域

31.避免默认捕获

C++有两种捕获方式:按值捕获、按引用捕获

按引用的默认捕获模式可能产生空悬引用

按至的默认捕获模式会忽悠你,好像可以对空悬引用免疫(其实不能) 让你以为闭包是独立的(实际上不可能独立)

按值的默认捕获极易受空悬指针的影响(尤其是this), 并会让人们认为lambda是自恰的。

32.使用初始化捕获将多个对象移入闭包

初始化捕获(init capture) 又成为广义lambda捕获(generalized lambda capture)

class Widget {
public:
    bool isValidated() const;
    bool isProcessed() const;
    bool isArchived() const;
    ...
private:
    ...
}

auto pw = std::make_unique<Widget>();
... // 配置*pw
auto func = [pw = std::move(pw)]
            {
                return pw->isValidated() && pw->isArchived();
            }

// 如果经由make_unique创建的对象已具备lambda表达式捕获的合适状态

auto func = [pw = std::make_unique<Widget>()]
            {
                return pw->isValidated() && pw->isArchived();
            }

33.对auto&&类型的形参使用decltypestd::forward

泛型lambda表达式(generic lambda)是C++14最令人振奋人心的特性之一, lambda可以在形参规格中使用auto啦。这个特性的实现: 闭包类中的operator()采用模板实现。

例如:

auto f = [](auto x){ return func(normalize(x)); }

闭包类的函数运算符如下:

class SomeComplierGeneratedClassName {
public:
    template<typename T>
    auto operator()(T x) const
    { return func(normalize(x)); }

    ...
}

本例中lambda唯一的作用就是把 x转发给normalize。如果normalize区分 左右值那么上例中的lambda是有问题的, 因为它总是传递左值(形参x)

解决方案:

auto f = [](auto&& param)
{ return func(normalize(std::forward<decltype(param)>())); }

34.优先使用lambda表达式,而非std::bind

35.优先选用基于任务而非基于线程的程序设计

36.如果异步时必须的,则指定std::launch::async

37.使std::thread类型对象在所有路径皆不可联结

38.对变化多端的线程句柄析构函数行为保持关注

39.考虑针对一次性事件通信使用以void为模板类型实参的期值

40.对并发使用std::atomic,对特种内存使用volatile

41.针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递

42.考虑置入而非插入