There are two main techniques people use to delay creation of an object. I will show how they apply for a single object, but you can extend these techniques to a static or dynamic array.
Aligned Storage
The first approach is using std::aligned_storage
, which is a glorified char
array with alignment taken into consideration:
template<typename T>
class Uninitialized1 {
std::aligned_storage_t<sizeof(T)> _data;
public:
template<typename... Args>
void construct(Args... args) {
new (&_data) T(args...);
std::cout << "Data: " << *reinterpret_cast<T*>(&_data) << "\n";
}
};
Note that I have left out things like perfect forwarding to keep the main point.
One drawback of this is that there's no way to have a constexpr
constructor that takes a value to copy into the class. This makes it unsuitable for implementing std::optional
.
Unions
A different approach uses plain old unions. With aligned storage, you have to be careful, but with unions, you have to be doubly careful. Don't assume my code is bug-free as is.
template<typename T>
class Uninitialized2 {
union U {
char dummy;
T data;
U() {}
U(T t) : data(t) {
std::cout << "Constructor data: " << data << "\n";
}
} u;
public:
Uninitialized2() = default;
Uninitialized2(T t) : u(t) {}
template<typename... Args>
void construct(Args... args) {
new (&u.data) T(args...);
std::cout << "Data: " << u.data << "\n";
}
};
A union stores a strongly-typed object, but we put a dummy with trivial construction before it. This means that the default constructor of the union (and of the whole class) can be made trivial. However, we also have the option of initializing the second union member directly, even in a constexpr
-compatible way.
One very important thing I left out is that you need to manually destroy these objects. You will need to manually invoke destructors, and that should bother you, but it's necessary because the compiler can't guarantee the object is constructed in the first place. Please do yourself a favour and study up on these techniques in order to learn how to utilize them properly, as there are some pretty subtle details, and things like ensuring every object is properly destroyed can become tricky.
I (barely) tested these code snippets with a small class and driver:
struct C {
int _i;
public:
explicit C(int i) : _i(i) {
std::cout << "Constructing C with " << i << "\n";
}
operator int() const { return _i; }
};
int main() {
Uninitialized1<C> u1;
std::cout << "Made u1\n";
u1.construct(5);
std::cout << "\n";
Uninitialized2<C> u2;
std::cout << "Made u2\n";
u2.construct(6);
std::cout << "\n";
Uninitialized2<C> u3(C(7));
std::cout << "Made u3\n";
}
The output with Clang was as follows:
Made u1
Constructing C with 5
Data: 5
Made u2
Constructing C with 6
Data: 6
Constructing C with 7
Constructor data: 7
Made u3