Effective Modern C++ - 4: 智能指针

part4 智能指针

1 裸指针问题

(1) 没指明 指向 单个对象还是数组

(2) 没指明 是否应该销毁 所指内容

(3) delete形式必须对: delete / delete []; 否则, 结果未定义

(4) 很难确保销毁 准确地沿着代码中的每一条路径(包括由 异常导致的路径)执行一次

    缺少路径 -> 资源泄漏; 多次销毁 -> 未定义行为

(5) 通常无法判断指针是否悬空, 即 是否指向原对象

智能指针 几乎可以做原始指针所能做的一切,但错误使用的机会更少

2 std::auto_ptr 问题

std::auto_ptr copy 动作(copy ctor 和 copy assignment)只在源和目的 auto_ptr都没用 const 修饰 时具有 move 语义; 否则, 编译报错

原因: copy ctor 和 copy assignment 的形参都是 non-const 引用

auto_ptr(auto_ptr& __a) throw() 
    : _M_ptr(__a.release() ) {} 
    
auto_ptr& 
operator=(auto_ptr& __a);

=> auto_ptr 无法用于 容器

C++98/11 容器 push_back 形参类型是 const 左值引用/ 多了 右值引用, push_back 底层对 引用的对象进行 copy 语义操作 => auto_ptr 作实参调 容器 push_back 时, 编译报错

    push_back(const value_type& __x);

    void push_back (value_type&& val); // C++11 新增

3 std::unique_ptr 完成 std::auto_ptr 所做的一切, 以及更多

避免 auto_ptr 会扭曲 copy 对象的含义的问题

item18 std::unique_ptr 用于 独享所有权的资源管理

(1) 不支持 copy 语义, 只支持 move语义: 移动后, 源 unique_ptr 为空

(2) 编译器不支持裸指针(如 new 返回值) 直接赋值给 unique_ptr

解决: 用 reset

移动构造/移动赋值

unique_ptr( unique_ptr&& u ) noexcept;

template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept;
unique_ptr& operator=( unique_ptr&& r ) noexcept;

template< class U, class E >
unique_ptr& 
operator=( unique_ptr<U, E>&& r ) noexcept;

unique_ptr& 
operator=( std::nullptr_t ) noexcept;

(3) unique_ptr 作 STL 容器的元素时, 不能直接传递, 因为不能拷贝和赋值 -> 解决: 用 std::move() 传递

unique_ptr<int> up(new int(20) );
 
vector<unique_ptr<int> > vec;
vec.push_back(std::move(up) );  

(4) unique_ptr 作 STL 容器的元素时, 不能作为类的成员变量

class A
{
private:
    vector<unique_ptr<int> > vec; //错误
};

remember

(1) unique_ptr 小、快、只支持移动、独享所有权

(2) 资源销毁: 默认 用 delete; 第2模板形参可指定 用户 Deleter

(3) std::unique_ptr 可容易、高效地转换为 std::shared_ptr

1 常见用法

(1) 在类层次中, 作 对象的 Factory Method 返回类型

caller 独占 工厂返回的资源

class Investment { … };

class Stock: public Investment 
{ … };
 
class Bond: public Investment 
{ … };
 
class RealEstate: public Investment 
{ … };
template<typename... Ts>        // return std::unique_ptr
    std::unique_ptr<Investment> // to an object created
makeInvestment(Ts&&... params); // from the given args
{
    // ...
    
    auto pInvestment =               // pInvestment is of type  std::unique_ptr<Investment>
        makeInvestment( arguments ); 
    // …
} // destroy *pInvestment

(2) 所有权 迁移场景

路径: 工厂返回资源-> 被移动到容器中 -> 移动到对象的成员 data 中 -> 对象销毁 -> 成员销毁过程中: 先销毁原资源

所有权链 中断(因 异常等原因)时, 也能保证 原资源被销毁: 只要保证管理资源的 unique_ptr 的 Dtor 能被触发

2 第2模板参数可用于指定 Deleter

默认下, 销毁将通过 delete 进行

unique_ptr 还可以指定 Deleter (函数/函数对象/lambda)

(1) Deleter 不含状态时优先选 lambda/函数对象

auto delInvmt = [](Investment* pInvestment) // custom deleter, a lambda expression
{                                           
    makeLogEntry(pInvestment);          
    delete pInvestment;                 
};

通过基类指针 delete 子类对象 => 基类 Dtor 必须为 virtual

class Investment {
public:
    …                       
    virtual ~Investment();  // base dtor must be virtual 
    …                       
};
template<typename... Ts>                                 
    std::unique_ptr<Investment, decltype(delInvmt)> // return type
