Skip to the content.

C++智能指针 - fluentcpp.com

Contact me


英文文件见拷贝Cpp_smart_pointers_ebook,原作者网站 fluentcpp.com

笔记:

unique_ptr​,​shared_ptr​,​weak_ptr​, scoped_ptr​, raw pointers

std::unique_ptr - OWN

std::unique_ptr<const House> buildAHouse();
// for some reason, I don't want you
// to modify the house you're being passed
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = move(p1); // now p2 hold the resource
// and p1 no longer hold anything

Raw pointers - ACCESS

std::unique_ptr<House> house = buildAHouse();
renderHouse(*house);

std::shared_ptr - HOLD

std::weak_ptr - ACCESS

void useMyWeakPointer(std::weak_ptr<int> wp)
{
    if (std::shared_ptr<int> sp = wp.lock())
    {
    // the resource is still here and can be used
    }
    else
    {
    // the resource is no longer here
    }
}
struct House
{
    std::shared_ptr<House> neighbour;
};
std::shared_ptr<House> house1 = std::make_shared<House>();
std::shared_ptr<House> house2 = std::make_shared<House>();;
house1->neighbour = house2;
house2->neighbour = house1;

boost::scoped_ptr(不在标准中)和std::auto_ptr(废弃)【略】

How to implement the pimpl idiom by using ​unique_ptr

The pimpl

假设有个冰箱类,里面有个发动机。

#include "Engine.h"
class Fridge
{
public:
    void coolDown();
private:
    Engine engine_;
};
#include "Fridge.h"
void Fridge::coolDown()
{
/* ... */
}

这个设计有个问题,由于Fridge.h 引入了 Engine.h,每个Fridge的客户类都间接引入了Engine类,因此如果Engine改动,多有的客户类Fridge都需要重新编译,即使他们并没有直接使用Engine

pimpl的解决方法是,加入一个间接层FridgeImpl:

class Fridge
{
public:
    Fridge();
    ~Fridge();
    void coolDown();
private:
    class FridgeImpl;
    FridgeImpl* impl_;
};
#include "Engine.h"
#include "Fridge.h"
class FridgeImpl
{
public:
    void coolDown()
    {
    /* ... */
    }

private:
    Engine engine_;
};

Fridge::Fridge() : impl_(new FridgeImpl) {}

Fridge::~Fridge()
{
    delete impl_;
} 

void Fridge::coolDown()
{
    impl_->coolDown();
} 

原理是指针只需要一个前置声明就可以编译,头文件就不需要看到FridgeImpl的完整定义。

Using std::unique_ptr to manage the life cycle

一个直接的想法就是拿std::unique_ptr替换掉原来原始指针来做:

头文件:

#include <memory>
class Fridge
{
public:
    Fridge();
    void coolDown();
private:
    class FridgeImpl;
    std::unique_ptr<FridgeImpl> impl_;
};

实现:

#include "Engine.h"
#include "Fridge.h"
class FridgeImpl
{ 
public:
    void coolDown()
    {
    /* ... */
    }
private:
    Engine engine_;
};

Fridge::Fridge() : impl_(new FridgeImpl) {}

但是会存在一个问题:

use of undefined type 'FridgeImpl'
can't delete an incomplete type

Destructor visibility

在delete指针出现未定义行为的情况有:

std::unique_ptr会在析构函数里检查类型是否可见才能管理资源,因此上述情况会遇到问题。

因此修改:声明析构器,阻止编译器为我们生成,因此头文件为:

#include <memory>
class Fridge
{ 
public:
    Fridge();
    ~Fridge();
    void coolDown();
private:
    class FridgeImpl;
    std::unique_ptr<FridgeImpl> impl_;
};

然后需要把析构器的实现放在FridgeImpl的实现后面:

#include "Engine.h"
#include "Fridge.h"
class FridgeImpl
{ 
public:
    void coolDown()
    {
    /* ... */
    }
private:
    Engine engine_;
};

Fridge::Fridge() : impl_(new FridgeImpl) {}

Fridge::~Fridge() = default;

How to Transfer ​unique_ptr​s From a Set to Another Set

两个​unique_ptr​s迁移很容易:

std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2;
p2 = std::move(p1); // the contents of p1 have been transferred to p2

但是​unique_ptr​s的集合迁移就不是很容易了。

Sets of unique_ptrs: unique and polymorphic

假设Derived类继承自Base类这种清理,为了多态,我们使用std::unique_ptr<Base>。然后我们就可以使用std::unique_ptr<Base>的集合,为了避免重复,使用std::set: std::set<std::unique_ptr<Base>>

元素之间的比较会调用​std::unique_ptr​operator<,用于比较内存地址。但是这不是我们想要的,我们希望的是两个元素没有相同的值,而不是相同的地址。因此我们需要自定义比较:

struct ComparePointee
{
    template<typename T>
    bool operator()(std::unique_ptr<T> const& up1, std::unique_ptr<T>
    const& up2)
    {
        return *up1 < *up2;
    }
};

std::set<std::unique_ptr<int>, ComparePointee> mySet;

