c++structunionundefined-behavior

Using std::string and int inside the union


Is it correct to use std::string (or other non trivial types) with int (or other trivial and non trivial types) inside the same union?

I am already implemented it like this:

#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;
} 

and it works pretty good. But i am not sure about this code. Is it correct code from the C++ standard perspective?


Solution

  • You should not use a union with non trivial types.
    In the past a union did not deal with proper construction and destruction of C++ objects in it. In C++11 it was addressed to some extent by requiring you to supply a proper constructor and destructor but you still had to keep track of the current type held in it.

    You took care of it manually in your code, which is technically correct, but very much error prone. You can easily get into UB land if you construct or destruct the wrong type in the union.

    In general in C++ is it advised to use std::variant for a generic sum-type / discriminated union (requires #include <variant>).

    From the documentation:

    The class template std::variant represents a type-safe union.

    (emphasys is mine)

    std::variant safely takes care of construction, destruction and assignment (copy or move) of the objects in it.

    So instead of your union I recommend to use something like:

    std::variant<int, std::string> m_value;
    

    This way your Foo constructor and destructor can be defaulted. The variant will take care of proper construction and destruction of the std::string (or any other non-trivial type in it).