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:

template <typename T>
auto debug_output(const T&) { // default implementation
    return "???";
}

template <typename T>
    requires std::integral<T>
auto debug_output(const T& t) {
    // return a range of characters representing the integer value of t
}

template <typename T>
    requires std::floating_point<T>
auto debug_output(const T& t) {
    // return a range of characters representing the floating point value of t
}

…
debug_output converts a type to its debug output.

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():

template <typename Cont, typename Rng>
void cont_assign(Cont& cont, Rng&& rng) {
    cont.clear();

    for (auto&& elem : std::forward<Rng>(rng)) {
        cont.push_back(std::forward<decltype(elem)>(elem));

    }
}
cont_assign is a generic algorithm to assign a range to a container.

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:

template <typename Cont, typename Rng>
void cont_assign(Cont& cont, Rng&& rng) {
    cont.clear();

    if constexpr (requires { cont.reserve(std::ranges::size(rng)); }) {
        cont.reserve(std::ranges::size(rng));
    }
    for (auto&& elem : std::forward<Rng>(rng)) {
        cont.push_back(std::forward<decltype(elem)>(elem));
    }
}
if constexpr can be used to optimize a generic algorithm for certain types.

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:

template <typename T>
    requires requires(const T& t) { t.debug_output(); }
auto debug_output(const T& t) noexcept(noexcept(t.debug_output())) {
    return t.debug_output();
}
Forward to a member function if there is one. Note that we also propagate noexcept-ness by using two C++11 features, noexcept and noexcept.

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.

template <typename T>
    requires std::is_pointer_v<T>
auto debug_output(const T& t) {
    // return a range of characters representing the address of t
}

template <typename T>
    requires std::is_pointer_v<T> && std::same_as<std::remove_cvref_t<decltype(*T())>, char>
auto debug_output(const T& t) {
    return debug_output(std::string_view(t));
}
We add two overloads for debug output of pointers.

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.

template <std::ranges::forward_range Rng>
bool all_same(Rng&& rng) {
    auto it = std::ranges::begin(rng);
    auto end = std::ranges::end(rng);
    if (it == end) return true;

    auto&& first = *it;
    for (++it; it != end; ++it) {
        if (*it != first) return false;
    }

    return true;
}
all_same is a generic algorithm to check whether all elements of a range are the same.

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:

template <std::ranges::forward_range Rng>
bool all_same(Rng&& rng) {
    if constexpr (tc::constexpr_size<Rng>() <= 1) {
        // If the range has zero or one element, all elements are the same.
        return true;
    } else {
        … // loop as before
    }
}
We use if constexpr in our first attempt at optimizing zero or one element ranges.

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:

template <std::ranges::forward_range Rng>
bool all_same(Rng&& rng) { … } // loop

template <std::ranges::forward_range Rng>
    requires (tc::constexpr_size<Rng>() <= 1)
bool all_same(Rng&& rng) {
    return true;
}
We use require and two overloads in our second attempt at optimizing zero or one element ranges.

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:

template <std::ranges::forward_range Rng>
bool all_same(Rng&& rng) {
    if constexpr (requires { requires tc::constexpr_size<Rng>() <= 1; }) {
        return true;
    } else {
        … // loop as before
    }
}
We use requires { requires } in our final attempt at optimizing zero or one element ranges.

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.

— by Jonathan Müller

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.

Share