c++ 智能指针学习、使用

原文:https://www.fluentcpp.com/2018/12/25/free-ebook-smart-pointers/

有一件事会迅速使您的c++代码变得混乱,并阻碍其可读性,那就是内存管理。如果做得不好,这可能会把一个简单的逻辑变成毫无表现力的混乱管理,并使代码失去对内存安全性的控制。

确保所有对象的编程任务正确删除非常低的抽象层次而言,由于编写良好的代码基本上可以归结为尊重的抽象级别,你想让这些任务远离您的业务逻辑(或任何形式的逻辑)。

智能指针可以有效地处理这个问题,并将您的代码从繁琐的工作中解脱出来。
本系列文章将向您展示如何利用它们使您的代码更有表现力和更正确。

我们将深入探讨这个主题,因为我希望每个人都能跟上本系列的所有内容,所以没有任何先决条件,我们从智能指针的基础知识开始。

stack 和 heap

与许多其他语言一样,c++有几种类型的内存,它们对应于物理内存的不同部分。它们是:静态、堆栈和堆。静态是一个非常丰富的主题,值得为其争光,因此这里我们只关注堆栈和堆。

stack

int f(int a)
{
	if(a > 0)
		{
			std::string s = "a positive number";
			std::cout << s << "\n";
			}
		}
		return a;
	}

这里a和s存储在堆栈中。从技术上讲,这意味着a和s被并排存储在内存中,因为它们被推到由编译器维护的堆栈上。
然而,这些问题与日常工作并不是很相关。
关于堆栈,有一件重要的、关键的、甚至是基本的事情需要了解。它是本系列其余部分的基础。好消息是,这很简单:

在堆栈上分配的对象在超出作用域时将被自动销毁。

你可以把这篇文章再读几遍,如果需要的话,可以把它纹在前臂上,然后打印出来
用t恤给你的配偶朗读这句话,这样你就能经常想起它,

在c++中,作用域是由一对括号({和})定义的,除了用于初始化对象的括号:

std::vector<int> v = {1, 2, 3}; // this is not a scope
if (v.size() > 0)
{ // this is the beginning of a scope
...
} // this is the end of a scope

对象超出范围有三种方式:
●遇到下一个右括号(}),
●遇到一个返回语句,
●在当前范围内抛出未在当前范围内捕获的异常。

在第一个代码示例中,s在if语句的右括号处被销毁,a在函数的返回语句处被销毁。

heap

堆是动态分配的对象存储的地方,也就是说,对象分配了一个新调用,它返回一个指针:

int * pi = new int(42);

在上面的语句之后,pi指向一个在堆上分配的int对象。

严格来说,new分配的内存叫做空闲存储。堆是由malloc、calloc和realloc分配的内存,它们是C的遗留部分,通常在新代码中不再使用,我们将在本文中忽略它们(但我们将在本系列的后面讨论它们)。但术语“堆”在开发人员术语中是如此普遍,用于讨论我在这里使用的任何动态分配的内存。

无论如何,要销毁由new分配的对象,我们必须通过调用delete手动完成

delete pi;

与堆栈相反,在堆上分配的对象不会自动销毁。这样做的好处是,它们可以保持比作用域末尾更长的时间,并且不会产生任何拷贝,除了那些非常便宜的指针。此外,指针允许以多态方式操纵对象:一个指向基类的指针实际上可以指向任何派生类的对象。

但作为这种灵活性的代价,它让开发者负责删除它们。

删除堆上的对象不是简单的任务:必须调用delete一次,而且只能调用一次,才能释放基于堆的对象。如果没有调用该对象,该对象就不会被释放,并且其内存空间不可重用—这称为内存泄漏。但另一方面,对同一地址多次调用delete会导致未定义的行为。

这就是代码变得混乱和失去表达性(有时甚至是正确性)的地方。实际上,为了确保正确地销毁所有对象,簿记从简单的删除到出现早期返回时的复杂标志系统。

另外,一些接口在内存管理方面是不明确的。考虑下面的例子:

House* buildAHouse();

作为这个函数的调用者,我应该删除它返回的指针吗?如果我不这样做,而没有人这样做,那就是内存泄漏。但如果我做了,其他人做了,那么这就是未定义行为。在魔鬼和深蓝色的大海之间。

我认为所有这些导致了c++作为一种内存管理方面的复杂语言的坏名声。

但幸运的是,智能指针将为您处理所有这些。

RAII: the magic four letters

RAII是c++中一个非常惯用的概念,它利用堆栈的基本属性(请查看您的手臂或配偶的上半身)来简化堆上对象的内存管理。事实上,RAII甚至可以用来方便和安全地管理任何类型的资源,而不仅仅是内存。哦,我就不写这四个字母的意思了,因为在我看来,这并不重要,也很混乱。你可以把他们当作某人的名字,比如c++的超级英雄

RAII的原理很简单:将资源(例如指针)封装到一个对象中,然后在析构函数中释放该资源。这就是智能指针的作用:

template <typename T>
class SmartPointer
{
public:
explicit SmartPointer(T* p) : p_(p) {}
~SmartPointer() { delete p_; }
private:
T* p_;
};

