Можно ли инвертировать порядок уничтожения?

У меня есть базовый класс, который реализует много основных функций, и ему нужно некоторое "хранилище" (блок памяти), которое должно быть предоставлено классом, который его наследует (или пользователем).

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<...>::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 начинает работать (и использует этот блок памяти)...

На данный момент я должен был решить это по-другому - вместо того, чтобы наследовать от Base, DynamicObject класс содержит unique_ptr и объект Base в качестве переменных-членов - таким образом я контролирую порядок строительства / разрушения, но есть и некоторые отрицательные стороны:

  • Я должен предоставить оболочку для каждой функции от Base которые должны быть выставлены производными классами
  • Я должен предоставить операторы преобразования в (const) ссылку на Base, как я хотел бы использовать объекты через ссылку на базовый класс

Я думал об использовании множественного наследования, так что DynamicObject будет наследоваться от двух баз - одна, которая будет предоставлять хранилище (частное наследование) и от Base (для наследования функциональности) - таким образом я бы также получил правильный порядок строительства / разрушения, но ценой использования "злого" множественного наследования...

Обратите внимание, что приведенный выше пример является лишь упрощением. Реальными вариантами использования являются объекты, такие как потоки и очереди сообщений для RTOS, которую я пишу ( https://github.com/DISTORTEC/distortos) - см. Dynamic*.hpp а также Static*.hpp объекты для реальных примеров - https://github.com/DISTORTEC/distortos/tree/master/include/distortos

Есть ли какая-нибудь хитрая уловка, которую я мог бы использовать, чтобы каким-то образом изменить порядок уничтожения? Что-то вроде использования делегирующего конструктора DynamicObject выше? Может быть, есть лучший способ добиться того же результата?

3 ответа

Решение

Вам нужно использовать удаленный тип, таким же образом, что 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 (после создания, функция удаления отвечает).

Теперь рассмотрим, что (указатель + удаление) уже существует, в виде std::unique_ptr<T, Deleter>, Так что вы можете сделать:

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

    }
};

Похоже, что вы действительно хотите, чтобы изменить иерархию; Base должно иметь некоторое хранилище, а не, скажем, StaticObject иметь Base, Это может быть реализовано, например, с помощью дженериков.

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

Это позволяет избежать проблемы использования объекта хранения для работы до его полной инициализации.

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;

Я избегал умных указателей, чтобы не усложнять, потому что не знаю, как вы хотите, чтобы ваши объекты хранилища принадлежали.

Другие вопросы по тегам