在上一篇文章中,虽然我们实现了一部分的类型转换支持,但是,我们并不能满足于此。既然我们的类叫做“SharedPtr”,那么,用户就有理由期望他的某些行为,能够和真正的pointer一致:
// Raw pointer
Derived* ptr;
Base* ptr2 = ptr;
// Smart pointer
SharedPtr<Derived *> sp;
SharedPtr<Base *> sp2 = sp;
首先,我们先来理一理我们所需要的工具:
Deleter
的类型),我们需要利用方法调用时的类型推断,因此,我们的构造函数是一个函数模板deleter
后,我们需要某种方法把该对象存储起来。由于我们的SharedPtr
只有一个模板参数,我们并没有办法直接在类中存放deleter
(毕竟,我们根本不知道他的类型究竟是什么)。于是,利用多态,我们将其放至另一个对象中。int* mRefCount
。SharedPtr<T>
之间的转型,我们也希望能够赋予用户更改其deleter
的能力,即deleter
和resource
之间的绑定不是永久的。为了支持SharedPtr<T>
之间的转型,我们需要某种方法检测其合法性。这里,我们可以利用编译器帮我们检查。但是,为了执行该检查,资源句柄必须有显式的赋值(或 copy construct)。在这一点上,我们有两个选择:
1) 和之前的实现一样,在内部的SharedData
类中进行赋值,此时依赖编译器对其进行检查。
2) 直接在SharedPtr<Resource>
中存储Resource
对象。
综合上面的4、5、6点,我们应该可以判断,mRefCout, mResource, mDeleter
三者应该作为三个单独的成员存在,而不应当像之前的实现,将它们绑定在一个内部对象中。虽然上一种方法在多个共享对象同时存在时,可以节省不少资源(内存),但是,却失去了很多的灵活性。同时,第6点的第2个选择,实现起来也会更加的简单。
虽然我们这里说要将三者用三个域(field)分开存储,但上一篇文章对deleter
的实现的讨论依然是有效的,在这里,我们依然要使用一个类将实际的deleter
包裹起来,然后利用虚函数实现运行时的多态。
这里,我们将这个包裹实际deleter
的类称作ResourceDeleter
:
class ResourceDeleter {
};
可是,现在实际的资源被我们直接存放在了SharedPtr
里,我们需要通过某种方式,让ResourceDeleter
的某个子类能够拿到该资源,从而删除之。现在,则需要通过参数,将资源传递进去。
class ResourceDeleter {
public:
virtual void Destroy(Resource resource) noexcept = 0;
virtual ~ResourceDeleter() noexcept {}
};
由于我们一般可以期望析构函数不会抛出异常,所以,这里我们也同样期望,用户所定义的deleter
不抛出异常。于是,我们声明 Destroy()
为 noexcept
。
至于实现,其实也很简单:
template<typename Deleter>
class DeleterImpl : public ResourceDeleter {
public:
DeleterImpl(Deleter deleter) : mDeleter{deleter} {
}
void Destroy(Resource resource) noexcept override {
mDeleter(resource);
}
private:
Deleter mDeleter;
};
而对于SharedPtr
,根据上面的讨论,我们首先可以确定他的几个成员变量:
template<typename Resource>
class SharedPtr {
private:
int* mRefCount;
Resource mResource;
ResourceDeleter* mDeleter;
}
接下来,我们先来实现 copy ctor:
template<typename T>
SharedPtr(const SharedPtr<T>& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter} {
++*mRefCount;
}
template<typename T, typename Deleter>
SharedPtr(const SharedPtr<T>& sp, Deleter deleter)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{new DeleterImpl<Deleter>{deleter}} {
// No need to catch exception while alloc memory for mDeleter
++*mRefCount;
}
实现的原理其实很简单,只是简单地复制数据。如果用户传递了额外的deleter
,我们则生成一个新的DeleterImpl
。
由于这两个函数长得实在太像了,以至于让人有一股将它们合并的冲动。遗憾的是,我们不能。因为在第二个函数中,只有在函数调用产生后,我们才能够拿到sp.mDeleter
,也就是说,我们没有办法用它来当第二个参数的默认实参。
更让人遗憾的是,如果仅仅声明上面的函数模板,编译器还会帮我们生成一个默认的复制构造函数!或者,如果我们定义了move ctor,他会被声明为 delete
,这同样不是我们所希望看到的。所以,重复还得继续。(由于我也还是在学习C++中,目前还不知道是否有更好的方案,如果有,以后会继续更新)。
SharedPtr(const SharedPtr& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter} { ++*mRefCount; }
对应的,析构函数我们可以这样实现:
~SharedPtr() {
if (!mRefCount) return;
if (--*mRefCount == 0) {
mDeleter->Destroy(mResource);
}
}
我们只需要对mRefCout
计数器减1,然后测试其是否等于0。是的话,则表示我们是最后一个引用该资源的对象。于是,删除之。
现在,也许你已经迫不及待要来测试一下这个实现了。我也可以负责任地告诉你,如果只是执行一些普通的测试,你会发现,他运行良好。但是,我们都太乐观了。
在上面的析构函数里,我们destroy
掉资源后,却没有delete mRefCount
,作为一个资源管理工具,自己却在泄露资源(此处是内存)。
嗯……等等,好像还有更糟糕的,我们连mDeleter
也给漏掉了!
对于mRefCount
来说,他的生命周期和我们所保护的资源是相同的,于是,在*mRefCount == 0
时,delete
掉他就可以了。而上面实现的mDeleter
,他的生命周期却不是那么的一致——他可能会在多个对象之间共享。这里说“可能”,是因为,一个资源,按照我们的实现,可以有多个deleter
。至于解决办法,其实也无外乎两种——“独占使用”和“计数器”。由于deleter
一般不会是什么大对象,这里,我们就选取了第一种。
即便选择的是第一种,这里也有两种方法可以实现。第一种是像上面那样,使用一个构造函数模板。而第二种就是所谓的“虚构造函数”(所谓的 虚构造函数,其实并不是构造函数,而是一个普通的虚函数,而他可以用来构造对象)。由于我们最后期望的,仅仅是一个享有所有权的ResourceDeleter
,这里我们选用第二种方法是一个更好的选择,因为第一种会生成多个构造函数,从而增大程序的体积。
class ResourceDeleter {
public:
// ...
virtual ResourceDeleter* Clone() noexcept = 0;
};
template<typename Deleter>
class DeleterImpl : public ResourceDeleter {
public:
ResourceDeleter*
Clone() {
return new DeleterImpl<Deleter>{mDeleter};
}
};
对应的,不接受deleter
的两个复制构造函数应修改为:
SharedPtr(const SharedPtr& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter->Clone()} { ++*mRefCount; }
template<typename T>
SharedPtr(const SharedPtr<T>& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter->Clone()} { ++*mRefCount; }
然后是析构函数:
~SharedPtr() {
if (!mRefCount) return;
if (--*mRefCount == 0) {
mDeleter->Destroy(mResource);
delete mRefCount;
}
delete mDeleter;
}
注意,这里我们总是需要释放 mDeleter
。
move constructor 的实现原理与之前类似,只是加上了上面实现 copy constructor 时所使用的技术(operator=()
也是一样):
template<typename T>
void swap(SharedPtr<T>& other) {
std::swap(mRefCount, other.mRefCount);
std::swap(mResource, other.mResource);
std::swap(mDeleter, other.mDeleter);
}
SharedPtr(SharedPtr&& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter} {
sp.mRefCount = nullptr;
}
template<typename T>
SharedPtr(SharedPtr<T>&& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter} {
sp.mRefCount = nullptr;
}
template<typename T, typename Deleter>
SharedPtr(SharedPtr<T>&& sp, Deleter deleter)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{new DeleterImpl<Deleter>{deleter}} {
sp.mRefCount = nullptr;
}
SharedPtr& operator=(SharedPtr sp) {
swap(sp);
return *this;
}
template<typename T>
SharedPtr& operator=(SharedPtr<T> sp) {
swap(sp);
return *this;
}
如果读者觉得关于move ctor
和 operator=()
的代码不能够理解,请确保你已经理解了上面关于 copy ctor
的描述和上一篇文章。如果还是不能理解,欢迎给文章留言,我会尽力解答。
在最后,我们再来实现一个跟上面的讨论不太相关的功能——默认构造函数。默认构造函数的一个最大的用途就是,有了他,用户可以定义数组和集合的默认元素:
SharedPtr<int *> ptrs[42];
std::vector<SharedPtr<int *>> vec(42);
下面先给出我的实现:
template<typename T = Resource,
typename Deleter = PointerDeleter<Resource>>
SharedPtr(T t = nullptr, Deleter d = PointerDeleter<Resource>{})
: mRefCount{nullptr}, mResource{t} {
// Since we construct the mDeleter after mRefCount,
// there is no need to initialize it.
try {
mRefCount = new int{1};
mDeleter = new DeleterImpl<Deleter>{d};
} catch (std::bad_alloc) {
if (mRefCount) delete mRefCount;
d(t);
throw;
}
}
这里的一个关键点是有两次内存分配。并且,两次分配都有可能失败。于是,我们需要在分配失败的时候捕获对应的异常,然后释放已分配的资源。对于内建(built-in)数据类型(这里指指针),在未初始化的情况下,其值是没有定义的。也即是说,典型情况下,如果有异常抛出,我们没有办法确定mRefCount
是否已经分配。于是,我们必须利用 member initialzer list 先将 mRefCount
初始化为 nullptr
。
另外一种可选的做法是,先用一个try
块包含整个函数体,然后在 member initialzer list 里直接初始化 mRefCount
,而后再在函数体中用另一个try
块来创建 mDeleter
。
由于文章有点长,下面照例继续给出SharedPtr
到目前为止的全部实现:
#include <new>
#ifndef LIBS_UTIL_SHARED_PTR_H_
#define LIBS_UTIL_SHARED_PTR_H_
#define DEBUG_SHARED_PTR
#if defined(DEBUG_SHARED_PTR)
#include <iostream>
#endif
namespace {
template<typename Pointer>
class PointerDeleter {
public:
void operator()(Pointer ptr) {
delete ptr;
}
};
}
namespace jl_util {
template<typename Resource>
class SharedPtr {
public:
template<typename T = Resource,
typename Deleter = PointerDeleter<Resource>>
SharedPtr(T t = nullptr, Deleter d = PointerDeleter<Resource>{})
: mRefCount{nullptr}, mResource{t} {
// Since we construct the mDeleter after mRefCount,
// there is no need to initialized it
try {
mRefCount = new int{1};
mDeleter = new DeleterImpl<Deleter>{d};
} catch (std::bad_alloc) {
if (mRefCount) delete mRefCount;
d(t);
throw;
}
}
// If we just declare the templatized copy ctor, the compiler
// will generate a default ctor for us.
SharedPtr(const SharedPtr& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter->Clone()} {
++*mRefCount;
}
template<typename T>
SharedPtr(const SharedPtr<T>& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter->Clone()} {
++*mRefCount;
}
template<typename T, typename Deleter>
SharedPtr(const SharedPtr<T>& sp, Deleter deleter)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{new DeleterImpl<Deleter>{deleter}} {
// No need to catch exception while alloc memory for mDeleter
++*mRefCount;
}
SharedPtr(SharedPtr&& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter} {
sp.mRefCount = nullptr;
}
template<typename T>
SharedPtr(SharedPtr<T>&& sp)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{sp.mDeleter} {
sp.mRefCount = nullptr;
}
template<typename T, typename Deleter>
SharedPtr(SharedPtr<T>&& sp, Deleter deleter)
: mRefCount{sp.mRefCount}, mResource{sp.mResource},
mDeleter{new DeleterImpl<Deleter>{deleter}} {
sp.mRefCount = nullptr;
}
SharedPtr& operator=(SharedPtr sp) {
swap(sp);
return *this;
}
template<typename T>
SharedPtr& operator=(SharedPtr<T> sp) {
swap(sp);
return *this;
}
~SharedPtr() {
if (!mRefCount) return;
if (--*mRefCount == 0) {
mDeleter->Destroy(mResource);
delete mRefCount;
#if defined(DEBUG_SHARED_PTR)
std::cout << "~SharedPtr(): resource destroyed"
<< std::endl;
#endif
}
delete mDeleter;
}
// Typically, Resource will be some type of pointer, and there is no way
// to safely get the raw type since we are not restrict to hold pointers.
// Therefore, we couldn't restrict modification of the returned raw handler.
// Actually, this class is design to hold any "resource". That's way the
// template parameter called "Resource".
// Since the class is named "SharedPtr", we call this function "pointer"
Resource pointer() const noexcept {
return mResource;
}
private:
class ResourceDeleter {
public:
virtual void Destroy(Resource resource) noexcept = 0;
virtual ResourceDeleter* Clone() = 0;
virtual ~ResourceDeleter() noexcept {}
};
template<typename Deleter>
class DeleterImpl : public ResourceDeleter {
public:
DeleterImpl(Deleter deleter) : mDeleter{deleter} {
}
void Destroy(Resource resource) noexcept override {
mDeleter(resource);
}
ResourceDeleter*
Clone() {
return new DeleterImpl<Deleter>{mDeleter};
}
private:
Deleter mDeleter;
};
int* mRefCount;
Resource mResource;
ResourceDeleter* mDeleter;
template<typename T>
void swap(SharedPtr<T>& other) {
std::swap(mRefCount, other.mRefCount);
std::swap(mResource, other.mResource);
std::swap(mDeleter, other.mDeleter);
}
};
}
#endif