关键是您可以将智能指针作为分配在堆栈上的对象来操作。编译器会自动调用智能指针的析构函数,因为…
在堆栈上分配的对象在超出作用域时将被自动销毁。这就会在包装好的指针上调用delete。只有一次。简单地说,智能指针的行为类似于指针,但是当它们被销毁时,它们会删除它们所指向的对象。

上面的代码示例只是为了理解RAII。但它绝不是一个完整的接口,一个现实的智能指针。

首先,智能指针在许多方面的行为与指针相似:它可以通过操作符或操作符->解除引用,也就是说,您可以调用它上的sp或sp->成员。它也可以转换为bool,所以它可以像指针一样在if语句中使用:

if(sp)
{
	...
}

它测试底层指针的零度。最后,底层指针本身可以用.get()方法访问。
第二,也许更重要的是,上面的接口缺少一个方面:它不处理复制!实际上,一个复制的SmartPointer也复制了底层指针,所以下面的代码有一个bug:

{
SmartPointer<int> sp1(new int(42));
SmartPointer<int> sp2 = sp1; // now both sp1 and sp2 point to
// the same object
} // sp1 and sp2 are both destroyed, the pointer is deleted twice!

实际上,它删除了基础对象两次,导致未定义的行为。
那么如何处理拷贝呢?这是不同类型的智能指针所不同的特性。
这让你可以非常精确地用代码表达你的意图。请继续关注,因为这是我们在本系列的下一部分中看到的内容。

unique_ptr , shared_ptr , weak_ptr ,scoped_ptr , raw pointers - Knowing your smart pointers

就像我们在讨论智能指针是什么时看到的那样,必须就如何复制智能指针采取一些积极的决策。否则,默认的复制构造函数可能会导致未定义的行为

事实证明,有几种有效的方法可以做到这一点,这就产生了各种智能指针。理解这些智能指针的作用是很重要的,因为它们是在代码中表达设计的方式,因此也可以通过阅读代码来理解设计。

我们在这里看到了各种类型的指针,存在于那里,大约按照有用性的递减顺序排序(根据我):

std::unique_ptr

在撰写本文时,这是默认使用的智能指针。它成为了c++ 11的标准。
unique_ptr的语义是它是内存资源的唯一所有者。一个std::unique_ptr将持有一个指针并在其析构函数中删除它(除非您自定义它,这是本系列另一部分的主题)。

这允许您在接口中表达您的意图。考虑以下函数:

std::unique_ptr<House> buildAHouse();

它告诉你它给你一个指向房子的指针,你是房子的主人。除了函数返回的unique_ptr之外,没有人会删除这个指针。既然您获得了所有权,这就给了您信心,您可以自由地修改指向对象的值。

注意std::unique_ptr是从工厂函数返回的首选指针。实际上,在处理内存的基础上,std::unique_ptr包装了一个普通指针,因此与多态性兼容。

但是这也是以另一种方式工作的,通过传递一个std::unique_ptr作为参数:

class House
{
public:
House(std::unique_ptr<PileOfWood> wood);
…

在这种情况下,房子获得了’PileOfWood’的所有权。

但是请注意,即使您接收到unique_ptr,也不能保证其他任何人都不能访问该指针。实际上,如果另一个上下文在unique_ptr中保留指针的副本,那么通过unique_ptr对象修改指向的对象当然会影响这个其他上下文。如果你不希望这种情况发生,表达它的方式是使用unique_ptr const:

std::unique_ptr<const House> buildAHouse();
// for some reason, I don't want you
// to modify the house you're being passed

为了确保只有一个unique_ptr拥有内存资源,不能复制std::unique_ptr。但是,所有权可以从一个unique_ptr转移到另一个
(您可以通过将unique_ptr移动到另一个unique_ptr中来传递或从函数中返回它们)。

移动可以通过函数值返回std::unique_ptr来实现,或者在代码中显式返回:

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

“什么?”,你可能会想。“我们在讨论智能指针,原始指针在这里做什么??”
嗯,即使原始指针不是智能指针,它们也不是“哑”指针。事实上,使用它们是有正当理由的,尽管这些理由并不经常发生。它们与参考文献有很多相同之处,但是后者应该是首选,除非在某些情况下(但这是另一篇文章的主题)。

现在我只想关注原始指针和引用在代码中表达了什么:原始指针和引用表示对对象的访问,但不是所有权。实际上,这是将对象传递给函数和方法的默认方式:

void renderHouse(House const& house);

当您持有一个带有unique_ptr的对象并希望将其传递给接口时,这一点特别需要注意。您不传递unique_ptr,也不传递对它的引用,而是对指向的对象的引用:

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

std::shared_ptr

shared_ptr进入了c++ 11的标准,但是在那之前就出现在了Boost中。
一个内存资源可以由几个std::shared_ptr同时持有。shared_ptr在内部维护一个计数,即它们中有多少拥有相同的资源,当最后一个被销毁时,它会删除内存资源。
因此,std::shared_ptr允许复制,但是使用引用计数机制来确保每个资源被删除一次且只删除一次。

乍一看,std::shared_ptr似乎是内存管理的灵丹妙药,因为它可以传递并且仍然保持内存安全性。
但是std::shared_ptr不应该默认使用,原因如下:

●拥有多个资源的同时占有者比只有一个唯一占有者(如std::unique_ptr)的系统更复杂。
●同时拥有多个资源持有者会使线程安全更加困难,
●当一个对象在域内没有共享,但由于技术原因在代码中仍以“共享”的形式出现时,它使代码反直观,
●由于与引用计数相关的簿记,这会导致在时间和内存方面的性能成本。

使用std::shared_ptr的一个好例子是在域中共享对象时。然后,使用共享指针以一种富有表现力的方式反映了它。通常,图中的节点可以很好地表示为共享指针,因为几个节点可以保存对另一个节点的引用。

在这里插入图片描述

std::weak_ptr

weak_ptr在c++ 11中输入了该语言,但在此之前就出现在了Boost中。
weak_ptr s可以与其他std::shared_ptr s一起保存对共享对象的引用,但它们不会增加引用计数。这意味着如果没有更多的std::shared_ptr持有一个对象,这个对象将被删除,即使一些弱指针仍然指向它。
因此,弱指针需要检查它指向的对象是否仍然是活的。为此,它必须复制到一个std::shared_ptr:

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
}
}

