Simplifying templates and #ifdefs with if constexpr
Introduction
if constexpr
is a C++17 feature which allows conditionally compiling code based on template parameters in a clear and minimal fashion. It is essentially an if
statement where the branch is chosen at compile-time, and any not-taken branches are discarded without being instantiated. An example:
The condition std::is_pointer_v<T>
checks whether or not T
is a pointer type. Either the if
or else
clause will be discarded at compile-time depending on how the condition evaluates. For example, get_value<int>
is essentially equivalent to
and get_value<int*>
is essentially equivalent to
This post will show how to use if constexpr
to simplify your template code and replace horrible macro code.
Simplifying template code
The major win from if constexpr
is in writing code which is predicated on some trait of a template argument, without having to write verbose specializations. Consider our get_value
example above. Without if constexpr
, we would need to implement it using SFINAE or tag dispatching:
Our if constexpr
version is far more simple and understandable than the above:
Recursive templates are also much easier to specify1:
The benefits are particularly clear when the number of specialisations you’d otherwise need to write becomes larger. Say we want to add support for decomposition declarations to a class so that we can write auto [a,b,c,d] = get_my_class();
. For that, we need to write specialisations of get
for each index:
With constexpr if
, we can write this all in a single template function:
Not only is this a win in terms of lines of code, it also decreases syntactic noise and increases source code locality.
Replacing #ifdef blocks
Another great use-case of constexpr if
is to replace horrible #ifdef
blocks. Consider an application which needs to act in different ways depending on the operating system. Without if constexpr
you could write the code like this:
This kind of code could be splattered throughout your application2, making it look generally awful. With if constexpr
, you could write this instead:
This technique replaces the macros with real C++ constructs and lets us write all of our conditionally-compiled code without relying on the preprocessor, which is A Good Thing™. For one, if we misspell the enum name or something, we’ll get a descriptive compiler error rather than things just breaking. Additionally, the if constexpr
introduces a proper C++ scope, so any extra variables we declare in it will have their lifetimes limited to the OS-specific code.
One difference between the code with macros and the code with if constexpr
is that the code in the if constexpr
branches is parsed and has non-dependent names looked up, even when the condition fails (see the next section), so if you are using some OS-specific libraries in one of the blocks, then compilation will fail. This could be solved by providing declarations for the functions used, so you should consider which option is more maintainable for your use-case if the header files you include will be different depending on preprocessor definitions.
This technique could also be used for things like debugging or profiling code, by translating your relevant preprocessor definition to a constexpr bool
or similar.
Caveats
Before we finish, a couple of notes about subtleties of this feature. The use of constexpr
in if constexpr
is not quite equivalent to constexpr
functions. constexpr
functions can be executed at both compile-time and run-time, and this choice depends on the context in which they are called.
In the above code, the first call to max
is used as a template argument, so is executed at compile-time, whereas the second call cannot be known at compile-time, so is called at run-time.
The condition for an if constexpr
is always executed at compile-time. As such, you can only put constant expressions in the condition.
Note also that although any not-taken branches are discarded, they still need to be valid for some instantiation, otherwise the code is ill-formed. For example, you can’t write nonsense in a never-taken branch:
Nor can you put in a static_assert(false,...)
, just like with normal template specializations3:
One last point of interest is that you need to be sure to look after your else
blocks. In runtime code, it’s pretty common to write this:
The above pattern does not work with if constexpr
in some cases. Consider a modified version of our original example:
The above code will not compile, if T
is a pointer, because the second return statement will not be discarded, so there will be two return statements which return objects with different types. In such a case, you need to make sure you wrap that last statement in an else
block.
We’re done! If you want to try out constexpr if
, it is currently supported in Clang 3.9 and GCC 7. I think that this feature will clean up generic programming significantly and should make anyone decrying the lack of new C++17 features think twice.
-
Although you’d be better off using fold expressions instead. ↩
-
Of course, this specific problem can often be fixed by designing wrappers for the functionality which hides the macros, but for small differences or other ad-hoc things, this kind of macro code is used quite often. ↩
-
You can use the usual
dependent_false
workaround to solve this. ↩
Let me know what you think of this article on twitter @TartanLlama or leave a comment below!