Search code examples
c++std-variant

How to initialize member std::variant variable based on the input of the constructor


I have a class BASE which contains a member variable of type std::variant which can contain an object of type A or B. Based on the input to the constructor at runtime, I want to initialize this variable to contain either A or B (calling their constructors with an input).

Something along these lines:

#include <iostream>
#include <string>
#include <variant>

class A {
private:
    char* arr;
public:
    A() = default;
    A(int n) {
        arr = new char[3];
        arr[0] = arr[1] = arr[2] = 'A';
        std::cout << "Constructor A called with " + std::to_string(n);
        std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; 
    }
    ~A() {
        std::cout << "Destructor A called";
        std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; 
        delete[] arr;
    }
};
class B {
private:
    char* arr;
public:
    B() = default;
    B(int n) {
        arr = new char[3];
        arr[0] = arr[1] = arr[2] = 'B';
        std::cout << "Constructor B called with " + std::to_string(n);
        std::cout << " [" << arr[0] << "," << arr[1] << "," << arr[2] << "]" << std::endl; 
    }
    ~B() {
        std::cout << "Destructor B called ";
        std::cout << " [" << arr[0]  << "," << arr[1] << "," << arr[2] << "]" << std::endl; 
        delete[] arr;
    }
};


class BASE {
private: 
     std::variant<A, B> myVar;

public:
     BASE() = delete;
     BASE(int i) {
         if (i == 0) {
             std::cout << "Initializing A" << std::endl;
             myVar = A(i);
         } else {
             std::cout << "Initializing B" << std::endl;
             myVar = B(i);
         }
     }
};

int main() {
    BASE test1(0);
}

This however does not work, since right after the assignment to myVar, the destructor of A (or B) is called and myVar contains garbage. See: https://onlinegdb.com/t3oK456_w

I thought I could initialize it in a list at the constructor, something like this:

BASE(int i) :
    myVar(i == 0 ? A("input.txt") : B("input.txt"))
{
}

but that (of course) does not work neither.

I appreciate any hint how to do it properly (ideally in C++17). Thanks


Solution

  • Look carefully at the output of your program (https://www.onlinegdb.com/KRvUQU7vm):

    Initializing A
    Constructor A called with 0
    Destructor A called
    Initializing B
    Constructor B called with 1
    Destructor A called
    Destructor B called
    Destructor B called
    Destructor A called

    Do you see that after "Constructor B called with 1" there's a "Destructor A called"? Where does it come from?

    The answer is that your class BASE contains std::variant<A, B> myVar;, and the constructor of BASE must initialize it before the body of the constuctor is run. To do it, it chooses the first type of the variant, that is, A, which is default constructed. If you modify the default constructors to

    A() {
        std::cout << "Default constructor of A\n";
    }
    

    and likewise for B, you'll see that when you create the 2 BASE objects (test1 and test2) there's a call to the default constructor of A before it even prints "Initializing A".

    Change the variant to std::variant<B, A> myVar; (switching the order of the types) and you'll see two calls to B's default constructor. But myVar does not contain garbage!

    In your other example (https://onlinegdb.com/t3oK456_w), which uses new[] and delete[], there's a double-free error because the default constructor does not call new[], but the destructor still calls delete[]. And that's why the result is garbage.

    So the way you use the constructor of BASE to decide which type to use in the variant is correct. The problem is that there's a "hidden" call to the default constructor of the first type (A), which must behave correctly.