C++ 自定义智能指针

Posted

技术标签:

【中文标题】C++ 自定义智能指针【英文标题】:C++ Custom Smart Pointer 【发布时间】:2017-08-16 22:01:02 【问题描述】:

最近我尝试实现我自己的智能指针版本。实现看起来有点像下面:

class Var 
private:
    void* value;
    unsigned short* uses;
public:
    Var() : value(nullptr), uses(new unsigned short(1))  
    template<typename K>
    Var(K value) : value((void*)new K(value)), uses(new unsigned short(1))  
    Var(const Var &obj) 
        value = obj.value;
        (*(uses = obj.uses))++;
    
    ~Var() 
        if (value == nullptr && uses == nullptr) return;
        if (((*uses) -= 1) <= 0) 
            delete value;
            delete uses;
            value = uses = nullptr;
        
    
    Var& operator=(const Var& obj) 
        if (this != &obj) 
            this->~Var();
            value = obj.value;
            (*(uses = obj.uses))++;
        
        return *this;
    
;

实现应该是直截了当的,因为value 持有指针,uses 计算引用。 请注意,指针存储为void*,并且指针类未固定为某些(通用)类型。

问题

智能指针在大多数情况下都在发挥作用……但以下情况除外:

class C 
public:
    Var var;
    C(Var var) : var(var) 
;
void test() 
    std::string string = std::string("Heyo");
    Var var1 = Var(string);
    C c = C(var1);
    Var var2 = Var(c);

void main() 
    test();

运行该代码时,第一个实例var1test 运行后不会被删除。 是的,使用void* 并不是最好的方法。然而,让我们不要离开话题。代码编译得非常好(如果有人可能会质疑我对子分配运算符的使用)。如果错误出现在删除 void* 中,则引用计数器 uses 将被删除,但事实并非如此。 我之前已经检查过析构函数,它们都被调用了。 还要注意程序运行时没有错误。

提前谢谢大家, 谢尔顿

【问题讨论】:

What is The Rule of Three?的可能重复 您有哪些标准库版本无法满足的“需求”? @Sheldon,在void* 上调用delete 是不对的。见timsong-cpp.github.io/cppwp/n3337/expr.delete#1。 你可能有多个错误,但void* 的东西是一个破坏整个概念的阻碍。 ideone.com/t02HsI 请注意,析构函数没有为 void* 运行 @Sheldon:如果您的代码具有未定义的行为,那么它是否可以正常工作并不重要。还是错了。 【参考方案1】:

我看到您的代码存在三个大问题:

    您将分配的对象指针存储为void*,然后按原样调用delete。这不会调用对象的析构函数。在调用delete 之前,您必须将void* 类型转换回原始类型,但您不能这样做,因为在Var 构造函数退出后您已经丢失了类型信息。

    您已将对象指针和引用计数器彼此分开。它们应始终保持在一起。最好的方法是将它们存储在 struct 中,然后根据需要进行分配和传递。

    您的operator= 正在调用this-&gt;~Var(),这是完全错误的。一旦你这样做了,this 指向的对象就不再有效!您需要保持实例处于活动状态,因此只需减少其当前引用计数器,如果需要释放其存储的对象,然后从源 Var 复制指针并增加该引用计数器。

试试这个替代实现 (Live Demo):

class Var

private:
    struct controlBlockBase
    
        unsigned short uses;    

        controlBlockBase() : uses(1)  
        virtual ~controlBlockBase()  
    ;

    template <class K>
    struct controlBlockImpl : controlBlockBase
    
        K value;
        controlBlockImpl(const K &val) : controlBlockBase(), value(val) 
    ;

    controlBlockBase *cb;

