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
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.