Подтвердить что ты не робот

Является ли явное определение конструкторов и деструкторов безопасным, в соответствии со стандартом С++?

Некоторые разработчики явно вызывают конструкторы и деструкторы для некоторых обходных решений. Я знаю, это не очень хорошая практика, но, похоже, это делается для реализации некоторых сценариев.

Например, в этой статье Красивые национальные библиотеки автор использует эту технику.

В приведенном ниже коде, в конце, можно видеть, что конструктор явно вызывается:

#include <limits>

template <class T>
struct proxy_allocator {
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
    typedef T *pointer;
    typedef const T *const_pointer;
    typedef T& reference;
    typedef const T &const_reference;
    typedef T value_type;

    template <class U>
    struct rebind {
        typedef proxy_allocator<U> other;
    };

    proxy_allocator() throw() {}
    proxy_allocator(const proxy_allocator &) throw() {}
    template <class U>
    proxy_allocator(const proxy_allocator<U> &) throw() {}
    ~proxy_allocator() throw() {}

    pointer address(reference x) const { return &x; }
    const_pointer address(const_reference x) const { return &x; }

    pointer allocate(size_type s, void const * = 0) {
        return s ? reinterpret_cast<pointer>(yl_malloc(s * sizeof(T))) : 0;
    }

    void deallocate(pointer p, size_type) {
        yl_free(p);
    }

    size_type max_size() const throw() {
        return std::numeric_limits<size_t>::max() / sizeof(T);
    }

    void construct(pointer p, const T& val) {
        new (reinterpret_cast<void *>(p)) T(val);
    }

    void destroy(pointer p) {
        p->~T();
    }

    bool operator==(const proxy_allocator<T> &other) const {
        return true;
    }

    bool operator!=(const proxy_allocator<T> &other) const {
        return false;
    }
};

Для некоторых сценариев, подобных этому, может быть необходимо явно вызвать конструкторы и деструкторы, но что говорит стандарт: это поведение undefined, является ли это неуказанным поведением, является ли это поведение, определенное реализацией, или оно четко определено

4b9b3361

Ответ 1

Да, он поддерживается и четко определен, он безопасен.

new (reinterpret_cast<void *>(p)) T(val);

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

Единственный способ уничтожить такой сконструированный объект - явно вызвать деструктор p->~T();.

Использование места размещения нового и явного уничтожения требует/допускает, что реализованный код управляет временем жизни объекта - в этом случае компилятор мало помогает; поэтому важно, чтобы объекты были построены в хорошо выровненных и достаточно распределенных местах. Их использование часто встречается в распределителях, например в OP, и std::allocator.

Ответ 2

Да, это абсолютно безопасно. По сути, все стандартные стандартные контейнеры, такие как std::vector, используют технику по умолчанию, поскольку это единственный способ разделить выделение памяти от конструкции элемента.

Более точно, шаблоны стандартных контейнеров имеют аргумент шаблона Allocator, который по умолчанию равен std::allocator, а std::allocator использует размещение new в своем allocate функция-член.

Это, например, то, что позволяет std::vector реализовать push_back таким образом, что распределение памяти не должно происходить все время, но вместо этого выделяется дополнительная память, когда текущая емкость больше не достаточна, готовя пространство для элементов, добавленных с будущими push_back s.

Это означает, что когда вы вызываете push_back сто раз в цикле, std::vector на самом деле достаточно умный, чтобы не выделять память каждый раз, что помогает производительности, поскольку перераспределение и перемещение содержимого контейнера в новое место памяти дорогостоящий.

Пример:

#include <vector>
#include <iostream>

int main()
{
    std::vector<int> v;

    std::cout << "initial capacity: " << v.capacity() << "\n";

    for (int i = 0; i < 100; ++i)
    {
        v.push_back(0);

        std::cout << "capacity after " << (i + 1) << " push_back()s: "
            << v.capacity() << "\n";
    }
}

Вывод:

initial capacity: 0
capacity after 1 push_back()s: 1
capacity after 2 push_back()s: 2
capacity after 3 push_back()s: 3
capacity after 4 push_back()s: 4
capacity after 5 push_back()s: 6
capacity after 6 push_back()s: 6
capacity after 7 push_back()s: 9
capacity after 8 push_back()s: 9
capacity after 9 push_back()s: 9
capacity after 10 push_back()s: 13
capacity after 11 push_back()s: 13
capacity after 12 push_back()s: 13
capacity after 13 push_back()s: 13
capacity after 14 push_back()s: 19

(...)

capacity after 94 push_back()s: 94
capacity after 95 push_back()s: 141
capacity after 96 push_back()s: 141
capacity after 97 push_back()s: 141
capacity after 98 push_back()s: 141
capacity after 99 push_back()s: 141
capacity after 100 push_back()s: 141

Но, конечно, вы не хотите вызывать конструктор для потенциальных будущих элементов. Для int это не имеет значения, но нам нужно решение для каждого T, включая типы без конструкторов по умолчанию. Это сила размещения new: сначала выделите память, затем поместите элементы в выделенную память позже, используя вызов конструктора вручную.


Как побочное примечание, все это было бы невозможно с new[]. Фактически, new[] - довольно бесполезная языковая функция.


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