if constexpr requires requires { requires }
Probably the two most useful features added to C++20 are requires
and requires
. They make it so much easier to control overload resolution, and when combined with if constexpr
in C++17, they allow basic reflection-based optimizations in templates. While requires requires
has gotten a lot of (negative?!) press for controlling overload resolution, its cousin requires { requires }
is a bit overlooked.
requires
is really useful
C++20 added requires
, a way to enable or disable a function overload based on some compile-time condition. For example, consider a facility for producing debug output of types for error reporting:
The two overloads with the requires
clause are only enabled for integers or floating point types, respectively, and are not considered otherwise. Additionally, overload resolution is smart: It knows that we want the overload with the most specific requirements, and it will only pick the first function when no other overload matches. This is also where concept
comes in: A concept
is simply a way to name a group of requirements that affects the search for more specific requirements. The technical term for that is subsumption. Because creating named requirements with concept
also comes with additional syntax sugar, you don't need requires
—so this blog post is gonna ignore concept
. In general, if you would use a concept
in only one place, it is too early to introduce it.
requires
is also really useful
requires
is another new feature for C++20 that allows us to check whether a condition is well-formed, instead of forcing us to rely on SFINAE. For example, let's say we're implementing a generic cont_assign(container, rng)
for every container with .push_back()
:
If the container additionally has a .reserve()
function and the range is sized, we can use it to avoid reallocations. Checking for .reserve()
is the job of requires
:
Note that the requires
not only checks for .reserve()
but also for std::ranges::size
.
In addition, requires
can also declare variables you can use in the body by writing requires(const T& a, U b) { … }
, or it can check the type of expressions using requires { { expr } -> std::same_as<V>; }
.
requires
and requires
combine to requires requires
Of course, both features, requires
and requires
, can combine to become even more powerful. By combining them, you can enable or disable an overload based on the well-formedness of an expression. For example, we can add a debug_output
overload for everything that has a .debug_output()
member function:
This is just like SFINAE, but better. First, we have a really nice syntax. Second, because we can use the requires(…)
syntax, we don't need the std::declval
hack to get expressions of a particular type. Third, we can easily check additional properties, like whether the expression result satisfies a particular concept, by using the { …} -> …;
syntax.
requires
and requires
also combine to requires { requires }
requires
and requires
also suppress errors in expressions that depend on template parameters: If the expression is ill-formed during instantiation, the compiler considers the requirement as not met, and ignores the overload or evaluates to false
, respectively. This behavior is essential when controlling overload resolutions. For example, let's say the debug output of a pointer is its address in hex, but if it's a char
pointer, it's treated as a C string.
The requires
clause in the second overload has two conditions: T
is a pointer and T
dereferences to a char
. Note that the second condition would be ill-formed if T
is not a pointer, but that does not cause a hard error. Instead, the overload is just silently skipped.
However, inside if constexpr
, we do not get this behavior, which is problematic. For example, let's say we are implementing an algorithm that checks whether all elements of a range are equal.
At think-cell, we have tc::constexpr_size<Rng>()
, which returns the size of a range as a compile-time constant for ranges where that is possible, like std::array
. We would now like to optimize all_same()
for those ranges. A first attempt could look like like this:
However, this code will not compile if Rng
does not have a compile-time size! For dynamically sized ranges, tc::constexpr_size
simply does not exist, and because we are not inside a requires
, the compiler will raise a hard error upon instantiation of all_same()
.
Now, we could fix that by using a requires
clause and an additional overload:
This works because the second overload is only picked if the condition is well-formed and evaluates to true
without raising a hard error in the process.
However, I don't like this solution. For starters, if the signature of all_same()
were more complex (for example, by taking an additional predicate), all those parameters and their type specifications have to be repeated. Additionally, it is not meant to be an open and user-extendable overload set—there should only be a single all_same()
function. This is why I used if constexpr
in the first place.
Luckily, we can just use requires
in the if constexpr
: We can use requires { … }
to introduce a scope that suppresses hard errors and returns a boolean, and then inside, use requires
to check the condition:
Writing requires
inside requires { … }
means "check this condition, and don't consider the requirements to match if it evaluates to false
." So, requires { … }
only evaluates to true
if the compile-time size of the range is less than or equal to one, and false
otherwise. Crucially, for ranges that do not have a compile-time size, requires { … }
evaluates to false
without causing a hard error.
This pattern is really useful: if constexpr
requires requires { requires }
.
requires requires { requires }
Unlike requires requires
and requires { requires }
, which are perfectly reasonable C++ code, requires requires { requires }
is completely silly. Since we are inside a requires
clause anyway, we can just pull the requires
condition out of the requires { … }
scope and check it directly. This will not cause hard errors anyway.
Do you have feedback? Send us a message at devblog@think-cell.com !
Sign up for blog updates
Don't miss out on new posts! Sign up to receive a notification whenever we publish a new article.
Just submit your email address below. Be assured that we will not forward your email address to any third party.
Please refer to our privacy policy on how we protect your personal data.