Is it possible to implement a constexpr
static_vector
?
By static_vector
, I mean a vector which has its storage space inside the vector (not heap allocated) and it cannot grow beyond the compile-time CAPACITY
. Something like this:
template <typename T, int CAPACITY>
struct static_vector {
unsigned char data[sizeof(T)*CAPACITY];
int size = 0;
template <typename ...PARAMETERS>
constexpr void emplace_back(PARAMETERS &&...parameters) {
new(data + size * sizeof(T)) T(std::forward<PARAMETERS>(parameters)...);
size++;
}
constexpr const T &operator[](int idx) const {
return std::launder(reinterpret_cast<const T *>(data))[idx];
}
};
(hopefully this implementation is UB-free).
If I try to use this implementation, I get various constexpr-related errors. Here is a little test program (godbolt):
struct Foo {
int x;
constexpr Foo(int x) : x(x) { }
};
constexpr int fn() {
static_vector<Foo, 3> a;
a.emplace_back(1);
a.emplace_back(2);
return a[0].x + a[1].x;
}
int main() {
constexpr int v = fn();
printf("%d\n", v);
}
As far as I know, it is possible to do this if heap allocated storage is used. The question is, is it possible to do this using an "embedded" space, like static_vector
does? I know that this is not possible with older standards, but are we going to have something in the next standard which makes this possible?
(I also tried to implement this using a union
, but it fails in a different way)
Is it possible to implement a
constexpr
static_vector
?
As of what's in the language for C++26 so far, not completely no.
You need to provide storage for the backing array, and you need to be able to construct and retrieve objects from there. What are our options?
We could use an array of unsigned char:
alignas(T) unsigned char a1[sizeof(T[N])];
But this requires reinterpret_cast
to get an object back out, and we can't reinterpret_cast
at compile time.
We could directly construct an array:
T a2[N];
But this requires us to actually construct N
objects. You definitely do not want to do that if T
isn't trivially default constructible — because initially you want your vector to be empty. It doesn't matter here if we use T[N]
or std::array<T, N>
, although the latter just directly supports N == 0
.
We could stash our array in a union:
union { T a3[N]; };
The problem with a3
is that the only way to actually start the lifetime of a3
and use the array like an array is... to construct the whole array. Which has the same problem as a2
again. And if we don't start the lifetime of a3
then none of the array operations work.
Or we could try to do an array of a union:
union U { T val; };
U a4[N];
The problem with this is that you have APIs that return a T*
that actually has to be a pointer to T
that you can do pointer arithmetic on, not a T
inside of a union.
That's why the current design for std::inplace_vector<T, N>
contains this sentence:
For any
N>0
, ifis_trivial_v<T>
isfalse
, then noinplace_vector<T, N>
member functions are usable in constant expressions.
Because because if is_trivial_v<T>
is true then constructing a2
is fine (that's the direct T[N]
version) — no initialization happens, and now we have a proper array that all the compile time evaluation can deal with easily enough.
I have a proposal in flight that will hopefully land in C++26 that will allow the union
solution to work for all T
, by ensuring that the language starts the lifetime of a3
(the union { T[N] }
version) without having to initialize the whole array. That will make it possible to have a completely constexpr static_vector
, which is why that paper also strikes the sentence I cited earlier.