为了便于使用:

template<typename T>
using UniquePointerSet = std::set<std::unique_ptr<T>,ComparePointee>;

Transferring unique_ptrs between two sets

UniquePointerSet<Base> source;
source.insert(std::make_unique<Derived>());
UniquePointerSet<Base> destination;

destination.insert(begin(source), end(source));

但是会出错:

error: use of deleted function 'std::unique_ptr<_Tp,
_Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp =
Base; _Dp = std::default_delete<Base>]'

这表示insert函数要采用拷贝元素。

C++17’s new method on set: merge

set和map在c++内部用树实现。c++17提供了合并功能:

destination.merge(source);

可以让destination接管source的树节点,之后source为空。由于只有节点修改了,节点内部没有修改,因此​​unique_ptr​s对此毫无察觉,甚至也没有移动。

但是c++17之前怎么办?

We can’t move from a set

在集合间移动元素的标准做法是​std::move

std::vector<std::unique_ptr<Base>> source;
source.push_back(std::make_unique<Derived>());
std::vector<std::unique_ptr<Base>> destination;
std::move(begin(source),end(source),std::back_inserter(destination));

之后,destination拥有了元素,source不为空,而是拥有一堆空的​unique_ptr​

同样用于集合:

UniquePointerSet<Base> source;
source.insert(std::make_unique<Derived>());
UniquePointerSet<Base> destination;
std::move(begin(source), end(source), std::inserter(destination,
end(destination)));

错误为:

error: use of deleted function 'std::unique_ptr<_Tp,
_Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)

这表明要进行拷贝,奇怪的是明明用的move为什么会拷贝/

这是集合访问元素的特性,解引用的时候,集合的迭代器不会返回​unique_ptr&,而返回const ​unique_ptr&,这是为了保证集合元素不被修改。因此完整的情况为:

因此,​const unique_ptr&调用了拷贝构造,导致了编译错误。

Making a sacrifice

因此在c++17 之前,在集合之间移动元素看起来不可能,或者不使用move或者不使用集合。

Keeping the set but paying up for the copies

class Base
{ 
public:
    virtual std::unique_ptr<Base> cloneBase() const = 0;
    // rest of Base...
};

class Derived : public Base
{ 
public:
    std::unique_ptr<Base> cloneBase() const override
    {
        return std::make_unique<Derived>(*this);
    } 
// rest of Derived...
};

复制的时候可以使用自定义的函数。

auto clone = [](std::unique_ptr<Base> const& pointer){ return
pointer->cloneBase(); };
std::transform(begin(source), end(source), std::inserter(destination,
end(destination)), clone);

或者:

for (auto const& pointer : source)
{
    destination.insert(pointer->cloneBase());
}

Keeping the move and throwing away the set

使用vector:

std::vector<std::unique_ptr<Base>> source;
source.push_back(std::make_unique<Derived>(42));
std::set<std::unique_ptr<Base>> destination;
std::move(begin(source), end(source), std::inserter(destination,
end(destination)));

由于vector的迭代器返回值不加const,因此也没有上面的问题。

Custom deleters

class House
{
public:
    explicit House(std::unique_ptr<Instructions> instructions)
    : instructions_(std::move(instructions)) {}
    House(House const& other)
    : instructions_(other.instructions_->clone()) {}
private:
    std::unique_ptr<Instructions> instructions_;
};

这里Instructions就需要支持多态的克隆:

class Instructions
{
    public:
    virtual std::unique_ptr<Instructions> clone() const = 0;
    virtual ~Instructions(){};
};
class Sketch : public Instructions
{
public:
    std::unique_ptr<Instructions> clone() const
    {
        return std::unique_ptr<Instructions>(new Sketch(*this));
    }
};
class Blueprint : public Instructions
{
public:
    std::unique_ptr<Instructions> clone() const
    {
        return std::unique_ptr<Instructions>(new Blueprint(*this));
    }
};

构建house的方法为:

enum class BuildingMethod
{
    fromSketch,
    fromBlueprint
};
House buildAHouse(BuildingMethod method)
{
    if (method == BuildingMethod::fromSketch)
        return House(std::unique_ptr<Instructions>(new Sketch));
    if (method == BuildingMethod::fromBlueprint)
        return House(std::unique_ptr<Instructions>(new Blueprint));
    throw InvalidBuildMethod();
}

如果对象来自于其他内存资源,情况就变得复杂了,例如来自栈的对象:

Blueprint blueprint;
House house(???); // how do I pass the blueprint to the house?

我们无法把unique_ptr绑定到一个栈对象上,毕竟无法delete。一个解决方法是拷贝到堆里,但是成本比较大。有没有其他办法呢?

Seeing the real face of ​std::unique_ptr

完整的类型包括删除器:

template<
    typename T, typename Deleter = std::default_delete<T>
> class unique_ptr;

因此一个可行的方法是自定义删除器:

struct GizmoDeleter
{
    void operator()(Gizmo* p)
    {
        oldFunctionThatDeallocatesAGizmo(p);
    }
};

using GizmoUniquePtr = std::unique_ptr<Gizmo, GizmoDeleter>;

