Эта программа плохо определена.
Правило таково, что если тип имеет тривиальный деструктор (см. this ), вам не нужно вызывать его. Итак, это:
return std::shared_ptr<T>(new (memory.release()) T());
почти правильно. Он опускает деструктор sizeof(T)
std::byte
s, который в порядке, создает новый T
в памяти, что нормально, а затем, когда shared_ptr
готов удалить, он вызывает delete this->get();
, что неправильно. Это сначала деконструирует T
, но затем освобождает T
вместо std::byte[]
, что , вероятно (не определено) не будет работать.
Стандарт C ++ §8.5.2.4p8 [expr.new]
Новое выражение может получить хранилище для объекта, вызвав функцию выделения. [...] Если выделенный тип является типом массива, имя функции выделения будет operator new[]
.
(Все эти «майские» аргументы в пользу того, что реализациям разрешено объединять смежные новые выражения и вызывать operator new[]
только для одного из них, но это не так, поскольку new
происходит только один раз (в make_unique
))
И часть 11 того же раздела:
Когда новое выражение вызывает функцию распределения, и это распределение не было расширено, новое выражение передает количество пространства, запрошенное функции распределения, в качестве первого аргумента типа std::size_t
. Этот аргумент должен быть не меньше размера создаваемого объекта; он может быть больше размера создаваемого объекта, только если объект является массивом. Для массивов char
, unsigned char
и std::byte
разница между результатом выражения new и адресом, возвращаемым
Функция выделения должна быть целым кратным самого строгого требования фундаментального выравнивания (6.6.5) любого типа объекта, размер которого не превышает размер создаваемого массива. [Примечание: потому что распределение
Предполагается, что функции возвращают указатели на хранилище, которые соответствующим образом выровнены для объектов любого типа с фундаментальным выравниванием, это ограничение на накладные расходы на выделение массива допускает общую идиому распределения
массивы символов, в которые впоследствии будут помещены объекты других типов. - конец примечания]
Если вы прочитаете §21.6.2 [new.delete.array], вы увидите, что значения по умолчанию operator new[]
и operator delete[]
выполняют те же действия, что и operator new
и operator delete
, проблема в том, что мы не Не знаю, какой размер передан ему, и он , вероятно, больше, чем delete ((T*) object)
(для хранения размера).
Посмотрим, что делают delete-выражения:
§8.5.2.5p8 [expr.delete]
[...] delete-expression вызовет деструктор (если есть) для [...] элементов удаляемого массива
P7.1
Если вызов выделения для нового выражения для удаляемого объекта не был опущен [...], выражение удаления должно вызывать функцию освобождения (6.6.4.4.2). Значение, возвращаемое из вызова выделения нового выражения, должно быть передано в качестве первого аргумента функции освобождения.
Поскольку std::byte
не имеет деструктора, мы можем безопасно вызвать delete[]
, поскольку он не будет делать ничего, кроме вызова функции освобождения (operator delete[]
). Нам просто нужно переосмыслить его обратно в std::byte*
, и мы вернем то, что вернул new[]
.
Другая проблема - утечка памяти, если конструктор T
выбрасывает. Простое исправление заключается в размещении new
, пока память все еще принадлежит std::unique_ptr
, поэтому, даже если она выдает команду, она правильно вызовет delete[]
.
T* ptr = new (memory.get()) T();
memory.release();
return std::shared_ptr<T>(ptr, [](T* ptr) {
ptr->~T();
delete[] reinterpret_cast<std::byte*>(ptr);
});
Первое размещение new
заканчивает время жизни sizeof(T)
std::byte
s и запускает время жизни нового T
объекта по тому же адресу, как указано в §6.6.3p5 [basic.life]
Программа может закончить время жизни любого объекта, повторно используя хранилище, которое занимает объект, или явно вызывая деструктор для объекта типа класса с нетривиальным деструктором. [...]
Затем, когда он удаляется, время жизни T
заканчивается явным вызовом деструктора, а затем, согласно вышесказанному, выражение delete освобождает хранилище.
Это приводит к вопросу:
Что если класс хранения не был std::byte
и не был тривиально разрушаем? Как, например, мы использовали нетривиальный союз в качестве хранилища.
Вызов delete[] reinterpret_cast<T*>(ptr)
вызовет деструктор для чего-то, что не является объектом. Это явно неопределенное поведение, и оно соответствует §6.6.3p6 [basic.life]
До начала жизни объекта, но после того, как хранилище, которое объект будет занимать, было выделено [...], любой указатель, представляющий адрес места хранения, где будет находиться объект, или
может быть использован, но только в ограниченном количестве. [...] Программа имеет неопределенное поведение, если: объект будет или имел тип класса с нетривиальным деструктором, а указатель используется в качестве операнда выражения удаления
Таким образом, чтобы использовать его, как описано выше, мы должны сконструировать его просто, чтобы снова уничтожить.
Конструктор по умолчанию, вероятно, работает нормально. Обычная семантика - «создать объект, который может быть разрушен», и это именно то, что мы хотим. Используйте std::uninitialized_default_construct_n
, чтобы построить их все, чтобы затем немедленно уничтожить их:
// Assuming we called `new StorageClass[n]` to allocate
ptr->~T();
auto* as_storage = reinterpret_cast<StorageClass*>(ptr);
std::uninitialized_default_construct_n(as_storage, n);
delete[] as_storage;
Мы также можем позвонить operator new
и operator delete
самим:
static void byte_deleter(std::byte* ptr) {
return ::operator delete(reinterpret_cast<void*>(ptr));
}
auto non_zero_memory(std::size_t size)
{
constexpr std::byte non_zero = static_cast<std::byte>(0xC5);
auto memory = std::unique_ptr<std::byte, void(*)(std::byte*)>(
reinterpret_cast<std::byte*>(::operator new(size)),
&::byte_deleter
);
std::fill(memory.get(), memory.get()+size, non_zero);
return memory;
}
template <class T>
auto on_non_zero_memory()
{
auto memory = non_zero_memory(sizeof(T));
T* ptr = new (memory.get()) T();
memory.release();
return std::shared_ptr<T>(ptr, [](T* ptr) {
ptr->~T();
::operator delete(ptr, sizeof(T));
// ^~~~~~~~~ optional
});
}
Но это очень похоже на std::malloc
и std::free
.
Третье решение может состоять в том, чтобы использовать std::aligned_storage
в качестве типа, заданного для new
, и использовать средство удаления как с std::byte
, поскольку выровненное хранилище является тривиальным агрегатом.