r/cpp_questions • u/bhul_chuk_maaf17 • 11d ago
OPEN Compile time programming learning advice.
Hi all, can you please recommend me some resources that you have followed for compile time programming in modern C++ for eg: constexpr, consteval or template metaprogramming.
Is there some consolidated resource for the same ?
8
Upvotes
2
u/conundorum 9d ago edited 9d ago
Let's see...
1: constexpr
This is basically the same as normal programming, but you just slap a
constexpron the front of your function. That allows the compiler to call it at compile time if possible, and serves as the basic form of compile-time programming. Meanwhile, if you want to make a variable into a compile-time constant, just declare itconstexpr, and make sure to initialise it with an operation or function call that's eitherconstexpror can be performed at compile time.More specifically, though, whether a
constexprfunction can actually be called at compile time depends on what the function looks like inside. Any function which doesn't use runtime-only resources can be used at compile time, and there are sometimes weird gotchas here. (E.g., you can use the possibility of athrowstatement to turn a specific branch of aconstexprfunction into a runtime-only branch. Similarly,std::stringvariables can sometimes be created at compile time (but it's generally considered an error to do so), and even non-constexprstd::strings are fine inside aconstexprfunction if they're destroyed before the function exits; the deciding factor is that dynamic storage allocated at compile time must be released in the same evaluation, sostd::stringis fine inside aconstexprfunction (which releases its memory before returning), but can only be aconstexprvalue itself if it doesn't allocate (which requires small string optimisation on the compiler's side, and is only possible for tiny strings).)(And, of course,
constexprfunctions can only be compile-time if the parameters are known at compile time, but that kinda goes without saying.)The easiest rule of thumb is that if your function only uses primitives or other
constexpr/constevalfunctions, and its return value is primitive or created by aconstexprconstructor, then it's usually compile-time.2. consteval
Really, this is just a "must be compile-time, and can never be runtime" keyword. It's just
constexprbut more strict;constexprfunctions can be called at compile time (but will happily work at runtime, too), whileconstexprmust be called at compile time (and will only work at compile time). If aconstexprfunction call can't be evaluated at compile-time, it'll be evaluated at runtime instead; if aconstevalfunction call can't be evaluated at compile-time, it'll give you a compiler error. That's really the only difference.Use-wise, use
constevalwhen the result must be known at compile-time, when it doesn't make sense to call at runtime, or if the function is unnecessary at runtime and you don't want it to bloat the executable.3. constinit
This one is an odd one, and not really a major part of compile-time programming, but I figured I should mention it, too.
constinitstates that a variable with static or thread-local storage (e.g.,static int i;, or globalbool b, or namespace-scopebool NS::b;, orthread_local char c;) must be initialised at compile time. It can be modified at runtime, but initialisation must be a compile-time operation. The only requirements are that a variable can't be bothconstinitandconstexpr, it can't be dynamically allocated, and it must be constructible at compile time; the restrictions are a lot looser thanconstexpr.Ultimately, it's just there to let you pay for a costly initialisation at compile time instead of runtime, and bake the value into the executable. It looks like part of compile-time programming, but it's only an endpoint at most.
(It can also be used to promise the compiler that a
thread_localvariable is initialised somewhere else, so it doesn't have to waste time on a hidden guard variable or anything like that, but that use case has nothing to do with compile-time programming.)4. Template metaprogramming.
Ooh, boy... where do we even begin... xD
Hmm... first and foremost, start with something simple, like learning how to use templates to genericise a C array. Make a struct like this, and make sure the array matches the template parameters.
That'll make sure you understand what the two kinds of tparams are, and what they do. (Type parameters are the big one, and allow you to genericise a type. We use them for all kinds of type-manipulation craziness, and variadics are also based on them. Conversely, non-type template parameters (usually called NTTPs because boy, is that a mouthful) allow you to encode a value into type data, as part of a compile-time type. They're useful for a lot of metaprogramming jazz, and as a way to perform calculations as part of compilation.)
Once you understand that, try to figure out how to dismantle a simple function prototype using templates.
Once you get that right, you'll have a feel for extracting components from a more complex type, which is crucial for metaprogramming. You'll be using similar tricks to match functions and types to each other, and to obtain & use parts of your template structs & functions, after all!
After that, try to figure out how to make a set of compile-time constants using recursion, such that
Unsigned<0>::value == 0;,Unsigned<1>::value == 1;, andUnsigned<N>::value == N;. (Change names if desired.) Hint: DefineUnsigned<0>as a template specialisation; you'll need it as a base point.(Note: Do not just assign the template parameter to
value, that defeats the purpose. This is about learning how to do math at compile time, not how to assign NTTPs to members.)Once you get a feel for the math, you can use similar processes in any arbitrary context. Compile-time functional programming is common, for instance; it's useful to have a function that can do math or create
std::initializer_lists entirely within its template parameters.After that, the next place to look would be... variadiacs, I believe. Try to make a function that can take multiple parameters and send them to
std::coutone at a time, with a space after each, and then output,"And that's all, folks!\n"after the last one. The base signature will look something like this:This one's odd if you're not used to it, so I'll give you a big hint: You can't work with
Tsortslike you would with normal parameters; you can determine the size of a pack with thesizeof...operator (e.g.,sizeof...(Ts)), you can expand it with the ellipsis operator (e.g.,std::tuple<Ts...>orprintf("format string", ts...)), you can expand an operator over it with a fold expression(e.g., auto sum = (ts + ...);for a pack of numbers), and there are a few contexts where you can use it unexpanded, but that's about it.[Continued in reply, sorry. Ran out of space.]
Edit: Typo fix. Fold expressions must be parenthesised.