一个典型的用例是打破shared_ptr循环引用。考虑以下代码:

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;

weak_ptr可以用于维护缓存。数据可能已从缓存中清除,也可能尚未清除,weak_ptr将引用该数据。

boost::scoped_ptr

scoped_ptr存在于Boost中,但没有包含在标准中(至少在c++ 17中是这样)。
它只是简单地禁用了复制,甚至是move构造。因此,它是资源的唯一所有者,它的所有权不能被转移。因此,scoped_ptr只能存在于…一个范围。或作为对象的数据成员。当然,作为一个智能指针,它保留了在析构函数中删除底层指针的优势。

How to implement the pimpl idiom by using unique_ptr

pimpl的意思是“实现指针”,它是一种广泛使用的技术,用于减少编译依赖关系。我们将看到如何使用智能指针实现pimpl习语。
实际上,pimpl习语有一个负责管理内存资源的指针,因此使用智能指针听起来只有逻辑,例如std::unique_ptr

使用std::unique_ptr管理生命周期

今天,在c++中留下一个原始指针来管理自己的资源有点令人不安。一种自然的做法是用std::unique_ptr(或另一个智能指针)替换它。
这样,Fridge析构器就不再需要做任何事情,我们可以让编译器自动为我们生成它

Destructor visibility

在c++中有一条规则,删除一个指针会导致未定义的行为,如果:

#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) {}

在这里插入图片描述
在c++中有一条规则,删除一个指针会导致未定义的行为,如果:
●该指针的类型为void*或
●所指向的类型是不完整的,也就是说只forward声明,like
FridgeImpl在头文件中。
解决方法:
如果在调用delete之前类型的定义是可见的,那么unique_ptr会检入它的析构函数。因此,如果类型只是前向声明,则拒绝编译并调用delete。

事实上,std::unique_ptr并不是提供这种检查的唯一组件:Boost还建议checked_delete函数及其兄弟函数来确保delete调用是格式良好的。
因为我们在Fridge类中删除了析构函数的声明,所以编译器会接管并为我们定义它。但是编译器生成的方法是内联声明的,所以它们直接在头文件中实现。在那里,FridgeImpl的类型是不完整的。
因此,错误。
解决方法是声明析构函数,从而阻止编译器为我们做这些。所以改成如下形式:

#include <memory>
class Fridge
{
public:
Fridge();
~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) {}
Fridge::~Fridge() = default;

这个程序可以编译、运行和工作。
在c++中实现pimpl时,还需要考虑许多其他重要方面。为此,我建议您看一看Herb Sutter的异常c++中的专用部分

How to Transfer unique_ptr s From a Set to Another Set

传输一个std::unique_ptr到另一个std::unique_ptr是一件简单的事情:

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

The case: transfering sets of unique_ptrs

我们首先看看std::set of std::unique_ptr将表示什么,然后看看在尝试将一个集合的内容传输到另一个集合时发生了什么问题。

unique_ptrs集:惟一的和多态的

在这里插入图片描述
我们可以通过使用某种句柄(指针或引用)来多态地使用基类。为了封装内存管理,我们将使用std::unique_ptr。
现在,如果我们想要实现Base的几个对象的集合,但它可以是任何派生类的集合,我们可以使用unique_ptr s的集合。
最后,我们可能想要防止我们的集合有重复。这就是std::set所做的。
注意,要实现这个约束,std::set需要一种方法来比较它的对象。
的确,通过这样宣布一组:

std::set<std::unique_ptr<Base>>

集合元素之间的比较将调用std::unique_ptr的操作符<,它将比较其中指针的内存地址。
在大多数情况下,这不是您想要的。当我们想到“没有重复”时,它通常意味着“没有逻辑重复”,例如:没有两个元素具有相同的值。而不是“内存中没有两个元素位于同一个地址”。

为了不实现逻辑重复,我们需要调用操作符< on Base(如果它存在,可以使用Base实例提供的id)来比较元素并确定它们是否是重复的。为了让集合使用这个运算符,我们需要定制集合的比较器:

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;

