Является ли явное определение конструкторов и деструкторов безопасным, в соответствии со стандартом С++?
Некоторые разработчики явно вызывают конструкторы и деструкторы для некоторых обходных решений. Я знаю, это не очень хорошая практика, но, похоже, это делается для реализации некоторых сценариев.
Например, в этой статье Красивые национальные библиотеки автор использует эту технику.
В приведенном ниже коде, в конце, можно видеть, что конструктор явно вызывается:
#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, является ли это неуказанным поведением, является ли это поведение, определенное реализацией, или оно четко определено
Ответы
Ответ 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.: Просто потому, что стандартные контейнеры внутренне используют новое место размещения, это не означает, что вы должны сходить с ума в свой собственный код. Это низкоуровневая техника, и если вы не реализуете свою собственную общую структуру данных, потому что ни один стандартный контейнер не предоставляет необходимые вам функциональные возможности, вы никогда не сможете использовать его вообще.