Анонимный союз С++ 11 с нетривиальными членами
Я обновляю мою структуру, и я хотел добавить к ней члена std::string. Исходная структура выглядит следующим образом:
struct Value {
uint64_t lastUpdated;
union {
uint64_t ui;
int64_t i;
float f;
bool b;
};
};
Просто добавление члена std::string к объединению, конечно, вызывает ошибку компиляции, потому что обычно нужно добавлять нетривиальные конструкторы объекта. В случае std::string (текст из informit.com)
Так как std::string определяет все шесть специальных функций-членов, U будет иметь неявно удаленный конструктор по умолчанию, конструктор копирования, оператор присваивания копии, конструктор перемещения, оператор назначения перемещения и деструктор. Эффективно это означает, что вы не можете создавать экземпляры U, если вы явно не определяете некоторые или все специальные функции-члены.
Затем веб-сайт продолжает приводить следующий пример кода:
union U
{
int a;
int b;
string s;
U();
~U();
};
Однако я использую анонимный союз внутри структуры. Я спросил ## С++ на freenode, и они сказали мне, что правильный способ сделать это - поместить конструктор в структуру вместо этого и дать мне этот примерный код:
#include <new>
struct Point {
Point() {}
Point(int x, int y): x_(x), y_(y) {}
int x_, y_;
};
struct Foo
{
Foo() { new(&p) Point(); }
union {
int z;
double w;
Point p;
};
};
int main(void)
{
}
Но оттуда я не могу понять, как сделать остальные специальные функции, определенные std::string, и, кроме того, я не совсем понимаю, как работает ctor в этом примере.
Могу ли я заставить кого-то объяснить это мне немного понятнее?
Ответы
Ответ 1
Здесь нет необходимости размещать новое место.
Члены Variant не будут инициализированы конструктором, сгенерированным компилятором, но не должно быть проблем с выбором и инициализацией, используя обычный список-инициализатор ctor. Члены, объявленные внутри анонимных объединений, фактически являются членами содержащего класса и могут быть инициализированы в конструкторе содержащего класса.
Это поведение описано в разделе 9.5. [class.union]
:
Союзный класс является объединением или классом, который имеет анонимный союз как прямой член. Объединенный класс X
имеет набор вариантов. Если X
является объединением, его членами варианта являются нестатические члены данных; в противном случае его членами варианта являются нестатические члены данных всех анонимных объединений, которые являются членами X
.
и в разделе 12.6.2 [class.base.init]
:
Инициализатор ctor может инициализировать вариантный член класса конструкторов. Если ctor-initializer задает более одного mem-инициализатора для одного и того же элемента или для одного и того же базового класса, ctor-инициализатор плохо сформирован.
Таким образом, код может быть просто:
#include <new>
struct Point {
Point() {}
Point(int x, int y): x_(x), y_(y) {}
int x_, y_;
};
struct Foo
{
Foo() : p() {} // usual everyday initialization in the ctor-initializer
union {
int z;
double w;
Point p;
};
};
int main(void)
{
}
Конечно, размещение new должно по-прежнему использоваться при винификации члена варианта, отличного от другого, инициализированного в конструкторе.
Ответ 2
Этот пример new (&p) Point()
- это вызов стандартного размещения new
оператора (через новое выражение размещения), поэтому вам нужно включить <new>
. Этот конкретный оператор отличается тем, что он не выделяет память, он возвращает только то, что вы ему передали (в данном случае это параметр &p
). Конечным результатом выражения является то, что объект был создан.
Если вы комбинируете этот синтаксис с явными вызовами деструктора, вы можете добиться полного контроля над временем жизни объекта:
// Let assume storage_type is a type
// that is appropriate for our purposes
storage_type storage;
std::string* p = new (&storage) std::string;
// p now points to an std::string that resides in our storage
// it was default constructed
// *p can now be used like any other string
*p = "foo";
// Needed to get around a quirk of the language
using string_type = std::string;
// We now explicitly destroy it:
p->~string_type();
// Not possible:
// p->~std::string();
// This did nothing to our storage however
// We can even reuse it
p = new (&storage) std::string("foo");
// Let not forget to destroy our newest object
p->~string_type();
Когда и где вы должны создавать и уничтожать член std::string
(пусть его называют s
) в вашем классе Value
зависит от вашего шаблона использования для s
. В этом минимальном примере вы никогда не строите (и, следовательно, уничтожаете) его в специальных членах:
struct Value {
Value() {}
Value(Value const&) = delete;
Value& operator=(Value const&) = delete;
Value(Value&&) = delete;
Value& operator=(Value&&) = delete;
~Value() {}
uint64_t lastUpdated;
union {
uint64_t ui;
int64_t i;
float f;
bool b;
std::string s;
};
};
Таким образом, допустимое использование Value
:
Value v;
new (&v.s) std::string("foo");
something_taking_a_string(v.s);
using string_type = std::string;
v.s.~string_type();
Как вы могли заметить, я отключил копирование и перемещение Value
. Причиной этого является то, что мы не можем копировать или перемещать соответствующий активный член союза, не зная, какой из них он активен, если он есть.