自己实现 SharedPtr(3)—— 进一步的转型(cast)支持

在上一篇文章中,虽然我们实现了一部分的类型转换支持,但是,我们并不能满足于此。既然我们的类叫做“SharedPtr”,那么,用户就有理由期望他的某些行为,能够和真正的pointer一致:

// Raw pointer
Derived* ptr;
Base* ptr2 = ptr;

// Smart pointer
SharedPtr<Derived *> sp;
SharedPtr<Base *> sp2 = sp;


首先,我们先来理一理我们所需要的工具:

  1. 为了能够支持任意的指针(资源)类型,我们的需要一个类模板
  2. 为了更易于使用(不需要同时指定资源类型和Deleter的类型),我们需要利用方法调用时的类型推断,因此,我们的构造函数是一个函数模板
  3. 我们的构造函数模板得到了任意类型的deleter后,我们需要某种方法把该对象存储起来。由于我们的SharedPtr只有一个模板参数,我们并没有办法直接在类中存放deleter(毕竟,我们根本不知道他的类型究竟是什么)。于是,利用多态,我们将其放至另一个对象中。
  4. 由于 copy construct 的新的对象所持有的是和参数一样的资源句柄(虽然由于类型转换,类型可能不同),两者必须使用同一个引用计数器。即我们必须有某种形式的int* mRefCount
  5. 即使是SharedPtr<T>之间的转型,我们也希望能够赋予用户更改其deleter的能力,即deleterresource之间的绑定不是永久的。
  6. 为了支持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 ctoroperator=() 的代码不能够理解,请确保你已经理解了上面关于 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





你可能感兴趣的:(C++,shared-ptr)