You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Before C++17, we had a few quite ugly-looking ways to write static if (if that works at compile time). For example, you could use tag dispatching or SFINAE. Fortunately, that’s changed, and we can now benefit from if constexpr and concepts from C++20!
Let’s see how we can use it and replace some std::enable_if code.
Updated in April 2021: C++20 changes - concepts.
Updated in August 2022: More if constexpr examples (use case 4).
Intro
Compile-time if in the form of if constexpr is a fantastic feature that went into C++17. With this functionality, we can improve the readability of some heavily templated code.
Additionally, with C++20, we got Concepts! This is another step to having almost “natural” compile-time code.
This blog post was inspired by an article @Meeting C++ with a similar title. I’ve found four additional examples that can illustrate this new feature:
Number comparisons
(New!) Computing average on a container
Factories with a variable number of arguments
Examples of some actual production code
But to start, I’d like to recall the basic knowledge about enable_if to set some background.
Why compile-time if?
Let’s start with an example that tries to convert an input into a string:
As you can see, there are three function overloads for concrete types and one function template for all other types that should support to_string(). This seems to work, but can we convert that into a single function?
In instantiation of 'std::__cxx11::string str(T) [with T =
std::__cxx11::basic_string<char>; std::__cxx11::string =
std::__cxx11::basic_string<char>]':
required from here
error: no matching function for call to
'to_string(std::__cxx11::basic_string<char>&)'
return std::to_string(t);
is_convertible yields true for the type we used (std::string), and we can just return t without any conversion… so what’s wrong?
Here’s the main point:
The compiler compiled all branches and found an error in the else case. It couldn’t reject the “invalid” code for this particular template instantiation.
That’s why we need static if that would “discard” code and compile only the matching statement. To be precise, we’d like to have a syntax check for the whole code, but some parts of the routine would not be instantiated.
std::enable_if
One way to write static if in C++11/14 is to use enable_if.
enable_if (and enable_if_v since C++14). It has quite a strange syntax:
template<boolB,classT=void>structenable_if;
enable_if will evaluate to T if the input condition B is true. Otherwise, it’s SFINAE, and a particular function overload is removed from the overload set. This means that on false the compiler “rejects” the code - this is precisely what we need.
Not easy… right? Additionally, this version looks far more complicated than the separate functions and regular function overloading we had at the start.
That’s why we need if constexpr from C++17 that can help in such cases.
After you read the post, you’ll be able to rewrite our str utility quickly (or find the solution at the end of this post).
To understand the new feature, let’s start with some basic cases:
Use Case 1 - Comparing Numbers
First, let’s start with a simple example: close_enough function that works on two numbers. If the numbers are not floating points (like when we have two ints), we can compare them directly. Otherwise, for floating points, it’s better to use some abs < epsilon checks.
I’ve found this sample from at Practical Modern C++ Teaser - a fantastic walkthrough of modern C++ features by Patrice Roy. He was also very kind and allowed me to include this example.
As you see, there’s a use of enable_if. It’s very similar to our str function. The code tests if the type of input numbers is is_floating_point. Then, the compiler can remove one function from the overload resolution set.
Wow… so just one function that looks almost like a normal function.
With nearly “normal” if :)
if constexpr evaluates constexpr expression at compile time and then discards the code in one of the branches.
But it’s essential to observe that the discarded code has to have the correct syntax. The compiler will do the basic syntax scan, but then it will skip this part of the function in the template instantiation phase.
That’s why the following code generates a compiler error:
But wait… it’s 2021, so why not add some concepts? :)
Up to C++20, we could consider template parameters to be something like a void* in a regular function. If you wanted to restrict such a parameter, you had to use various techniques discussed in this article. But with Concepts, we get a natural way to restrict those parameters.
As you can see, the C++20 version switched to two functions. Now, the code is much more readable than with enable_if. With concepts, we can easily write our requirements for the template parameters:
requiresstd::is_floating_point_v<T>
is_floating_point_v is a type-trait (available in <type_traits> library) and as you can see the requires clause evaluates boolean constant expressions.
The second function uses a new generalized function syntax, where we can omit the template<> section and write:
constexprboolclose_enough20(autoa,autob){}
Such syntax comes from generic lambdas. This is not a direct translation of our C++11/14 code as it corresponds to the following signature:
The makeInvestment interface is unrealistic because it implies that all derived object types can be created from the same types of arguments. This is especially apparent in the sample implementation code, where our arguments are perfect-forwarded to all derived class constructors.
For example if you had a constructor that needed two arguments and one constructor with three arguments, then the code might not compile:
Now, if you write make(bond, 1, 2, 3) - then the else statement won’t compile - as there no Stock(1, 2, 3) available! To work, we need something like static if that will work at compile-time and reject parts of the code that don’t match a condition.
Some posts ago, with the help of one reader, we came up with a working solution (you can read more in Nice C++ Factory Implementation 2).
As you can see, the “magic” happens inside constructArgs function.
The main idea is to return unique_ptr<Type> when Type is constructible from a given set of attributes and nullptr when it’s not.
Before C++17
In my previous solution (pre C++17) we used std::enable_if and it looked like that:
// before C++17
template<typenameConcrete,typename...Ts>enable_if_t<is_constructible<Concrete,Ts...>::value,unique_ptr<Concrete>>constructArgsOld(Ts&&...params){returnstd::make_unique<Concrete>(forward<Ts>(params)...);}
All the complicated syntax of enable_if went away; we don’t even need a function overload for the else case. We can now wrap expressive code in just one function.
if constexpr evaluates the condition, and only one block will be compiled. In our case, if a type is constructible from a given set of attributes, then we’ll compile the make_unique call. If not, then nullptr is returned (and make_unique is not even instantiated).
Before C++17 benefits, the code looked as follows:
template<class_Ret,class_Pair>constexpr_Ret_Pair_get(_Pair&_Pr,integral_constant<size_t,0>)noexcept{// get reference to element 0 in pair _Pr
return_Pr.first;}
template<class_Ret,class_Pair> constexpr_Ret_Pair_get(_Pair&_Pr,integral_constant<size_t,1>)noexcept{ // get reference to element 1 in pair _Pr return_Pr.second; }
template<size_t_Idx,class_Ty1,class_Ty2> _NODISCARDconstexprtuple_element_t<_Idx,pair<_Ty1,_Ty2>>& get(pair<_Ty1,_Ty2>&_Pr)noexcept{ // get reference to element at _Idx in pair _Pr using_Rtype=tuple_element_t<_Idx,pair<_Ty1,_Ty2>>&; return_Pair_get<_Rtype>(_Pr,integral_constant<size_t,_Idx>{}); }
And after the change, we have:
template<size_t_Idx,class_Ty1,class_Ty2>_NODISCARDconstexprtuple_element_t<_Idx,pair<_Ty1,_Ty2>>&get(pair<_Ty1,_Ty2>&_Pr)noexcept{// get reference to element at _Idx in pair _Pr
ifconstexpr(_Idx==0){return_Pr.first;}else{return_Pr.second;}}
It’s only a single function and much easier to read! No need for tag dispatch with the integral_constant helper.
In the other library, this time related to SIMD types and computations (popular implementation by Agner Fog ), you can find lots of instances for if constexpr:
// zero_mask: return a compact bit mask mask for zeroing using AVX512 mask.
// Parameter a is a reference to a constexpr int array of permutation indexes
template<intN>constexprautozero_mask(intconst(&a)[N]){uint64_tmask=0;inti=0;
Without if constexpr the code would be much longer and potentially duplicated.
Wrap up
Compile-time if is an amazing feature that significantly simplifies templated code. What’s more, it’s much more expressive and nicer than previous solutions: tag dispatching or enable_if (SFINAE). Now, you can easily express your intentions similarly to the “run-time” code.
We also revised this code and examples to work with C++20! As you can see, thanks to concepts, the code is even more readable, and you can “naturally” express requirements for your types. You also gain a few syntax shortcuts and several ways to communicate such restrictions.
In this article, we’ve touched only basic expressions, and as always, I encourage you to play more with this new feature and explore.
Going back…
And going back to our str example:
Can you now rewrite the str function (from the start of this article) using if constexpr? :) Try and have a look at my simple solution @CE.
Simplify Code with if constexpr and Concepts in C++17/C++20
https://ift.tt/Ap38Ng7
Before C++17, we had a few quite ugly-looking ways to write
static if
(if
that works at compile time). For example, you could use tag dispatching or SFINAE. Fortunately, that’s changed, and we can now benefit fromif constexpr
and concepts from C++20!Let’s see how we can use it and replace some
std::enable_if
code.if constexpr
examples (use case 4).Intro
Compile-time if in the form of
if constexpr
is a fantastic feature that went into C++17. With this functionality, we can improve the readability of some heavily templated code.Additionally, with C++20, we got Concepts! This is another step to having almost “natural” compile-time code.
This blog post was inspired by an article @Meeting C++ with a similar title. I’ve found four additional examples that can illustrate this new feature:
But to start, I’d like to recall the basic knowledge about
enable_if
to set some background.Why compile-time if?
Let’s start with an example that tries to convert an input into a string:
Run at Compiler Explorer.
As you can see, there are three function overloads for concrete types and one function template for all other types that should support
to_string()
. This seems to work, but can we convert that into a single function?Can the “normal”
if
just work?Here’s a test code:
It sounds simple… but try to compile this code:
You might get something like this:
is_convertible
yieldstrue
for the type we used (std::string
), and we can just returnt
without any conversion… so what’s wrong?Here’s the main point:
The compiler compiled all branches and found an error in the
else
case. It couldn’t reject the “invalid” code for this particular template instantiation.That’s why we need static if that would “discard” code and compile only the matching statement. To be precise, we’d like to have a syntax check for the whole code, but some parts of the routine would not be instantiated.
std::enable_if
One way to write static if in C++11/14 is to use
enable_if
.enable_if
(andenable_if_v
since C++14). It has quite a strange syntax:enable_if
will evaluate toT
if the input conditionB
is true. Otherwise, it’s SFINAE, and a particular function overload is removed from the overload set. This means that onfalse
the compiler “rejects” the code - this is precisely what we need.We can rewrite our basic example to:
Not easy… right? Additionally, this version looks far more complicated than the separate functions and regular function overloading we had at the start.
That’s why we need
if constexpr
from C++17 that can help in such cases.After you read the post, you’ll be able to rewrite our
str
utility quickly (or find the solution at the end of this post).To understand the new feature, let’s start with some basic cases:
Use Case 1 - Comparing Numbers
First, let’s start with a simple example:
close_enough
function that works on two numbers. If the numbers are not floating points (like when we have twoints
), we can compare them directly. Otherwise, for floating points, it’s better to use someabs < epsilon
checks.I’ve found this sample from at Practical Modern C++ Teaser - a fantastic walkthrough of modern C++ features by Patrice Roy. He was also very kind and allowed me to include this example.
C++11/14 version:
As you see, there’s a use of
enable_if
. It’s very similar to ourstr
function. The code tests if the type of input numbers isis_floating_point
. Then, the compiler can remove one function from the overload resolution set.And now, let’s look at the C++17 version:
Wow… so just one function that looks almost like a normal function.
With nearly “normal” if :)
if constexpr
evaluatesconstexpr
expression at compile time and then discards the code in one of the branches.But it’s essential to observe that the discarded code has to have the correct syntax. The compiler will do the basic syntax scan, but then it will skip this part of the function in the template instantiation phase.
That’s why the following code generates a compiler error:
Checkpoint: Can you see some other C++17 features that were used here?
You can play with the code @Compiler Explorer
Adding Concepts in C++20
But wait… it’s 2021, so why not add some concepts? :)
Up to C++20, we could consider template parameters to be something like a
void*
in a regular function. If you wanted to restrict such a parameter, you had to use various techniques discussed in this article. But with Concepts, we get a natural way to restrict those parameters.Have a look:
As you can see, the C++20 version switched to two functions. Now, the code is much more readable than with
enable_if
. With concepts, we can easily write our requirements for the template parameters:is_floating_point_v
is a type-trait (available in<type_traits>
library) and as you can see therequires
clause evaluates boolean constant expressions.The second function uses a new generalized function syntax, where we can omit the
template<>
section and write:Such syntax comes from generic lambdas. This is not a direct translation of our C++11/14 code as it corresponds to the following signature:
Additionally, C++20 offers a terse syntax for concepts thanks to constrained auto:
Alternatively, we can also put the name of the concept instead of a
typename
and without therequires
clause:In this case, we also switched from
is_floating_point_v
into a conceptfloating_point
defined in the<concepts>
header.See the code here: @Compiler Explorer
Ok, how about another use case?
Use case 2 - computing the average
Let’s stay in some “numeric” area, and now we’d like to write a function that takes a vector of numbers and returns an average.
Here’s a basic use case:
Out function has to:
double
.In C++20, we can use ranges for such purposes, but let’s treat this function as our playground and test case to learn.
Here’s a possible version with Concepts:
For the implementation, we need to restrict the template parameter to be integral or floating-point.
We don’t have a predefined concept that combines floating point and integral types, so we can try writing our own:
And use it:
Or we can also make it super short:
We can also rewrite it with C++14 enable_if
See the working code @Compiler Explorer
Use case 3 - a factory with variable arguments
In the item 18 of Effective Modern C++ Scott Meyers described a function called
makeInvestment
:This is a factory method that creates derived classes of
Investment
and the main advantage is that it supports a variable number of arguments!For example, here are the proposed types:
The code from the book was too idealistic and didn’t work - it worked until all your classes have the same number and types of input parameters:
Scott Meyers: Modification History and Errata List for Effective Modern C++:
For example if you had a constructor that needed two arguments and one constructor with three arguments, then the code might not compile:
Now, if you write
make(bond, 1, 2, 3)
- then theelse
statement won’t compile - as there noStock(1, 2, 3)
available! To work, we need something like static if that will work at compile-time and reject parts of the code that don’t match a condition.Some posts ago, with the help of one reader, we came up with a working solution (you can read more in Nice C++ Factory Implementation 2).
Here’s the code that could work:
As you can see, the “magic” happens inside
constructArgs
function.The main idea is to return
unique_ptr<Type>
when Type is constructible from a given set of attributes andnullptr
when it’s not.Before C++17
In my previous solution (pre C++17) we used
std::enable_if
and it looked like that:std::is_constructible
see cppreference.com - allows us to quickly test if a list of arguments could be used to create a given type.In C++17 there’s a helper:
So we could make the code shorter a bit…
Still, using
enable_if
looks ugly and complicated. How about a C++17 version?With
if constexpr
Here’s the updated version:
Super short!
We can even extend it with a little logging features, using fold expression:
Cool… right? :)
All the complicated syntax of
enable_if
went away; we don’t even need a function overload for theelse
case. We can now wrap expressive code in just one function.if constexpr
evaluates the condition, and only one block will be compiled. In our case, if a type is constructible from a given set of attributes, then we’ll compile themake_unique
call. If not, thennullptr
is returned (andmake_unique
is not even instantiated).C++20
With concepts we can easily replace
enable_if
:But I wonder if that’s better? I think in this case,
if constexpr
looks much better and easier to follow.Here’s the working code @Compiler Explorer
Use case 4 - real-life projects
if constexpr
is not only cool for experimental demos, but it found its place in production code.If you look at the open-source implementation of STL from the MSVC team, we can find several instances where
if constexpr
helped.See this Changelog: https://github.com/microsoft/STL/wiki/Changelog
Here are some improvements:
if constexpr
instead of tag dispatch in:get<I>()
andget<T>()
forpair
. #2756,if constexpr
instead of tag dispatch, overloads, or specializations in algorithms likeis_permutation()
,sample()
,rethrow_if_nested()
, anddefault_searcher
. Generating Icons with Pixel Sorting #2219 ,<map>
and<set>
’s common machinery. Bytecode compilers and interpreters | Max Bernstein #2287 and few others,if constexpr
instead of tag dispatch in: Optimizations infind()
. 2018我的人生游戏指南-欲望与成救模式 - 少数派 #2380,basic_string(first, last)
. The Coding Kata: FizzBuzzWhizz in Java8 社区 #2480if constexpr
to simplify code. Write your Own Virtual Machine #1771Let’s have a look at improvements for
std::pair
:Untag dispatch
get
forpair
by frederick-vs-ja · Pull Request #2756 · microsoft/STLBefore C++17 benefits, the code looked as follows:
And after the change, we have:
It’s only a single function and much easier to read! No need for tag dispatch with the
integral_constant
helper.In the other library, this time related to SIMD types and computations (popular implementation by Agner Fog ), you can find lots of instances for
if constexpr
:https://github.com/vectorclass/version2/blob/master/instrset.h
One example is the mask function:
Without
if constexpr
the code would be much longer and potentially duplicated.Wrap up
Compile-time
if
is an amazing feature that significantly simplifies templated code. What’s more, it’s much more expressive and nicer than previous solutions: tag dispatching orenable_if
(SFINAE). Now, you can easily express your intentions similarly to the “run-time” code.We also revised this code and examples to work with C++20! As you can see, thanks to concepts, the code is even more readable, and you can “naturally” express requirements for your types. You also gain a few syntax shortcuts and several ways to communicate such restrictions.
In this article, we’ve touched only basic expressions, and as always, I encourage you to play more with this new feature and explore.
Going back…
And going back to our
str
example:Can you now rewrite the
str
function (from the start of this article) usingif constexpr
? :) Try and have a look at my simple solution @CE.Even more
You can find more examples and use cases for
if constexpr
in my C++17 Book: C++17 in Detail @Leanpub or @Amazon in Printvia C++ Stories
October 11, 2022 at 02:49PM
The text was updated successfully, but these errors were encountered: