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?
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?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With