This is another experiment in the “LLM as Tutor” series, where we use GPT to teach us about Unions and Variants in Modern C++ (specifically, all the code here is C++17).
The text (and, crucially, the code) has been lightly edited and verified for correctness.

Unions
In C++, a union is a special data structure where all members share the same memory space, allowing only one member to be active at a time. This is useful for scenarios where you need to store different types in the same memory region, but never simultaneously.
Here’s how to define and use a union in C++ and combine it with “placement new” for flexible memory management.
Using union with Placement new
When using union with classes that have constructors or destructors, C++ requires you to manage the construction and destruction manually, which is where placement new comes in.
Consider this example with two classes, MyObject and AnotherObject, which are stored within a union.
Example Code
#include <iostream>
#include <cstdlib> // for malloc and free
class MyObject {
public:
MyObject(int x) : x_(x) { std::cout << "Constructing MyObject with x=" << x_ << '\n'; }
~MyObject() { std::cout << "Destroying MyObject\n"; }
void show() const { std::cout << "MyObject: x=" << x_ << '\n'; }
private:
int x_;
};
class AnotherObject {
public:
AnotherObject(double y) : y_(y) { std::cout << "Constructing AnotherObject with y=" << y_ << '\n'; }
~AnotherObject() { std::cout << "Destroying AnotherObject\n"; }
void show() const { std::cout << "AnotherObject: y=" << y_ << '\n'; }
private:
double y_;
};
// Union with non-trivial members
union MyUnion {
MyObject myObj;
AnotherObject anotherObj;
// Constructor and Destructor
MyUnion() {} // Default constructor does nothing
~MyUnion() {} // Destructor does nothing; we call destructors manually
};
int main() {
MyUnion u;
// Use placement new to construct MyObject in the union
new (&u.myObj) MyObject(42);
u.myObj.show();
// Explicitly call destructor for MyObject
u.myObj.~MyObject();
// Use placement new to construct AnotherObject in the same memory
new (&u.anotherObj) AnotherObject(3.14);
u.anotherObj.show();
// Explicitly call destructor for AnotherObject
u.anotherObj.~AnotherObject();
return 0;
}
Running this code yields:
$ /Users/marco/Development/Playgrounds/samples/build/Debug/unions
Constructing MyObject with x=0
Constructing MyObject with x=42
MyObject: x=42
Destroying MyObject
Constructing AnotherObject with y=3.14
AnotherObject: y=3.14
Destroying AnotherObject
Process finished with exit code 0
Explanation
- Union Declaration: MyUnion contains two types, MyObject and AnotherObject, but only one can be active at any time.
- Manual Construction: Since union does not automatically call constructors or destructors, we use placement new to construct the active member explicitly.
- Explicit Destruction: Call the destructor of the active member manually before constructing another member in the same memory location.
General Guidelines for Using union in C++
- Plain Old Data (POD): Use union with simple data types (e.g., int, float) without explicit constructors/destructors when you want low-level control.
- Non-Trivial Members: When using classes with constructors/destructors, you must manually handle their construction and destruction.
- Placement new for Complex Types: Use placement new to control construction in the union’s memory space when necessary.
References
Placement New
In C++, “placement new” is a variant of the new operator that allows you to construct an object in a pre-allocated memory buffer, rather than allocating memory for it on the heap. This is useful when you want more control over where an object is created, such as in memory-mapped files, shared memory, or custom memory pools.
Syntax
void* buffer = std::malloc(sizeof(MyClass)); // allocate raw memory
MyClass* obj = new (buffer) MyClass(arg1, arg2); // placement new
The syntax new(buffer) MyClass(arg1, arg2); constructs a MyClass object at the address pointed to by buffer. Here, arg1 and arg2 are arguments to the constructor of MyClass.
Key Points
- No Memory Allocation: Placement new does not allocate memory; it only calls the constructor.
- Manual Deallocation: Since placement new does not manage memory, you must manually free the memory using std::free or by other means.
- Explicit Destruction: You must explicitly call the destructor when done, since delete does not work with placement new.
Example
#include <iostream>
#include <cstdlib> // for malloc and free
class MyClass {
public:
explicit MyClass(int x) : x_(x) { std::cout << "Constructing MyClass\n"; }
~MyClass() { std::cout << "Destroying MyClass\n"; }
private:
int x_;
};
int main() {
// Allocate memory without initializing the object
void* buffer = std::malloc(sizeof(MyClass));
// Construct the object in the allocated memory
MyClass* obj = new (buffer) MyClass(10);
// Use the object
// ...
// Call destructor explicitly
obj->~MyClass();
// Free the allocated memory
std::free(buffer);
return 0;
}
// == Output:
// Constructing MyClass
// Destroying MyClass
Use Cases
- Memory Pools: Efficiently manage a fixed memory block by constructing objects within it.
- Custom Allocators: Implement custom memory management strategies.
- Embedded Systems: Control memory layout in resource-constrained environments.
References
For more on placement new, refer to the C++ Standard Library documentation on new expressions.
Variants
In C++17 and beyond, std::variant generally offers a safer and more feature-rich alternative to union. However, there are still cases where union can be preferable. Here’s a comparison to help understand when to choose one over the other:
When to Use std::variant
std::variant is essentially a safer and more expressive alternative to union. It provides additional functionality and solves some of the issues that come with union.
Advantages of std::variant
- Type Safety:
std::variantkeeps track of which type is currently active, which reduces the risk of accessing an uninitialized or inactive member. - Automatic Management:
std::variantautomatically handles construction, destruction, and assignment of the active type, so you don’t need to worry about manually managing these steps. - Access Methods:
std::variantprovides thestd::get,std::holds_alternative, andstd::visitfunctions, which make it easy to retrieve the active value or apply different operations based on the active type. - Extensible and Expressive: With
std::variant, adding or removing types is straightforward. It also supports move semantics, so you can use it efficiently with non-trivial types.
Example
#include <iostream>
#include <variant>
class MyObject {
public:
MyObject(int x) : x_(x) { std::cout << "Constructing MyObject with x=" << x_ << '\n'; }
~MyObject() { std::cout << "Destroying MyObject\n"; }
void show() const { std::cout << "MyObject: x=" << x_ << '\n'; }
private:
int x_;
};
class AnotherObject {
public:
AnotherObject(double y) : y_(y) { std::cout << "Constructing AnotherObject with y=" << y_ << '\n'; }
~AnotherObject() { std::cout << "Destroying AnotherObject\n"; }
void show() const { std::cout << "AnotherObject: y=" << y_ << '\n'; }
private:
double y_;
};
int main() {
std::variant<MyObject, AnotherObject> v{MyObject(42)};
std::get<MyObject>(v).show();
v = AnotherObject(3.14);
std::get<AnotherObject>(v).show();
return 0;
}
When to Use union
There are still some specific cases where union may be preferred, even in C++17 and later:
- Memory Constraints:
std::variantrequires additional memory for tracking the active type, while union only needs enough memory for the largest member. If memory usage is a critical concern, such as in embedded or real-time systems, union may be better. - Performance Optimization:
std::variantincludes extra logic for type management, which might add some overhead compared to a simple union. In performance-critical, low-level code, a union may provide slightly better efficiency, though this difference is typically minimal. - Interfacing with C APIs: If you’re working with C libraries or hardware interfaces that require unions, using union directly can simplify the code and avoid conversion overhead.
- Simplicity for POD Types: If you’re working only with simple POD (Plain Old Data) types where no construction/destruction logic is needed, union can be simpler and more transparent than std::variant.
Summary
For most general-purpose cases, especially with complex or non-trivial types, std::variant is preferred due to its type safety, expressiveness, and automatic management. However, union may still be beneficial when strict control over memory layout and performance is essential.






Leave a comment