Search code examples
c++unions

How does C++ union know the type stored in it and which destructor to call?


Working with a union of two classes, it appears in this simple example that the union remembers the last class stored in it and calls the correct destructor for that object:

#include <iostream>

using std::cout;
using std::endl;

struct yes{
    yes(){cout<<"yes-c"<<endl;}
    ~yes(){cout<<"yes-d"<<endl;}
};
struct no{
    no(){cout<<"no-c"<<endl;}
    ~no(){cout<<"no-d"<<endl;}
};
struct u{
    union{
        yes y;
        no n;
    };
    u(yes _y):y(_y){}
    u(no _n):n(_n){}        
    ~u(){}
};


int main() {
    yes y;
    no n;
    {
    u uu(n);
    }

    return 0;
}

Output:

yes-c
no-c
no-d
no-d
yes-d

So the uu will call the correct destructor ~no() for the union, as if it records the type when the union is constructed. How does this work?


Solution

  • Short answer: It doesn't.

    If you add a copy-constructor to no you will see that there are actually three no objects being created, but only two are destructed.

    First you create the object n. Then when you pass it by value to the u constructor, it is copied once into the _n argument. That _n object is then copied into the uu.n member.

    The destructions are of the _n argument in the u constructor, and the n object in the main function.


    Here's your program with some slight modification to add the copy-constructor and to keep track of the no objects:

    #include <iostream>
    
    struct yes{
        yes(){std::cout<<"yes-c"<<std::endl;}
        ~yes(){std::cout<<"yes-d"<<std::endl;}
    };
    struct no{
        no(){std::cout<<"no-c : "<<n<<std::endl;}
        no(no const& o)
            : n(o.n + 1)
        {
            std::cout << "no-cc : " << o.n << " -> " << n << '\n';
        }
    
        ~no(){std::cout<<"no-d : "<<n<<std::endl;}
        int n = 0;
    };
    struct u{
        union{
            yes y;
            no n;
        };
        u(yes _y):y(_y){}
        u(no _n):n(_n){}
        ~u(){}
    };
    
    int main()
    {
        yes y;
        no n;
        {
            u uu(n);
        }
    }
    

    Without optimizations or copy-elision this will create the output

    yes-c
    no-c : 0
    no-cc : 0 -> 1
    no-cc : 1 -> 2
    no-d : 1
    no-d : 0
    yes-d
    

    The output no-c : 0 is for the creation of the n object in the main function.

    The output no-cc : 0 -> 1 is for the copying into the u constructor argument _n.

    The output no-cc : 1 -> 2 is for the copying of the argument _n into the unions n object.

    The output no-d : 1 is the destruction of the _n argument.

    The output no-d : 0 is the destruction of the n object in the main function.