【问题标题】:Is it possible to invert order of destruction?是否可以颠倒破坏顺序?
【发布时间】:2016-01-18 08:47:54
【问题描述】:

我有一个实现许多基本功能的基类,它需要一些必须由继承它的类(或用户)提供的“存储”(内存块)。

class Base
{
public:

    Base(void* storage, size_t storageSize) :
        storage_{storage},
        storageSize_{storageSize}
    {
        // do something with the storage...
    }

    ~Base()
    {
        // do something with the storage...
    }

    // member functions

private:

    void* storage_;
    size_t storageSize_;
};

这里需要注意的是,这个内存块在构造函数和析构函数中使用的。

这在子类使用静态存储时效果很好:

template<size_t Size>
class StaticObject : public Base
{
public:

    StaticObject() :
        Base{&storage, Size}
    {

    }

private:

    typename std::aligned_storage<Size>::type staticStorage_;
};

我知道存储是在构造之前使用的(在 Base 的构造函数完成后“构造”)和在销毁之后(在 Base 的析构函数开始运行之前“销毁”),但是对于微不足道的std::aligned_storage&lt;...&gt;::type 这没什么区别。

但是,当我想将它与动态分配的存储一起使用时,这个想法完全失败了:

class DynamicObject : public Base
{
public:

    DynamicObject(size_t size) :
        DynamicObject{std::unique_ptr<uint8_t>{new uint8_t[size]}, size}
    {

    }

private:

    DynamicObject(std::unique_ptr<uint8_t>&& dynamicStorage, size_t size) :
        Base{dynamicStorage.get(), size},
        dynamicStorage_{std::move(dynamicStorage)}
    {

    }

    std::unique_ptr<uint8_t> dynamicStorage_;
};

正如您在委托构造函数中看到的那样,我设法创建(分配)存储空间,然后它将用于Base 的构造函数 - 稍微“颠倒”构造顺序。这个特定阶段也可以正常工作。问题是析构函数,因为我真的想不出任何解决我的问题的方法 - 在上面的代码中,动态分配的存储将在 Base 的析构函数开始运行之前被释放(并使用这个内存块)......

现在我必须以不同的方式解决这个问题 - DynamicObject 类包含 unique_ptrBase 的对象作为成员变量,而不是从 Base 继承 - 这样我可以控制构造顺序/破坏,但也有一些负面的方面:

  • 我必须为来自Base 的每个函数提供一个包装器,这些函数应该由派生类公开
  • 我必须提供转换运算符来(const)引用Base,因为我想通过引用基类来使用对象

我考虑过使用多重继承,这样DynamicObject 将从两个基础继承——一个提供存储(私有继承)和一个从Base(继承功能)——这样我也可以获得正确的构造/销毁顺序,但以使用“邪恶”多重继承为代价......