为了有效地传输元素,我们使用insert方法:destination.insert(begin(source), end(source));
但是这会导致编译错误!
在这里插入图片描述

实际上,插入方法试图复制unique_ptr元素。
那怎么办呢?

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

c++中的集合和映射在内部实现为树。这让他们确保了他们接口的方法所保证的算法复杂性。在c++ 17之前,它不会显示在界面上。
c++ 17将合并方法添加到集合:

destination.merge(source);

这使得目的地接管源内部树的节点。这就像对列表进行拼接。因此,在执行这一行之后,destination拥有source拥有的元素,source为空。
由于只有节点被修改,而不是节点内部的内容,因此unique_ptr s感觉不到任何东西。他们甚至没有被移动。

目的地现在有unique_ptr s,故事结束。
如果你没有生产c++ 17,我写这几行的时候很多人都有这种情况,你能做什么?

We can’t move from a set

将元素从一个集合移动到另一个集合的标准算法是std::move。
以下是它如何与std::vector一起工作

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拥有的元素,并且source不是空的,而是空的unique_ptr s。

现在让我们对我们的集合做同样的事情:

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

我们得到相同的编译错误在开始时,一些unique_ptrs被复制:

在这里插入图片描述

这可能看起来令人吃惊。move算法的目的是避免在unique_ptr元素上复制,而是移动它们,那么为什么要复制它们呢?
答案在于集合如何提供对其元素的访问。当解除引用时,集合的迭代器不会返回unique_ptr&,而是返回const unique_ptr&。它是为了确保集合内的值不会在集合没有意识到的情况下被修改。实际上,它可以打破排序的不变量。

这就是发生的事情:
●std::move set上的迭代器的解引用,并获取const unique_ptr&,
●它调用std::移动该引用,从而获得const unique_ptr&&,
●它调用insert输出迭代器上的insert方法,并传递const unique_ptr&&,
●insert方法有两个重载:一个采用const unique_ptr&,另一个采用unique_ptr&&。一个是unique_ptr&&。由于我们传递的类型是const,所以编译器无法解析对第二个方法的调用,而是调用第一个方法。
然后插入输出迭代器调用集合上的插入重载,该重载采用const unique_ptr&,然后使用左值引用调用unique_ptr的复制构造函数,这将导致编译错误。

Making a sacrifice

所以在c++ 17之前,从集合中移动元素似乎是不可能的。必须放弃一些东西:要么是移动,要么是布景。这让我们有两个可能要放弃的方面。

Keeping the set but paying up for the copies

要放弃移动并接受将元素从一个集合复制到另一个集合,我们需要复制unique_ptr s所指向的内容。
为此,让我们假设Base has是一个在派生中被覆盖的多态克隆方法:

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...
};

在call site,我们可以将unique_ptrs从一个集合复制到另一个集合,例如:

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

或者通过loop 的方法:

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

Keeping the move and throwing away the set

不允许移动发生的集合是源集合。如果你只需要目标有唯一的元素,你可以用std::向量替换源集合。
实际上,std::vector并不向其迭代器返回的值添加常量。因此,我们可以通过std::move算法将其元素从其中移动:

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)));

然后,目标集包含一个unique_ptr,其中包含以前位于源中的内容,而源向量现在包含一个空的unique_ptr。

Live at head

您可以看到,有一些方法可以解决将unique_ptr从一个集合传输到另一个集合的问题。但真正的解决方案是c++ 17中的std::set的归并方法。
随着语言的发展,标准库变得越来越好。让我们尽可能地移动到(哈哈)最新版本的c++,永远不要回头看。

Custom deleters

让我们以一个House类为例,它携带它的构建指令,它是多态的,可以是一个草图,也可以是一个成熟的蓝图:

在这里插入图片描述

处理指令生命周期的一种方法是将它们作为unique_ptr存储在内部。然后说,房子的副本可以复制说明:

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_;
};

实际上,指令有一个多态克隆,它是由派生类实现的:

{
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));
}
};

这里有一种建造房子的方法:

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将导致未定义的行为。
一种解决方案是复制蓝图并在堆上分配它。这可能是
好吧,或者它可能代价高昂(我曾经遇到过类似的情况,它曾经是这个项目的瓶颈)。
但是传递在堆栈上分配的对象是合理的需要。然后,当对象来自堆栈时,我们不希望房子破坏其析构器中的指令。

std::unique_ptr在这里有什么帮助?

Seeing the real face of std::unique_ptr

大多数时候,c++唯一指针被用作std::unique_ptr。但是它的完整类型有第二个模板参数,它的deleter:

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

std::default_delete是调用时调用delete的函数对象。但它只是删除程序的默认类型,可以为自定义删除程序更改它。
这样就可以为具有用于处理其资源的特定代码的类型使用唯一指针。这发生在来自C的遗留代码中,一个函数通常负责释放一个对象及其内容:

struct GizmoDeleter
{
void operator()(Gizmo* p)
{
oldFunctionThatDeallocatesAGizmo(p);
}
};
using GizmoUniquePtr = std::unique_ptr<Gizmo, GizmoDeleter>;

