Search code examples
c++unionsglm-math

Problem with union containing a glm::vec2 (non-trivial default constructor)


I have a the following struct:

struct Foo
{
    union
    {
        glm::vec2 size;
        struct { float width, height; };
    };

    Foo() = default;
};

If I create an instance of Foo with new, I get the following error:

call to implicitly-deleted default constructor of 'Foo'

Note that I've read a few answers on SO, all are talking about using placement-new, but I did not fully understand how to apply that to my simple test-case.


Solution

  • At most one non-static data member of a union may have a brace-or-equal-initializer. [Note: if any non-static data member of a union has a non-trivial default constructor (12.1), copy constructor (12.8), move constructor (12.8), copy assignment operator (12.8), move assignment operator (12.8), or destructor (12.4), the corresponding member function of the union must be user-provided or it will be implicitly deleted (8.4.3) for the union. — end note ]

    The relevant part is

    if any non-static data member of a union has a non-trivial [member function] [...], [then] the corresponding member function of the union must be user-provided or it will be implicitly deleted.

    So you will have to manually implement the constructor and destructor (and if you need them also the copy constructor, move constructor, copy assignment operator and move assignment operator).

    The compiler can't provide a default constructor because there's no way it could know which of the union members should be initialized by the default constructor, or which union member the destructor needs to destruct.

    So using the default constructor or trying to =default; it won't work, you'll have to provide user-specified implementations.


    A basic implementation could look like this:

    struct Foo
    {
        bool isVector;
        union
        {
            std::vector<float> vec;
            struct { float width, height; };
        };
    
        // by default we construct a vector in the union
        Foo() : vec(), isVector(true) {
    
        }
    
        void setStruct(float width, float height) {
            if(isVector) {
                vec.~vector();
                isVector = false;
            }
            this->width = width;
            this->height = height;        
        }
    
        void setVector(std::vector<float> vec) {
            if(!isVector) {
                new (&this->vec) std::vector<float>();
                isVector = true;
            }
    
            this->vec = vec;
        }
    
        ~Foo() {
            if(isVector) vec.~vector();
        }
    };
    

    godbolt example

    Note that you need to manually manage the lifetime of the union members, since the compiler won't do that for you.

    In case the active union member is the struct, but you want to activate the vector member, you need to use placement new to construct a new vector in the union.

    The same is true in the opposite direction: if you want to switch the active union member from the vector to the struct, you need to call the vector destructor first.

    We don't need to clean up the struct, since it's trivial, so no need to call it's constructor / destructor.


    If possible i would recommend using std::variant or boost::variant instead, since those do exactly that: they keep track of the active union member and call the required constructors / destructors as needed.