C++20 Dynamic Allocations at Compile-time
You may already have heard and seen that C++20 brings the ability to allocate dynamic memory at compile-time. This leads to
std::string being fully
constexpr in C++20. In this post, I like to give you a solid idea of where you can use that.
How does dynamic allocation at compile-time work
First, let's ensure that we all understand how dynamic allocations at compile-time work. In the early draft of the paper (P0784R1), proposed so-called non-transient allocations. They would have allowed us to allocate memory at compile-time and keep it to run-time. The previously allocated memory would then be promoted to static storage. However, various concerns did lead to allowing only transient allocations. That means what happens at compile-time stays at compile-time. Or in other words, the dynamic memory we allocate at compile-time must be deallocated at compile-time. This restriction makes a lot of the appealing use-cases impossible. I personally think that there are many examples out there that are of only little to no benefit.
The advantages of
I like to take a few sentences to explain what in my book are the advantages of
First, computation at compile-time does increase my local build-time. That is a pain, but it speeds up the application for my customers - a very valuable benefit. In the case where a
constexpr function is evaluated only at compile-time, I get a smaller binary footprint. That leads to more potential features in an application. I'm doing a lot of stuff in an embedded environment which is usually a bit more constrained than a PC application, so the size benefit does not apply to everyone.
constexpr functions, which are executed at compile-time, follow the perfect abstract machine. The benefit here is that the compiler tells me about undefined behavior in the compile-time path of a
constexpr function. It is important to understand that the compiler only inspects the path taken if the function is evaluated in a
constexpr context. Here is an example to illustrate what I mean.
1 2 3 4 5 6 7 8
This simple function
div is marked
div is used to initialize three variables. In A, the result of the call to
div is assigned to a
constexpr variable. This leads to
div being evaluated at compile time. The values are 4 and 2. The next two calls to
div divide four by zero. As we all know, only Chuck Norris can divide by zero. Now, B assigns the result to a non-
constexpr variable. Hence
div is executed at run-time. In this case, the compiler does not check for the division by zero despite that the
constexpr. This changes as soon as we assign the call to
div to a
constexpr variable as done in C. Because
div gets evaluated at compile-time now, and the error is on the
constexpr path, the compilation is terminated with an error like:
1 2 3 4 5 6 7 8 9 10 11
Catching such an error right away is, aside from not making it, the best thing that can happen.
Dynamic allocations at compile-time
As I stated initially, I think many examples of dynamic allocations at compile-time are with little real-world impact. A lot of the examples look like this:
1 2 3 4 5 6 7 8 9 10
Yes, I think there is a benefit to having
constexpr. But whether this requires a container with dynamic size or if a variadic template would have been the better choice is often unclear to me. I tend to pick the template solution in favor of reducing the memory allocations.
The main issue I see is that most often, the dynamically allocated memory must go out of the function. Because this is not possible, it boils down to either summing something up and return only that value or falling back to say
So, where do I think dynamic allocations at compile-time come in handy and are usable in real-world code?
A practical example of dynamic allocations at compile-time for every C++ developer
All right, huge promise in this heading, but I believe it is true.
Here is my example. Say we have an application that has a function
GetHome that returns the home directory of the current user. Another function
GetDocumentsDir which returns, as the name implies, the documents folder within the home directory of the user. In code, this can look like this:
1 2 3 4 5 6 7 8 9 10 11 12
Not rocket science, I know. The only hurdle is that the compiler figures out that
getenv is never
constexpr. For now, let's just use
std::is_constant_evaluated and return an empty string.
What both functions return is a
std::string. Now that we have a
std::string, we can make these two functions
constexpr as shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
The issue is that while it may look nice but due to the restriction of allocations at compile-time, the functions are unusable at compile-time. They both return a
std::string which contains the result we are interested in. But it must be freed before we leave compile-time. Yet, the user's home directory is a dynamic thing that is 100% run-time dependent. So absolutely no win here, right?
Well, yes. For your normal program, allocations at compile-time don't do anything good here. So time to shift our focus to the non-normal program part, which is testing. Because the dynamic home directory makes tests environment-dependent, we change
GetHome slightly to return a fixed home directory if
TEST is defined. The code then looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Say we like to write a basic test checking that the result matches our expectations. I use Catch2 here:
1 2 3 4
Still no use at compile-time of
GetHome. Why not? If we look closely, we now have everything in place. Due to the defined test environment,
GetHome no longer depends on
getenv. For our test case above, we are not really interested in having the string available at run-time. We mostly care about the result of the comparison in
How you approach this, is now a matter of taste. In my post C++20: A neat trick with consteval, I showed a solution with a
consteval function called
as_constant. If you like using
as_constant here, the test can look like this:
1 2 3 4
I probably would soon start defining something like
DCHECK for dual execution and encapsulate the
as_constant call there. This macro then executes the test at compile and run-time. That way, I ensure to get the best out of my test.
1 2 3 4 5 6 7 8
In an even better world, I would detect whether a function is evaluable at compile-time and then simply add this step of checking in
CHECK. However, the pitty here is that such a check must check whether the function is marked as
consteval but not execute it, because once such a function contains UB, the check would fail.
But let's step back. What happens here, and why does it work?
as_constant enforces a compile-time evaluation of what it gets called with. In our case, we create two temporary
std::strings, which are compared, and the result of this comparison is the parameter value of
as_constant. The interesting part here is that temporaries in a compile-time-context are compile-time. What we did is forcing the comparison of
GetDocumentsDir with the expected string to happen at compile-time. We then only promote the boolean value back into run-time.
The huge win you get with that approach is that in this test at compile-time, the compiler will warn you about undefined behavior,
- like an of-by-one error (which happened to me while I implemented my own constexpr string for the purpose of this post);
- memory leaks because not all memory gets deallocated;
- comparisons of pointers of different arrays;
- and more...
With the large RAM, we have today, memory leaks are hard to test for not so in a
constexpr context. As I said so often, the compiler is our friend. Maybe our best friend when it comes to programming.
Of course, there are other ways. You can make the same comparison as part of a
static_assert. The main difference I see is that the test will fail early, leading to a step-by-step failure discovery. Sometimes it is nicer to see all failing tests at once.
Another way is to assign the result of the comparison to a
constexpr variable that saves introducing
I hope you agree with my initial promise, the example I showed you is something that every programmer can adapt.
Sometimes it helps to think a bit out of the box. Even with the restrictions of compile-time allocations, there are ways where we can profit from the new abilities.
- Make functions that use dynamic memory
- Look at which data is already available statically.
- Check whether the result, like the comparison above, is enough, and the dynamic memory can happily be deallocated at compile-time.
Your advantages are:
- Use the same code for compile and run-time;
- Catch bugs for free with the compile-time evaluation;
- In more complex cases, the result can stay in the compile-time context because it is more like in the initial example with
- Overtime, maybe we get non-transient allocations. Then your code is already ready.
I hope you learned something today. If you have other techniques or feedback, please reach out to me on Twitter or via email.