public:
    Var() : cb(nullptr)  

    template<typename K>
    Var(const K &value) : cb(new controlBlockImpl<K>(value))  

    Var(const Var &obj) : cb(obj.cb) 
        if (cb) 
            ++(cb->uses);
        
    

    Var(Var &&obj) : cb(nullptr) 
        obj.swap(*this);
    

    ~Var() 
        if ((cb) && ((cb->uses -= 1) <= 0)) 
            delete cb;
            cb = nullptr;
        
    

    Var& operator=(const Var& obj) 
        if (this != &obj) 
            Var(obj).swap(*this);
        
        return *this;
    

    Var& operator=(Var &&obj) 
        obj.swap(*this);
        return *this;
    

    /* or, the two above operator= codes can be
    merged into a single implementation, where
    the input parameter is passed by non-const
    value and the compiler decides whether to use
    copy or move semantics as needed:

    Var& operator=(Var obj) 
        obj.swap(*this);
        return *this;
        
    */

    void swap(Var &other)
    
        std::swap(cb, other.cb);
    

    unsigned short getUses() const 
        return (cb) ? cb->uses : 0;
    

    template<class K>
    K* getAs() 
        if (!cb) return nullptr;
        return &(dynamic_cast<controlBlockImpl<K>&>(*cb).value);
    
;

void swap(Var &v1, Var v2) 
    v1.swap(v2);


更新:话虽这么说,Var 所做的基本上与使用 std::any 包裹在 std::shared_ptr 中的效果相同,所以您不妨直接使用它们( std::any 仅适用于 C++17 及更高版本,早期版本使用 boost::any):

class Var

private:
    std::shared_ptr<std::any> ptr;

public:
    template<typename K>
    Var(const K &value) : ptr(std::make_shared<std::any>(value))  

    void swap(Var &other) 
        std::swap(ptr, other.ptr);
    

    long getUses() const 
        return ptr.use_count();
    

    template<class K>
    K* getAs() 
        return any_cast<K>(ptr.get());
    
;

void swap(Var &v1, Var &v2) 
    v1.swap(v2);

【讨论】:

@MooingDuck:谢谢,我更新了我的答案和演示 @Sheldon:如果指针不是用placement-new 分配的,那么直接调用析构函数是未定义的行为。此代码旨在避免调用析构函数时的强制转换问题。并且请不要使用static_cast,如果您转换为实际未存储的错误类型,则会引入新的错误。使用dynamic_cast 的全部意义在于确保有效转换为正确的类型。如果存储X 值,则只能检索X 指针。如果你想要一个基类指针,在检索到基类指针后将检索到的指针分配给它。 @Sheldon:你不能在controlBlockImpl&lt;Derived&gt;controlBlockImpl&lt;Base&gt; 之间转换,它们是不相关的类型。如果KBase(为什么?你会slice the stored object!),你必须从controlBlockImpl&lt;Base&gt; 检索Base*,然后将Base* 转换为Derived*(由于切片而无效) !)。如果你有一个Derived 源对象,你必须通过controlBlockImpl&lt;Derived&gt; 存储一个Derived 对象以避免切片,然后从中检索一个Derived*,并将Derived* 分配给Base* 而不进行强制转换。 @Sheldon 您在Var 中存储对象的副本 的要求违背了智能指针的用途。您可以只使用 std::variantstd::any 代替,包裹在 std::shared_ptr 中进行引用计数,以获得 Var 尝试手动完成的相同效果。 @Sheldon:您所描述的将行不通。如果您存储对象的副本,则必须将Derived 对象存储为Derived,以便取回有效的Derived。如果将其存储为 sliced Base,则无法返回有效的 Derived!再多的选角也无法做到这一点。切片会破坏Derived 数据,只留下Base 数据。如果您想将Derived 对象存储为Base,以便稍后检索Derived,则不能存储对象的副本,必须存储@987654380 @指向原始Derived对象的指针。这是所有标准智能指针所做的

以上是关于C++ 自定义智能指针的主要内容,如果未能解决你的问题,请参考以下文章

c++三种智能指针以及相关知识自写智能指针

C++ 中的智能指针-基础

C++原生指针,引用与智能指针

c++复习笔记——智能指针详细解析(智能指针的使用,原理分析)

Visual Studio中智能指针对象的自定义视图?

C++ 浅析智能指针