makeInvestment(Ts&&... params)
{
    // (1) 创建 空 unique_ptr
    std::unique_ptr<Investment, decltype(delInvmt)> 
        pInv(nullptr, delInvmt); 
    
    // (2) 指向合适对象
    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)...));
    }
    
    // (3) 返回
    return pInv;
}

(2) Deleter 含状态时优先选 函数(指针)

void delInvmt2(Investment* pInvestment) // custom deleter as function
{                                       
     makeLogEntry(pInvestment); 
     delete pInvestment;
}

template<typename... Ts>                // return type has
std::unique_ptr<Investment,             // size of Investment*
                void (*)(Investment*)>  // plus at least size
makeInvestment(Ts&&... params);         // of function pointer!

3 管理 单个对象 / 数组unique_ptr<T> / unique_ptr<T[]>

unique_ptr 管理数组通常没有实践价值

std::array / std::vector / std::string 通常总是比原始数组更好

1个例外: 类 C 的 API, 返回一个裸指针指向堆数组

4 unique_ptr 最吸引人的特性: 容易、高效地转换为 shared_ptr

工厂总是返回 unique_ptr(比 shared_ptr 高效), client 需要时转换成 shared_ptr

std::shared_ptr<Investment> sp = // converts std::unique_ptr  to std::shared_ptr
    makeInvestment( arguments );

item19 std::shared_ptr 用于 共享所有权的资源管理

shared_ptr 内存模型.png

1 Deleter 不再作 第2模板形参, 而是作 Ctor 的第2参数

auto loggingDel = [](Widget *pw) // custom deleter
{                                   
    makeLogEntry(pw);
    delete pw;
};
 
std::unique_ptr< Widget,            // deleter type is
        decltype(loggingDel)>       // part of ptr type
    upw(new Widget, loggingDel);
 
std::shared_ptr<Widget>             // deleter type is not
    spw(new Widget, loggingDel);    // part of ptr type
auto customDeleter1 = [](Widget *pw) { … }; // custom deleters,
auto customDeleter2 = [](Widget *pw) { … }; // each with a different type
            
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 };

2 错误用法

(1) 用 裸指针 赋值给 不同的 shared_ptr: 不能 直接赋值

原因: 第2个之后的 shared_ptr 会建立新的 Control Block => 1份资源 对应 多份 RC(引用计数)

auto pw = new Widget; // pw is raw ptr
…
std::shared_ptr<Widget> spw1(pw, loggingDel); // create control block for *pw
…
std::shared_ptr<Widget> spw2(pw, loggingDel); // create 2nd control block for *pw!

解决: 必须通过第1个 shared_ptr 中转

std::shared_ptr<Widget> spw1(new Widget, // direct use of new
                             loggingDel);

std::shared_ptr<Widget> spw2(spw1);     // spw2 uses same
                                        // control block as spw1

(2) 对象自身指针 this, 直接用于 赋给/构造 shared_ptr -> 与 (1) 的问题本质相同

std::vector<std::shared_ptr<Widget>> processedWidgets;

class Widget 
{
public:
    …
    void process();
    …
};

void Widget::process()
{
    … // process the Widget
    processedWidgets.emplace_back(this); // add it to list of
}                                        // processed Widgets;
                                         // this is wrong!

解决: 类继承自 std::enable_shared_from_this + 工厂函数 + this 换成 shared_from_this()

CRTP

class Widget: public std::enable_shared_from_this<Widget> 
{
public:
    // factory function that perfect-forwards args to a private ctor
    template<typename... Ts>
        static std::shared_ptr<Widget> 
    create(Ts&&... params);
    
    …
    void process(); // as before
    …
private:
    … // ctors
};
void Widget::process()
{
    // as before, process the Widget
    …
    // add std::shared_ptr to current object to processedWidgets
    processedWidgets.emplace_back(shared_from_this());
}

CRTP 奇怪的循环模板模式: 子类 作 基类模板实参

(1) 静态多态: 基类 non-virtual 接口函数static_cast 强转 this基类指针, 然后调子类的实现函数

实例化基类时, 编译器已经知道子类的 实现函数声明