(顺便说一下,作为简化遗留代码的一个步骤,为了使其与std::unique_ptr兼容,此技术非常有用。)
现在有了这个特性,让我们回到激励场景。

Using several deleters

我们最初的问题是,我们希望unique_ptr删除指令,除非它们来自堆栈,在这种情况下,我们希望它不去管它们。
在给定的情况下,可以自定义删除器以删除或不删除。为此,我们可以使用几个删除函数,都是相同的函数类型(void()(Instructions):

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

删除函数为:

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

一个删除对象,另一个不做任何事情。
使用std::unique_ptr的出现需要替换为InstructionUniquePtr,唯一的指针可以这样构造

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

除非参数来自堆栈,这种情况下可以使用no-op删除器:

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

正如iaanus在Reddit上指出的,我们应该注意,如果unique_ptr移出堆栈对象的作用域,它将指向一个不再存在的资源。在此之后使用unique_ptr会导致内存损坏。
而且,就像Bart在评论中提到的,我们应该注意到,如果House的构造函数有多个参数,那么我们应该在单独的语句中声明unique_ptr的构造,像这样:

InstructionsUniquePtr instructions(new Sketch, deleteInstructions);
return House(move(instructions), getHouseNumber());

实际上,如果抛出异常,可能会出现内存泄漏(有效c++的cf Item 17)。
另外,当我们不使用自定义删除器时,我们不应该直接使用new,而应该使用std::make_unique,它允许您传递用于构造指向对象的参数

Safety belt

如果您小心地避免了内存损坏,那么使用自定义删除程序可以解决最初的问题,但是它会导致传递参数的语义发生一点变化,这可能是许多错误的根源。
通常,持有一个std::unique_ptr意味着成为它的所有者。这意味着可以修改指向对象。但是,如果对象来自堆栈(或者当它通过no-op deleter传递时来自其他地方),则唯一指针仅持有对外部拥有的对象的引用。在这种情况下,您不希望惟一的指针修改对象,因为它会对调用者产生副作用,这将使代码更难推理。
因此,当使用这种技术时,确保工作指针const对象:

using InstructionsUniquePtr =
std::unique_ptr< const Instructions, void(*)( const
Instructions*)>;
and the deleters become:
void deleteInstructions( const Instructions* instructions){ delete
instructions;}
void doNotDeleteInstructions( const Instructions* instructions){}

这样,唯一指针就不会在类之外造成麻烦。这将为您节省大量的调试工作。

Custom deleters are ugly

当你看表达式定义一个unique_ptr自定义删除:

std::unique_ptr<const Computer, void(*)(const Computer*)>;

它的密度足够大,看太久对你的眼睛是危险的。我们不应该在产品代码中到处散布这样的表达。所以最自然的方法就是写一个别名

using ComputerConstPtr = std::unique_ptr<const Computer,
void(*)(const Computer*)>;

哪个在界面中表现更好:

void plugIn(ComputerConstPtr computer);

但是,当我们创建unique_ptr的新实例时,丑陋的地方仍然存在,因为我们每次都必须传递一个删除器:

ComputerConstPtr myComputer(new Computer, deleteComputer);

这里我们定义了删除:

void deleteComputer(const Computer* computer){ delete computer;}
void doNotDeleteComputer(const Computer* computer){}

这带来了三个问题。第一个是,我们不应该在我们想要智能指针删除它的资源的情况下指定任何东西。这就是智能指针最初的用途。
当然,这是特别的,因为它可能不得不不删除它的资源,因为某些情况下。但为什么删除它的名义情况会因为特殊情况而负担?
名称空间的第二个问题是冗长。想象一下,我们
Computer type在一个嵌套的名称空间内,就像在生产代码中经常出现的那样:

namespace store
{
namespace electronics
{
namespace gaming
{
class Computer
{
// ...
};
using ComputerConstPtr = std::unique_ptr<const Computer,
void(*)(const Computer*)>;
void deleteComputer(const Computer* computer);
void doNotDeleteComputer(const Computer* computer);
}
}
}


store::electronics::gaming::ComputerConstPtr myComputer(new
store::electronics::gaming::Computer,
store::electronics::gaming::deleteComputer);

这是一行很难的代码。而且说得很少。
最后一个问题是,我们为要定制删除的每种类型定义了一个delete和一个doNotDelete函数。即使它们的实现没有特定于计算机类型或任何其他类型。但是,请注意,即使是这样模板删除:

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

…不会让代码变得更简单。实际上,在实例化指针时,我们仍然需要指定模板类型:

store::electronics::gaming::ComputerConstPtr myComputer(new
store::electronics::gaming::Computer,
doDelete <store::electronics::gaming::Computer> );
A unique interface

下面是流利的c++读者Sergio Adan提出的建议,它可以解决上述两个问题:对所有类型上的所有自定义删除器使用相同的接口。
这可以在另一个名称空间(技术名称空间)中定义。在这个示例中,我们将这个名称空间称为util。
然后在这个名称空间中编写创建定制unique_ptr的所有公共代码。
让我们调用这个助手MakeConstUnique作为实例。下面是它的代码:

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>);
}
}