请注意,上面的示例只是一个简化。真正的用例是我正在编写的 RTOS(https://github.com/DISTORTEC/distortos)的线程和消息队列之类的对象 - 有关实际示例,请参见 Dynamic*.hppStatic*.hpp 对象 - https://github.com/DISTORTEC/distortos/tree/master/include/distortos

有什么聪明的技巧可以用来以某种方式颠倒破坏顺序吗?类似于上面DynamicObject 的委托构造函数的使用?也许有更好的方法来达到同样的效果?

【问题讨论】:

  • 将 Base 拆分为两个类,一个存储内存(并且是多态的)的类和一个使用内存的类。让后者持有前者的实例,作为构造函数参数传递。
  • 我担心按照这些思路进行的任何设计都会很脆弱,而且你最好多考虑整体设计而不是如何制作这个特定的细节实施方法工作。
  • 定义分配/释放接口,管理基类内存,并提供派生类的实现。
  • @AlanStokes - 它不起作用,因为构造函数/析构函数中的虚函数不像“正常”函数那样工作。
  • @FreddieChopin 是的,但如果内存管理器是一个单独的对象,则无关紧要,这就是我的建议。

标签: c++ c++11 inheritance constructor destructor


【解决方案1】:

您需要使用类型擦除删除器,与 shared_ptr 使用的方式相同(但不是 unique_ptr 使用的方式,它成为类型的一部分)。

class Base
{
public:
    typedef void (*StorageDeleter)(void*);

    Base(void* storage, size_t storageSize, StorageDeleter deleter = nullptr) :
        storage_{storage},
        storageSize_{storageSize},
        deleter_{deleter}
    {
        // do something with the storage...
    }

    virtual ~Base()
    {
        // do something with the storage...
        if (deleter_) deleter_(storage_);
    }

    // member functions

private:
    void* storage_;
    size_t storageSize_;
    StorageDeleter deleter_;
};

/* no changes to this one */
template<size_t Size>
class StaticObject;

class DynamicObject : public Base
{
    static void array_deleter(void* p) { uint8_t* pExact = (uint8_t*)p; delete [] pExact; }
public:
    DynamicObject(size_t size) :
        DynamicObject{std::unique_ptr<uint8_t[]>{new uint8_t[size]}, size}
    {

    }

private:
    DynamicObject(std::unique_ptr<uint8_t[]>&& dynamicStorage, size_t size) :
        Base{dynamicStorage.get(), size, &DynamicObject::array_deleter},
    {
        dynamicStorage.release();
    }
};

请注意,删除器是一个静态成员函数——它不需要DynamicObject 的实时派生实例才能正确运行。

我还修复了您对std::unique_ptr 的使用,以便在Base 构造期间抛出异常的情况下使用数组释放器(构造后由删除函数负责)。

现在,考虑 (pointer+deleter) 已经存在,形式为std::unique_ptr&lt;T, Deleter&gt;。所以你可以这样做:

class Base
{
    typedef void (*StorageDeleter)(void*);
    typedef std::unique_ptr<void, StorageDeleter> AutofreePtr;

public:
    Base(AutofreePtr&& storage, size_t storageSize) :
        storage_{std::move(storage)},
        storageSize_{storageSize}
    {
        // do something with the storage...
    }

    virtual ~Base()
    {
        // do something with the storage...
    }

    // member functions

private:
    AutofreePtr storage_;
    size_t storageSize_;
};

template<size_t Size>
class StaticObject : public Base
{
    static void no_delete(void*) {}
public:

    StaticObject() :
        Base{{&storage, &StaticObject::no_delete}, Size}
    {

    }

private:

    typename std::aligned_storage<Size>::type staticStorage_;
};

class DynamicObject : public Base
{
    static void array_deleter(void* p) { uint8_t* pExact = (uint8_t*)p; delete [] pExact; }
public:

    DynamicObject(size_t size) :
        DynamicObject{{new uint8_t[size], &DynamicObject::array_deleter}, size}
    {

    }
};

【讨论】:

  • IMO,如果想走这条路,应该认真考虑使用std::shared_ptr;增加的简单性和灵活性可能会超过它所带来的轻微开销。
  • @Hurkyl:但是即使你使用空删除器,shared_ptr 仍然会动态分配元数据对象,违反了要求。我确实考虑过,并认为支付“引用计数”成本是可以的,但当问题明确排除它时,不要添加动态分配。
  • @Hurkyl:另一方面,应该能够使用std::unique_ptr&lt;uint8_t[], void (*)(void*)&gt; 并通过无操作删除器或default_deleter,而无需支付更多开销。
  • @BenVoigt - 您的最后一条评论(以及您的回答)似乎是一个非常好的主意 - 我可以将此类 std::unique_ptr 放在 Base 中,并将 std::move() 放在派生类的构造函数中。这似乎是一个完美的解决方案,也可能是最短的!
  • @FreddieChopin:好的,我想这就是它的样子。
【解决方案2】:

这避免了在完全初始化之前使用存储对象进行工作的问题。

class Base
{
public:

    Base(void* storage, size_t storageSize) :
        storage_{storage},
        storageSize_{storageSize}
    {
    }

    virtual ~Base()
    {
    }

private:
    void* storage_;
    size_t storageSize_;
};

class Worker
{
    Worker(Base* storage)
        : storage(storage)
    {
        // do something with the storage
    }

    ~Worker()
    {
        // do something with the storage
    }

    Base* storage;
};

Base* storage = new FancyStorage;
Worker w(storage);
delete storage;

我避免使用智能指针以保持简单,因为我不知道您希望如何拥有存储对象。

【讨论】:

  • 这里没有继承 :( 多态性似乎是问题的关键要求。
  • @BenVoigt FancyStorage 的实现留作练习。请参阅最后几行代码。
  • @BenVoigt - 我不需要多态性,但我希望派生对象易于使用,并且“静态”变体不得使用任何动态内存。
  • @NeilKirk - 我不知道如何将您的解决方案应用于我的问题。我希望这些对象完全“包含”(可独立使用)。您将如何从问题中划分 Base、StaticObject 和 DynamicObject 以使用您的 Base 和 Worker?每个部分会去哪里?
  • @FreddieChopin 我不明白。不是将存储直接合并到使用它做“某事”的类中,而是将存储抽象到另一组类中。
【解决方案3】:

听起来你真正想要的是颠倒层次结构; Base 应该有一些存储空间,而不是说,StaticObjectBase。例如,这可以使用泛型来实现

template< typename Storage >
class Base
{
    Storage storage; // Storage could be a private base class too
public:
    // Fix: use perfect forwarding
    template< typename T... >
    Base(T ...args):Storage(args...) { /* More initialization */ }

    ~Base() {
        // Still safe to use storage!
    }

    void set_all_storate_to_zero()
    {
        memset(storage.ptr(), 0, storage.size());
    }
};

【讨论】:

  • 什么是多态指针类型?您不能在此解决方案中使用Base*,您需要Base&lt;Storage&gt;*,它不允许混合存储方案。
  • 我也考虑过这一点,但这会重复编译的代码 - Base 和 Base 将是两个不同的类(因此实际上它们并不常见),它们的集合不同(相同的)功能。
  • @Ben: OP 中的Base 已经不适合动态多态。如果 OP 如此需要,他可以将 Storage 设为多态类型。
  • @Hurkyl:但是,Base&lt;Storage&gt; 不会以多态方式使用Storage
猜你喜欢
  • 2021-12-30
  • 2021-06-10
  • 2011-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-01
  • 1970-01-01
相关资源
最近更新 更多