template <class T> 
struct Base
{
    void interface()
    {
        // ...
        static_cast<T*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        T::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();

    static void static_sub_func();
};

(2) 子类实例创建和析构独立的计数

item20 std::weak_ptr 用于可能悬挂类似 std::shared_ptr 的指针

(1) weak_ptr -> shared_ptr

[1] 构造: 作 arg 构造

[2] 赋值: lock() 成员函数

(2) shared_ptr -> weak_ptr

[1] 构造: 作 arg 构造

[2] 赋值: 作 arg 赋值

1 监控相应的 std::shared_ptr 所管理的资源 是否 被销毁, 不影响 资源的 RC

std::weak_ptr 悬挂 == 过期( expired ) <=> 资源 被销毁

auto spw =                      // after spw is constructed,
    std::make_shared<Widget>(); // the pointed-to Widget's  ref count (RC) is 1
    
…

std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget as spw.
                                // RC remains 1
…
spw = nullptr;  // RC goes to 0, Widget is destroyed.
                // wpw now dangles
                
if (wpw.expired() ) … // if wpw doesn't point to an object…
                      // true 
std::shared_ptr<Widget> spw1 
    = wpw.lock();        // if wpw's expired, spw1 is null

auto spw2 = wpw.lock();  // same as above, but uses auto

由 weak_ptr 构造 shared_ptr

std::shared_ptr<Widget> spw3(wpw);  // if wpw's expired,
                                    // throw std::bad_weak_ptr

2 weak_ptr + shared_ptr + unique_ptr 融合的例子: 基于 cache(缓存) 的 快速加载

    std::unique_ptr<const Widget> 
loadWidget(WidgetID id);
    std::shared_ptr<const Widget> 
fastLoadWidget(WidgetID id)
{
    // (1) 无序 map: 资源 ID, 资源 的 weak_ptr(监控资源是否被销毁)
    static std::unordered_map<WidgetID,
            std::weak_ptr<const Widget> > cacheControlBlock;
    
    // (2) 取出资源的管理指针: 取出 指定 ID 的 weak_ptr, 赋值给 shared_ptr
    auto objPtr = cacheControlBlock[id].lock(); // objPtr is std::shared_ptr
                                                // to cached object (or null
                                                // if object's not in cache)
    
    // (3) 若 shared_ptr 为空(即 资源不在 cache), 加载资源(用工厂函数), 用缓存控制块管理起来
    if (!objPtr)                            // if not in cache
    { 
        objPtr = loadWidget(id);            // load it
        cacheControlBlock[id] = objPtr;     // cache it
    }
    
    // (4) 返回 资源的管理指针
    return objPtr;
}

3 指针悬挂 & 循环引用

指针悬挂 & 循环引用.png

A/B 内部都有 指针指向对方, 且外部都被 shared_ptr 管理

(1) 1方(如 A)内部应该放 shared_ptr

(2) 另1方(如 B)内部

[1] 裸指针 -> 问题: A 销毁后, B 的指针悬挂, 而 B 检测不到指针悬挂

[2] shared_ptr -> 问题: 循环引用 + A/B 的 RC = 2 => A/B lifetime 结束时, 由于 A/B 的 RC 只能减为 1 => A/B 均内存泄漏

[3] weak_ptr -> A 的 RC = 1, B 的 RC = 2 => A/B lifetime 结束时, A/B 的 RC 减为 0/1, A 销毁, B能检测到自己的指针悬挂 => B 的 RC 减为 0, B 销毁

item21 std::make_unique 和 std::make_shared 优先于直接 new

template<typename T, typename... Ts>
    std::unique_ptr<T> 
make_unique(Ts&&... params)
{
    return std::unique_ptr<T>(
        new T(std::forward<Ts>(params)...) );
}

item22 使用 Pimp 时, 在实现文件中定义 特殊成员函数

remember

(1) Pimp 通过 减少编译依赖性 来 减少构建时间

(2) unique_ptr 用于 pImpl 指针, 必须要在所属类的 头文件中声明/源文件中 unique_ptr 所指类定义之后定义 特殊成员函数(Dtor + Move Ctor/Assignment 或 Copy Ctor/Assignment)

(3) shared_ptr 没有像 unique_ptr 的要求

// "widget.h"
class Widget  
{ 
public:
    Widget();
    …
private:
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3; // Gadget is some user-defined type
}; 

1 pimp 所指 structpimp 所属类的成员, 在 pimp 所属类的头文件中 只声明不定义

// "widget.h"
class Widget    
{ 
public:
    Widget();
    ~Widget(); // dtor is needed—see below
    …
private:
    struct Impl; // declare implementation struct
    Impl *pImpl; // and pointer to it
};

pimp 所指类 在 pimp 所属类的源文件中 定义

// "widget.cpp"
#include "widget.h" // in impl. file "widget.cpp"
#include "gadget.h"
#include <string>
#include <vector>

struct Widget::Impl  // definition of Widget::Impl
{ 
    std::string name; // with data members formerly
    std::vector<double> data; // in Widget
    Gadget g1, g2, g3;
};

Widget::Widget() // allocate data members for
    : pImpl(new Impl) // this Widget object
{}

Widget::~Widget() // destroy data members for
{ delete pImpl; } // this object

2 pimp 从 裸指针 换成 unique_ptr

// in "widget.h"
class Widget 
{ 
public:
    Widget();
    …
private:
    struct Impl; 
    std::unique_ptr<Impl> pImpl; // use smart pointer instead of raw pointer
}; 
// "widget.cpp"
#include "widget.h" 
#include "gadget.h"
#include <string>
#include <vector>

struct Widget::Impl     // as before
{ 
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
};

Widget::Widget()                        // per Item 21, create
    : pImpl(std::make_unique<Impl>())   // std::unique_ptr
    {}                                  // via std::make_unique

3 client 编译报错

#include "widget.h"
Widget w; // error!

原因: 对不完整的类型 sizeof 或 delete

编译器 自动生成的 Dtor 销毁 unique_ptr

[1] 先用 static_assert 保证 unique_ptr 所指对象 Widget::Impl完整的(Dtor 能看到 Widget::Impl 的定义)

默认 Dtor 在 头文件中 inline => 看不到 源文件中 Widget::Impl 的定义 => static_assert fail

[2] 再调 unique_ptr 的 Dtor: delete unique_ptr 管理的 cache

解决: 头文件中显式声明 Dtor, 源文件中 Widget::Impl 定义之后 定义 Dtor

// "widget.h"
class Widget { 
public:
    Widget();
    ~Widget();      // declaration only
    …
private:            // as before
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};
// "widget.cpp"
#include "widget.h" 
#include "gadget.h"
#include <string>
#include <vector>

struct Widget::Impl     // as before
{ 
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
};

Widget::Widget()                        // per Item 21, create
    : pImpl(std::make_unique<Impl>())   // std::unique_ptr
    {}  
    
Widget::~Widget() // ~Widget definition 
    {}
Widget::~Widget() = default; // same effect as above

4 Pimp 自然地支持 移到语义, 但 声明 Dtor 会阻止编译器自动生成 move Ctor/Assignment -> 解决: 显式声明 move Ctor/Assignment

// "widget.h"
class Widget { 
public:
    Widget();
    ~Widget();
    