Using several deleters

可以根据情况自定义删除器,因此我们可以使用多个相同函数类型的删除函数,如void(*)(Instructions*)​:

using InstructionsUniquePtr = std::unique_ptr<Instructions,
void(*)(Instructions*)>;

删除函数有:

void deleteInstructions(Instructions* instructions){ delete
instructions;}
void doNotDeleteInstructions(Instructions* instructions){}

使用的时候:

if (method == BuildingMethod::fromSketch)
    return House(InstructionsUniquePtr(new Sketch,
deleteInstructions));
if (method == BuildingMethod::fromBlueprint)
    return House(InstructionsUniquePtr(new Blueprint,
deleteInstructions));

Blueprint blueprint;
House house(InstructionsUniquePtr(&blueprint,
doNotDeleteInstructions));

Safety belt

通常来讲,std::unique_ptr表示拥有OWN,意味着可以修改指向的对象,但是如果对象来自栈,那么std::unique_ptr就变成了指向外部拥有的对象。这种情况下,我们不希望修改这个对象,因为有可能产生副作用。

因此应该让指针指向一个const对象:

using InstructionsUniquePtr =
std::unique_ptr<const Instructions, void(*)(const
Instructions*)>;

删除函数变成:

void deleteInstructions(const Instructions* instructions){ delete
instructions;}
void doNotDeleteInstructions(const Instructions* instructions){}

A unique interface

给上面的情况创建一个统一的接口:

namespace util
{
    template<typename T>
    void doDelete(const T* p)
    {
        delete p;
    }

    template<typename T>
    void doNotDelete(const T* x)
    { }

    template<typename T>
    using CustomUniquePtr = std::unique_ptr<const T, void(*)(const T*)>;

    template<typename T>
    auto MakeConstUnique(T* pointer)
    {
        return CustomUniquePtr<T>(pointer, doDelete<T>);
    } 

    template<typename T>
    auto MakeConstUniqueNoDelete(T* pointer)
    {
        return CustomUniquePtr<T>(pointer, doNotDelete<T>);
    }
}

两种情况的实际使用为:

auto myComputer = util::MakeConstUnique(new
store::electronics::gaming::Computer);

auto myComputer = util::MakeConstUniqueNoDelete(new
store::electronics::gaming::Computer);

上面这个接口可以:

Whichever way, a resource must be disposed of

在释放资源的时候打印日志:

class Computer
{ 
public:
    explicit Computer(std::string&& id) : id_(std::move(id)){}
    ~Computer(){std::cout << id_ << " destroyed\n";}
private:
    std::string id_;
};

store::electronics::gaming::Computer c("stack-based computer");

auto myHeapBasedComputer = util::MakeConstUnique(new
store::electronics::gaming::Computer("heap-based computer"));

auto myStackBasedComputer = util::MakeConstUniqueNoDelete(&c);

Changes of deleter during the life of a unique_ptr

如果在unique_ptr拥有资源的声明周期中自定义的删除函数要变化怎么办?

using IntDeleter = void(*)(int*);
using IntUniquePtr = std::unique_ptr<int, IntDeleter>;

假设对于奇数和偶数的删除函数有去呗:

void deleteEvenNumber(int* pi)
{
    std::cout << "Delete even number " << *pi << '\n';
    delete pi;
} 

void deleteOddNumber(int* pi)
{
    std::cout << "Delete odd number " << *pi << '\n';
    delete pi;
}

Assigning from another std::unique_ptr

IntUniquePtr p1(new int(42), deleteEvenNumber);
IntUniquePtr p2(new int(43), deleteOddNumber);
p1 = move(p2);

那么问题来了,p1是用原来的删除函数呢还是move进来的删除函数呢?

Delete even number 42
Delete odd number 43

输出结果可以看到,移动会把正确的删除函数一起move过去。

Resetting the pointer

std::unique_ptr<int> p1(new int(42));
p1.reset(new int(43));

reset函数只接受一个参数,不能接受新的删除函数,因此无法指定新的删除函数:

Delete even number 42
Delete even number 43

但是可以再来一个函数指定:

p1.get_deleter() = deleteOddNumber;

如果想一步指定的话可以直接赋值:

IntUniquePtr p1(new int(42), deleteEvenNumber);
p1 = IntUniquePtr(new int(43), deleteOddNumber);

How to Return a Smart Pointer AND Use Covariance

The problem: Covariant return type vs. smart pointers

c++支持协变返回类型:

struct Base {};
struct Derived : Base {};

struct Parent
{
    virtual Base * foo();
};
struct Child : Parent
{
    virtual Derived * foo() override ;
};

这样可以得到多态下的函数返回子类型,对于指针和引用都ok,但是对于智能指针会出错:

#include <memory>
struct Base {};
struct Derived : Base {};
struct Parent
{
    virtual std::unique_ptr<Base> foo();
} ;
struct Child : Parent
{
    virtual std::unique_ptr<Derived> foo() override ;
} ;

如何处理请看英文文件见拷贝Cpp_smart_pointers_ebook,网站 fluentcpp.com