Можно ли сделать новое размещение в памяти управляемым умным указателем? - PullRequest
0 голосов
/ 08 января 2019

Контекст

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

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

Живая демоверсия

Ответы [ 2 ]

0 голосов
/ 08 января 2019

Эта программа плохо определена.

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

0 голосов
/ 08 января 2019
std::shared_ptr<T>(new (memory.release()) T())

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

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

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


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

...