Использование std::string и int внутри объединения

Правильно ли использовать 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++?

Вам следует использовать std::variant, который является подходящим общим типом суммы для C++ (особенно с нетривиальными типами).

wohlstad 09.05.2024 16:31

@wohlstad спасибо за предложение, но в любом случае... я хочу знать, можно ли использовать этот код на C++ или существует какое-то неопределенное поведение?

Samuel Smith 09.05.2024 16:33

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

Marek R 09.05.2024 16:35

Это основная реализация std::variant. Почему вы хотите изобрести велосипед? Вы столкнетесь с утомительными неудачами и множеством ловушек, касающихся жизненных проблем. Оберните variant в свой класс и предоставьте аксессор /*value_type*/* get<Type>(Foo&).

Red.Wave 09.05.2024 16:47

Выглядит нормально, но вам также могут понадобиться конструкторы копирования/перемещения и операторы присваивания копирования/перемещения, чтобы этот тип можно было использовать на практике («правило пяти»), например в std::vector<Foo>. Использование std::variant упрощает эту задачу.

chi 09.05.2024 17:27
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
97
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Не следует использовать 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

Remy Lebeau 09.05.2024 16:47

@wohlstad У меня сложилось впечатление, что основным преимуществом «маркетинга» является безопасность типов. Но в cppref упоминается: «Все типы должны соответствовать требованиям Destructible (в частности, типы массивов и типы, не являющиеся объектами, не допускаются).», но у объединений есть маркер «начиная с C++11», связанный с конструкторами и деструкторы. Я думаю, именно это имел в виду Реми.

TheNomad 09.05.2024 17:00

@TheNomad - исправил опечатку - спасибо.

wohlstad 09.05.2024 17:03

@RemyLebeau, если я правильно понимаю, начиная с C++11, вам необходимо предоставить конструктор и деструктор для объединения, содержащего нетривиальные типы, но вам все равно придется отслеживать текущий тип «вручную» (в отличие от std::variant). Обновил свой ответ.

wohlstad 09.05.2024 17:09

Другие вопросы по теме