Simplifying templates and #ifdefs with if constexpr
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:
std::is_pointer_v<T> checks whether or not
T is a pointer type. Either the
else clause will be discarded at compile-time depending on how the condition evaluates. For example,
get_value<int> is essentially equivalent to
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:
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:
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.
Before we finish, a couple of notes about subtleties of this feature. The use of
if constexpr is not quite equivalent to
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
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.
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. ↩
Let me know what you think of this article on twitter @TartanLlama or leave a comment below!