15

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?

10
  • 2
    void h5(std::integral auto i) {}? Commented Oct 15 at 12:02
  • 6
    What's wrong with 3? It is explicit. Though I would probably name it like namespace unqualified { template <class T> concept Integral = std::integral<std::remove_cvref_t<T>>; } and then void h3(unqualified::Integral auto&&) {} Commented Oct 15 at 12:02
  • 1
    @Jarod42 Now that you mention that.... Kind of makes sense, I am hard pressed to think of a situation where you would have an rvalue reference to an integral. And just having an lvalue reference... that's an in/out argument probably also not what is intended Commented Oct 15 at 12:04
  • 3
    When you create your concept, you have indeed to think if it applies to references too or not (and to cv qualifiers). Then when using a concept with extra stuff, up to you to create another concept, or to use the requires in place. Subsumption would be the main difference. Commented Oct 15 at 12:09
  • 2
    simplified example aside, the crux of the current issue is that you use a concept that does not apply to references but your premise is that it should. The only way out is to explicitly remove the referenceness from T Commented Oct 15 at 13:24

1 Answer 1

11

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?

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.