Ответ 1
Эта программа не очень хорошо определена.
Правило состоит в том, что если тип имеет тривиальный деструктор (см. Это), вам не нужно вызывать его. Итак, это:
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[]
.
(Все эти "may" могут быть вызваны тем, что реализациям разрешено объединять смежные новые выражения и вызывать 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 new[]
и operator delete[]
выполняют те же действия, что и operator new
и operator delete
, проблема в том, что мы не знаем ему передан размер, и он, вероятно, больше, чем то, что вызывает delete ((T*) object)
(чтобы сохранить размер).
Смотря что делают delete-выражения:
§8.5.2.5p8 [expr.delete]
[...] delete-выражение вызовет деструктор (если есть) для [...] элементов удаляемого массива
P7.1
Если вызов выделения для нового выражения для удаляемого объекта не был опущен [...], выражение удаления должно вызвать функцию освобождения (6.6.4.4.2). Значение, возвращаемое из вызова выделения нового выражения, должно быть передано в качестве первого аргумента функции освобождения.
Поскольку std::byte
не имеет деструктора, мы можем безопасно вызывать delete[]
, поскольку он не будет делать ничего, кроме вызова функции deallocate (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
и запускает время жизни нового объекта 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
чтобы 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
потому что выровненное хранилище является тривиальным агрегатом.