Правильно ли использовать std::string
(или другие нетривиальные типы) с int
(или другими тривиальными и нетривиальными типами) внутри одного объединения?
Я уже реализовал это так:
#include <iostream>
#include <string>
struct Foo
{
enum Type {data_string, data_int};
int m_type;
Foo(Type t) : m_type(t)
{
if (t == Type::data_string) {
new (&s) std::string();
}
}
~Foo()
{
if (m_type == Type::data_string) {
s.~basic_string();
}
}
union
{
int n;
std::string s;
};
};
int main()
{
Foo f1(Foo::Type::data_string);
f1.s = "hello ";
std::cout << f1.s;
Foo f2(Foo::Type::data_int);
f2.n = 100;
std::cout << f2.n;
}
и это работает очень хорошо. Но я не уверен насчет этого кода. Правильный ли это код с точки зрения стандарта C++?
@wohlstad спасибо за предложение, но в любом случае... я хочу знать, можно ли использовать этот код на C++ или существует какое-то неопределенное поведение?
Ваш код неуклюжий, но правильный. Union инициализируется и деинициализируется должным образом. UB будет в том случае, если вы инициализируете объединение для одного объекта и используете/деинициализируете его как другое.
Это основная реализация std::variant
. Почему вы хотите изобрести велосипед? Вы столкнетесь с утомительными неудачами и множеством ловушек, касающихся жизненных проблем. Оберните variant
в свой класс и предоставьте аксессор /*value_type*/* get<Type>(Foo&)
.
Выглядит нормально, но вам также могут понадобиться конструкторы копирования/перемещения и операторы присваивания копирования/перемещения, чтобы этот тип можно было использовать на практике («правило пяти»), например в std::vector<Foo>
. Использование std::variant
упрощает эту задачу.
Не следует использовать union
с нетривиальными типами.
Раньше объединение не занималось правильным созданием и уничтожением объектов C++ в нем. В C++11 эта проблема была в некоторой степени решена, требуя от вас предоставить правильный конструктор и деструктор, но вам все равно приходилось отслеживать текущий тип, хранящийся в нем.
Вы позаботились об этом вручную в своем коде, что технически правильно, но очень подвержено ошибкам. Вы можете легко попасть в землю UB, если построите или разрушите союз не того типа.
В целом в C++ рекомендуется использовать std::variant для общего типа суммы/дискриминируемого объединения (требуется #include <variant>
).
Из документации :
Шаблон класса std::variant представляет типобезопасное объединение.
(выделено мной)
std::variant
безопасно заботится о строительстве, разрушении и назначении (копировании или перемещении) находящихся в нем объектов.
Поэтому вместо вашего союза я рекомендую использовать что-то вроде:
std::variant<int, std::string> m_value;
Таким образом, ваш Foo
конструктор и деструктор могут быть default
изменены. Вариант позаботится о правильном построении и уничтожении std::string
(или любого другого нетривиального типа в нем).
«Объединение не предназначено для правильного создания и удаления объектов C++ в нем». - это было верно в старые времена, но было решено в C++11. Но да, сейчас предпочтительнее std::variant
@wohlstad У меня сложилось впечатление, что основным преимуществом «маркетинга» является безопасность типов. Но в cppref упоминается: «Все типы должны соответствовать требованиям Destructible (в частности, типы массивов и типы, не являющиеся объектами, не допускаются).», но у объединений есть маркер «начиная с C++11», связанный с конструкторами и деструкторы. Я думаю, именно это имел в виду Реми.
@TheNomad - исправил опечатку - спасибо.
@RemyLebeau, если я правильно понимаю, начиная с C++11, вам необходимо предоставить конструктор и деструктор для объединения, содержащего нетривиальные типы, но вам все равно придется отслеживать текущий тип «вручную» (в отличие от std::variant
). Обновил свой ответ.
Вам следует использовать
std::variant
, который является подходящим общим типом суммы для C++ (особенно с нетривиальными типами).