使用此代码,在使用自定义删除器对特定类型使用unique_ptr时,不需要定义任何其他内容。例如,要创建一个unique_ptr的实例,当它超出作用域时删除它的资源,我们写:

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

并创建一个不删除其资源:

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

这个界面有趣的地方在于:
●通常情况下不再提到删除,
●由于返回类型MakeConstUnique,我们现在可以使用auto。
请注意,所有这些都使我们减少到Computer名称空间的一次出现,而不是三次。

Specific deleters

现在,如果由于某些原因,我们不想在类计算机上调用delete,而是调用一个特定的专用函数呢?这可能发生在来自C的类型,例如:

void deleteComputer(const Computer* computer)
{
specificFunctionThatFreesAComputer(computer);
}

为了继续对该类型使用MakeConstUnique,我们可以对该类型计算机进行该模板函数的完全专门化。通过重新打开util命名空间,我们可以在模块定义计算机中做到这一点:

namespace util
{
template<>
auto MakeConstUnique(store::electronics::gaming::Computer*
pointer)
{
return
CustomUniquePtr<store::electronics::gaming::Computer>(pointer,
specificFunctionThatFreesAComputer);
}
}

在这种情况下,客户端代码可能也不会用new分配它的指针。

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,看看在unique_ptr的生命周期中,该删除器什么时候可以更改。
下面是一个玩具样例,我们在int上使用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包含一个具有适当删除器的偶数,它正在接管p2中资源的所有权。问题是:它将如何破坏这种资源?它会使用它构建时使用的删除器,还是会把p2的删除器连同它的资源所有权一起带来?

下面是这个程序输出的内容(删除程序打印出了信息——请看文章顶部的代码):

在这里插入图片描述
使用正确的删除器删除每个资源,这意味着赋值确实带来了删除器。这样做是有意义的,因为否则资源将不会使用正确的删除器被释放。

Resetting the pointer

改变std::unique_ptr中包含的资源的另一种方法是调用它的reset方法,如下面的简单示例所示:

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

reset方法对当前资源(42)调用删除程序,然后处理新的资源
(43)。
但是reset方法只接受一个参数,即新资源。不能将它与这个新资源一起传递一个删除程序。因此,在我们的例子中,它不能再直接用于偶数和奇数。确实,以下守则:

IntUniquePtr p1(new int(42), deleteEvenNumber);
p1.reset(new int(43)); // can't pass deleteOddNumber

在这里插入图片描述
这在我们这里是不正确的。
事实上,通过利用unique_ptr的get_deleter方法通过非const引用返回deleter,我们可以在单独的语句中手动更改这个删除器(感谢Marco Arena指出了这一点):

p1.get_deleter() = deleteOddNumber;

但是为什么重置没有一个删除参数呢?以及如何在一条语句中将新资源移交给std::unique_ptr及其适当的删除器?

Howard Hinnant,他是std::unique_ptr组件的主要设计者和作者之一,回答了这个关于堆栈溢出的问题:
下面是在我们最初的例子中如何使用他的答案:

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 ;
} ;

这里,我们希望来自Child的foo方法返回Base *,以便成功重写(和编译!)使用协变返回类型,我们实际上可以用它的任何派生类型替换Base 。例如,派生的
这适用于指针,和引用…但是当你尝试使用智能指针的时候:

#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 ;
} ;

编译器报错

use cases

由于这个问题是通用的,让我们用一个越来越复杂的用例的广泛面板:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
通过以自然的方式处理所有这些情况,解决方案应该可以用于大多数生产问题。

Preamble: Separation of concerns + private virtual function

我们将把它分割成两个成员函数,而不是用一个克隆成员函数来处理所有事情。在以下代码中:

class some_class
{
public:
std::unique_ptr<some_class> clone() const
{
return std::unique_ptr<some_class>(this->clone_impl());
}
private:
virtual some_class * clone_impl() const
{
return new some_class(*this) ;
}
};

第一个函数clone_impl()使用复制构造函数执行克隆的实际工作。
它提供了一个强大的保证(只要复制构造函数提供它),并将指针的所有权转移到新创建的对象。虽然这通常是不安全的,但是我们假设在这种情况下,除了clone()函数之外,没有人可以调用这个函数,这是通过clone_impl()的私有访问来强制执行的。

第二个函数clone()检索指针,并将其所有权给unique_ptr。
这个函数本身不会失败,因此它提供了与clone_impl()相同的强大保证。

Simple Hierarchy: Covariance + Name hiding

使用上面的技术,我们现在可以产生一个简单的OO层次结构:

在这里插入图片描述

class cloneable
{
public:
virtual ~cloneable() {}
std::unique_ptr<cloneable> clone() const
{
return std::unique_ptr<cloneable>(this->clone_impl());
}
private:
virtual cloneable * clone_impl() const = 0;
};
/
class concrete : public cloneable
{
public:
std::unique_ptr<concrete> clone() const
{
return std::unique_ptr<concrete>(this->clone_impl());
}
private:
virtual concrete * clone_impl() const override
{
return new concrete(*this);
}
};
int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = c->clone();
cloneable * p = c.get();
std::unique_ptr<clonable> pp = p->clone();
}