    Widget(Widget&& rhs);            // declarations
    Widget& operator=(Widget&& rhs); // only
    …
private: // as before
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};
// "widget.cpp"
#include <string> 
…  
struct Widget::Impl { … }; 
Widget::Widget() // as before
    : pImpl(std::make_unique<Impl>() )
    {}

Widget::~Widget() = default;            // as before

Widget::Widget(Widget&& rhs) = default;             // defini-
Widget& Widget::operator=(Widget&& rhs) = default;  // tions

<=>

Widget::Widget(Widget&& rhs)
    : pImpl(rhs.pImpl) {}

Widget& 
Widget::operator=(Widget&& rhs)
{
    pImpl = rhs.pImpl; // unique_ptr 的 移动赋值 
}

5 copy 语义

class Widget { // still in "widget.h"
public:
    … // other funcs, as before
    
    Widget(const Widget& rhs);            // declarations
    Widget& operator=(const Widget& rhs); // only
    
private:                    // as before
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};
#include "widget.h" // as before,
…                   // in "widget.cpp"

struct Widget::Impl { … };          // as before

Widget::~Widget() = default;        // other funcs, as before

Widget::Widget(const Widget& rhs)   // copy ctor
    : pImpl(std::make_unique<Impl>(*rhs.pImpl) )
{}

Widget& 
Widget::operator=(const Widget& rhs) // copy operator=
{
    *pImpl = *rhs.pImpl;
    return *this;
}

6 unique_ptr 换成 shared_ptr

std::shared_ptr Deleter 类型 不是 智能指针类型的一部分 =>

[1] 编译器生成 更大的 运行时数据结构更慢的 运行时代码

[2] 所属类 调用其 Dtor/Copy ctor(Assignment)不要求 shared_ptr 所指 对象是完整(能被看到)的

class Widget { // in "widget.h"
public:
    Widget();
    …               // no declarations for dtor
                    // or move operations
private:
    struct Impl; 
    std::shared_ptr<Impl> pImpl; // std::shared_ptr
};                               // instead of std::unique_ptr
Widget w1;
auto w2(std::move(w1)); // move-construct w2
w1 = std::move(w2); // move-assign w1
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容