Можно ли инвертировать порядок уничтожения?
У меня есть базовый класс, который реализует много основных функций, и ему нужно некоторое "хранилище" (блок памяти), которое должно быть предоставлено классом, который его наследует (или пользователем).
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;
Я избегал умных указателей, чтобы не усложнять, потому что не знаю, как вы хотите, чтобы ваши объекты хранилища принадлежали.