看到我们做了什么了吗?
通过分离关注点,我们能够在层次结构的每个级别使用协方差来产生clone_impl成员函数,返回我们想要的指针类型。
在c++中使用一个烦人的特性,名字隐藏(例如,在派生类中声明一个名字时,这个名字隐藏基类中所有同名的符号),我们隐藏
成员函数clone()返回我们想要的精确类型的智能指针。

当从一个concrete克隆时,我们获得一个unique_ptr,当从一个cloneable克隆时,我们获得一个unique_ptr。
对于使用不安全的raii所有权转移的clone_impl成员函数,您可能会感到不安,但是由于成员函数是私有的,并且只能通过克隆调用,所以问题得到了缓解。这限制了风险,因为类的用户不能错误调用它。
这解决了问题,但是增加了一些样板代码。

Simple Hierarchy, v2: Enter the CRTP

CRTP是一种c++风格,它支持将派生类名注入到模板基中。您可以在关于Fluent c++上的CRTP的系列中了解它。
我们将使用它在CRTP基类中用正确的派生原型声明方法,然后通过继承将方法注入到派生类本身:

template <typename Derived, typename Base>
class clone_inherit<Derived, Base> : public Base
{
public:
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(*this);
}
};

clone_inherit是一个CRTP,它知道它的派生类,也知道它的所有直接基类。它像往常一样实现协变clone_impl()和隐藏clone()成员函数,但是它们使用强制类型转换来遍历类型层次结构。
这使得我们可以将上面定义的具体类更改为:

class concrete
: public clone_inherit<concrete, cloneable>
{
};
int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = b->clone();
cloneable * p = c.get();
std::unique_ptr<clonable> pp = p->clone();
}

如您所见,具体类现在不再混乱。
这有效地将多态协变克隆()添加到类的层次结构中。
这个CRTP是我们通用解决方案的基础:下一步都将在它的基础上进行构建。

Multiple Inheritance: Variadic templates to the rescue

OO层次结构的一个复杂之处是多重继承。

在这里插入图片描述

在我们的例子中,我们如何扩展我们的解决方案,以支持具体类继承自两个提供相同克隆特性的基类的情况?
解决方案首先需要两个基类foo和bar来提供clone / clone_impl成员函数:

class foo
{
public:
virtual ~foo() = default;
std::unique_ptr<foo> clone() const
{
return std::unique_ptr<foo>(this->clone_impl());
}
private:
virtual foo * clone_impl() const = 0;
};
/
class bar
{
public:
virtual ~bar() = default;
std::unique_ptr<bar> clone() const
{
return std::unique_ptr<bar>(this->clone_impl());
}
private:
virtual bar * clone_impl() const = 0;
};

这里有一些样板文件,但是我们将稍后处理它。现在,我们必须解决继承问题,c++ 11为我们提供了一个简单的解决方案:可变参数模板。
我们只需要修改clone_inherit CRTP来支持它:

template <typename Derived, typename ... Bases>
class clone_inherit : public Bases...
{
public:
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(static_cast<const Derived & >(*this));
}
};

现在我们可以使用它来编写具体的类:

class concrete
: public clone_inherit<concrete, foo, bar>
{
};

最后,但并非最不重要的是,我们可以使用协方差和智能指针的类:

int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = c->clone();
foo * f = c.get();
std::unique_ptr<foo> ff = f->clone();
bar * b = c.get();
std::unique_ptr<bar> bb = b->clone();
}
Multiple Inheritance v2: Specialization to the rescue

现在,让我们解决这个问题:foo和bar都提供相同的“可克隆”特性。在我们的例子中,两者都应该是可摧毁的。
解决方案是专门化clone_inherit来处理不需要基类的情况,提供虚拟析构函数,并从它继承foo和bar:

template <typename Derived, typename ... Bases>
class clone_inherit : public Bases...
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(static_cast<const Derived & >(*this));
}
};
/
template <typename Derived>
class clone_inherit<Derived>
{
public:
virtual ~clone_inherit() = default;
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const = 0;
};
This way, we can now write:
class foo
: public clone_inherit<foo>
{
};
/
class bar
: public clone_inherit<bar>
{
};
/
class concrete
: public clone_inherit<concrete, foo, bar>
{
};

最后,但并非最不重要的是,我们可以使用协方差和智能指针的类:

int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = c->clone();
foo * f = c.get();
std::unique_ptr<foo> ff = f->clone();
bar * b = c.get();
std::unique_ptr<bar> bb = b->clone();
}

Deep Hierarchy: Abstracting

OO层次结构的另一个复杂之处在于,它们可以深入两个层次:

在这里插入图片描述
问题是,正如Scott Meyers建议我们的那样,非叶子类不应该自己实例化(更有效的c++,第33项)。
在我们的例子中,非叶子类中的clone_impl方法必须是纯虚的。
因此,我们的解决方案必须支持声明clone_impl纯虚拟或实现的选择。
首先,我们添加一个专用类型,用于“标记”一个类型:

template <typename T>
class abstract_method
{
};

然后,我们再次部分专门化clone_inherit类以使用该类型,这意味着
(因为之前的专门化),4种不同的clone_inherit实现:

