Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic way to define type constraint on forwarding reference argument

When using a type constraint on a forwarding reference argument, the constraint is given as an lvalue reference to the type. For example, the call to h in the following code does not compile because it would require std::integral<int &> to hold, but integral is not true for references (see https://godbolt.org/z/Taeb5Exdv):

#include <concepts>

void f(std::integral auto &i) {}
void g(const std::integral auto &i) {}
void h(std::integral auto &&i) {}

int main() {
    int one = 1;
    f(one);
    g(one);
    h(one); // "[...] the expression 'is_integral_v<_Tp> [with _Tp = int&]' evaluated to 'false'"
}

(The error is counter-intuitive to me, because f and g will evaluate integral<int> rather than integral<int&>/integral<const int&>, so my mind unconsciously extrapolated that to a "rule" like "the template arguments have cvref removed". But OK, it's more complicated; forwarding references are different, probably for a reason; I can accept that this is just "the way it works".)

I can work around this by replacing h by e.g. (see https://godbolt.org/z/fa9f7eeq6)

void h1(auto &&i) requires std::integral<std::remove_cvref_t<decltype(i)>>  {}

or

template <class T>
void h2(T &&i) requires std::integral<std::remove_cvref_t<T>>  {}

or

template <class T>
concept Integral_without_cvref = std::integral<std::remove_cvref_t<T>>;

void h3(Integral_without_cvref auto &&i) {}

All are a bit complicated: h1 and h2 requiring more syntax in the function declarations and h3 requiring a special concept.

Is there a more idiomatic/succinct way to declare constraints on forwarding reference arguments?

like image 532
Sven Sandberg Avatar asked Jan 28 '26 14:01

Sven Sandberg


1 Answers

In C++20 (and C++23), no. If you have a concept (like std::integral) that doesn't work on reference types and you want to allow a forwarding reference to that concept, you'll have to do it manually in either of the ways you're showing — manually stripping the reference on the function side or providing a custom concept that does so if this is sufficiently common.

In C++26, we have concept template parameters now — which allow you to have concept wrappers. This allows writing:

template <class T, template <class...> concept C, class... Args>
concept ForwardsTo = C<std::remove_cvref_t<T>, Args...>;

template <ForwardsTo<std::integral> T>
void f(T&& x);

template <ForwardsTo<std::convertible_to, int> T>
void g(T&& x);

Now, std::convertible_to isn't the best example for this particular case since std::convertible_to<int&, int> is true (whereas std::integral<int&> is false), but it's just my go-to-example for a binary concept and illustrating what the rest of the Args... might be useful for.


Note that we cannot make ForwardsTo<std::convertible_to<int>> work — this is because the type-constraint syntax that we have (template <std::convertible_to<int> T>) is very specific to that grammar, it cannot be used anywhere else. This is, I think, pretty unfortunate from a syntax perspective. And you cannot really make it work either because of variadic concepts. For example, what does std::invocable<F> mean? Is that checking if F is invocable with no args or is that supposed to be partial concept application for checking if some to-be-provided type is invocable with F?

like image 110
Barry Avatar answered Jan 31 '26 02:01

Barry