Можно ли сделать новое размещение в памяти управляемым умным указателем?

контекст

Для целей тестирования мне нужно построить объект на ненулевой памяти. Это можно сделать с помощью:

{
    struct Type { /* IRL not empty */};
    std::array<unsigned char, sizeof(Type)> non_zero_memory;
    non_zero_memory.fill(0xC5);
    auto const& t = *new(non_zero_memory.data()) Type;
    // t refers to a valid Type whose initialization has completed.
    t.~Type();
}

Поскольку это утомительно и выполняется несколько раз, я хотел бы предоставить функцию, возвращающую умный указатель на такой экземпляр Type. Я придумал следующее, но я боюсь, что где-то скрывается неопределенное поведение.

Вопрос

Хорошо ли определена следующая программа? В частности, является ли тот факт, что std::byte[] был выделен, но Type эквивалентного размера освобождается проблема?

#include <cstddef>
#include <memory>
#include <algorithm>

auto non_zero_memory(std::size_t size)
{
    constexpr std::byte non_zero = static_cast<std::byte>(0xC5);

    auto memory = std::make_unique<std::byte[]>(size);
    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));
    return std::shared_ptr<T>(new (memory.release()) T());
}    

int main()
{
    struct Type { unsigned value = 0; ~Type() {} }; // could be something else
    auto t = on_non_zero_memory<Type>();
    return t->value;
}

Живая демо

Ответы

Ответ 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 потому что выровненное хранилище является тривиальным агрегатом.

Ответ 2

std::shared_ptr<T>(new (memory.release()) T())

Неопределенное поведение. Память, которая была получена memory была для std::byte[] но средство delete shared_ptr выполняет вызов delete для указателя на T Так как указатель больше не имеет тот же тип, вы не можете вызвать delete для него в [expr.delete]/2

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

Вы должны будете предоставить shared_ptr пользовательским средством удаления, которое уничтожает T а затем возвращает указатель на его тип источника и вызывает delete[] для этого.


Также следует отметить, что new (memory.release()) T() сам по себе будет неопределенным, если в memory выделен тип, который имеет нетривиальное уничтожение. Вам придется вызвать деструктор указателя из memory.release() прежде чем использовать его в памяти.