// general: inheritance + clone_impl implemented
template <typename Derived, typename ... Bases>
class clone_inherit : public Bases...
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(static_cast<const Derived & >(*this));
}
};
/
// specialization: inheritance + clone_impl NOT implemented
template <typename Derived, typename ... Bases>
class clone_inherit<abstract_method<Derived>, Bases...> : public
Bases...
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const = 0;
};
/
// specialization: NO inheritance + clone_impl implemented
template <typename Derived>
class clone_inherit<Derived>
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(static_cast<const Derived & >(*this));
}
};
/
// specialization: NO inheritance + clone_impl NOT implemented
template <typename Derived>
class clone_inherit<abstract_method<Derived>>
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const = 0;
};
class cloneable
: public clone_inherit<abstract_method<cloneable>>
{
};
/
class abstracted
: public clone_inherit<abstract_method<abstracted>, cloneable>
{
};
/
class concrete
: public clone_inherit<concrete, abstracted>
{
};
int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = c->clone();
abstracted * a = c.get();
std::unique_ptr<abstracted> aa = a->clone();
cloneable * p = c.get();
std::unique_ptr<clonable> pp = p->clone();
}

Diamond Inheritance: Virtual-ing

OO层次结构的另一个复杂之处是我们可以有一个菱形继承:

在这里插入图片描述

在c++中,这意味着我们可以选择:基类是虚拟继承的,还是不是?

因此,这一选择必须由clone_inherit提供。问题是,声明虚拟继承要复杂得多,因为模板参数包…或者是吗?
让我们写一个类来做间接:

template <typename T>
class virtual_inherit_from : virtual public T
{
using T::T;
};

这个类实际上将虚拟继承应用到它的基类T上,这正是我们想要的。现在,我们所需要的是使用这个类来显式我们的虚拟继承需要:

class foo
: public clone_inherit<abstract_method<foo>,
virtual_inherit_from<cloneable>>
{
};
class bar
: public clone_inherit<abstract_method<bar>,
virtual_inherit_from<cloneable>>
{
};
/
class concrete
: public clone_inherit<concrete, foo, bar>
{
};
int main()
{
std::unique_ptr<concrete> c = std::make_unique<concrete>();
std::unique_ptr<concrete> cc = c->clone();
foo * f = c.get();
std::unique_ptr<foo> ff = c->clone();
bar * b = c.get();
std::unique_ptr<bar> bb = c->clone();
cloneable * p = c.get();
std::unique_ptr<cloneable> pp = p->clone();
}

The whole package

The whole clone-ing code is:
/
template <typename T>
class abstract_method
{
};
/
template <typename T>
class virtual_inherit_from : virtual public T
{
using T::T;
};
/
template <typename Derived, typename ... Bases>
class clone_inherit : public Bases...
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
54
protected:
// desirable, but impossible in C++17
// see: http://cplusplus.github.io/EWG/ewg-active.html#102
// using typename... Bases::Bases;
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(static_cast<const Derived & >(*this));
}
};
/
template <typename Derived, typename ... Bases>
class clone_inherit<abstract_method<Derived>, Bases...> : public
Bases...
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return std::unique_ptr<Derived>(static_cast<Derived
*>(this->clone_impl()));
}
protected:
// desirable, but impossible in C++17
// see: http://cplusplus.github.io/EWG/ewg-active.html#102
// using typename... Bases::Bases;
private:
virtual clone_inherit * clone_impl() const = 0;
};
/
template <typename Derived>
class clone_inherit<Derived>
{
public:
virtual ~clone_inherit() = default;
55
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const override
{
return new Derived(static_cast<const Derived & >(*this));
}
};
/
template <typename Derived>
class clone_inherit<abstract_method<Derived>>
{
public:
virtual ~clone_inherit() = default;
std::unique_ptr<Derived> clone() const
{
return
std::unique_ptr<Derived>(static_cast<Derived*>(this->clone_impl()));
}
private:
virtual clone_inherit * clone_impl() const = 0;
};
/
... and the user code is:
/
class cloneable
: public clone_inherit<abstract_method<cloneable>>
{
};
56
/
class foo
: public clone_inherit<abstract_method<foo>,
virtual_inherit_from<cloneable>>
{
};
/
class bar
: public clone_inherit<abstract_method<bar>,
virtual_inherit_from<cloneable>>
{
};
/
class concrete
: public clone_inherit<concrete, foo, bar>
{
};

总的来说,这并不坏。
我们会在生产代码中使用它吗?虽然这组技术很有趣,但它不能在Visual Studio 2017上编译(虚拟继承、diamond和协方差不能很好地混合在一起)
Visual Studio),在我们的例子中,它是一个showstopper。但是它至少可以用GCC 5.4.0 +和Clang 3.8.0 +进行编译。
这组技术展示了如何通过使用两种正交的c++范例(面向对象和通用(模板))巧妙而又简单的组合,我们可以分解出代码,以产生在其他方法中难以或不可能得到的简洁结果
c语言。
它还展示了一系列可以应用于其他地方的技术(模拟协方差、提供特性的继承间接),每一个都依赖于像lego片段一样组装的c++特性来产生所需的结果。
我觉得这很酷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值