Unconstrained requires-expression parameter

Viewed 146

Can I use constrained generic types inside C++20 constraints for a concept? As an example, say I'd like to write a requirement that a candidate class T have a function func that can take an argument of any type that satisfies std::ranges::range:

template<typename T>
concept Foo = requires (T t, std::ranges:range s) {
    { t.func(a); } -> std::convertible_to<double>;
}

But GCC gives me an error message that a placeholder type cannot be used where I've put std::ranges::range.

2 Answers

It seems to me that your looking a struct with an (declared only) operator T () for a generic T

struct generic_argument
 {
   template <typename T>
   operator T () const;
 };

so you can pass it (inside a decltype()) for every expected argument and also a template argument.

So your concept (sorry... I simplify it removing the ranges argument) become (if I understand what do you want) something as

template<typename T>
concept Foo 
   = std::convertible_to<decltype(std::declval<T>().func(std::declval<generic_argument>())),
                         double>;

The following is a full compiling example

#include <iostream>

struct A { double func (int a) { return a; } };
struct B { double not_func (long a) { return a; } };
struct C { std::string func (char) { return "abc"; } };
struct D { float func (auto) { return 1.0f; } };

struct E { double func (int a) { return a; }
           std::string func (char) { return "abc"; } };

struct generic_argument
 {
   template <typename T>
   operator T () const;
 };

template<typename T>
concept Foo 
   = std::convertible_to<decltype(std::declval<T>().func(std::declval<generic_argument>())),
                         double>;

template <Foo T>
void bar (T const &)
 { std::cout << "bar, Foo version\n"; }

template <typename T>
void bar (T const &)
 { std::cout << "bar, generic version\n"; }

int main()
 {
   bar(A{}); // print bar, Foo version
   bar(B{}); // print bar, generic version [no func() function]
   bar(C{}); // print bar, generic version [no convertible to double]
   bar(D{}); // print bar, Foo version
   bar(E{}); // print bar, generic version [two func() function]
 }

I'm in doubt regarding the E case: two func() methods, one only return a type convertible to double, from bar(E{}) we get "bar, generic version".

I have learned that, no, this is not possible. The following article by Arthur O'Dwyer explains that in general, C++20 concepts cannot check mathematical quantifiers (https://quuxplusone.github.io/blog/2020/08/10/concepts-cant-do-quantifiers/)

People frequently ask how to do things like this in C++20:

Make a concept Renderer that is satisfied if and only if t.render(u) is valid for all Printable types U. (From /r/cpp.)

Make a concept ValidSize that is satisfied if and only if t is convertible to some integral type U. (From the standard.)

The key words above are “all” and “some.” In mathematics, these are known as the universal and existential quantifiers:

Renderer ⇔ ∀U∈Printable: t.render(u) is well-formed

ValidSize ⇔ ∃U∈Integral: U u = t; is well-formed

C++20 Concepts do not support either quantifier.

...

The example in my question is identical to the Renderer example below, asking the concept to check that T t has func where t.func(a) is well-formed for all types A that satisfy std::ranges::range. This uses a universal quantifer, which C++20 concepts cannot check.


As to whether or not such a thing could be possible for C++20 to support, from what I gather from comments such as Unconstrained requires-expression parameter, this would not even be possible, in the sense that it reduces to solving the halting problem. In direct conversation with the author of the article, Arthur O'Dwyer, he points out that at some point the constraints have to come down to a closed, fully specified list of concrete type:

the only thing that'll help is to get rid of the quantifiers and deal with closed sets of types. For example, you can't do

template<typename T>
concept Foo = requires(T t) { t.func(a); };  // FOR-SOME-a; or FOR-ALL-a

but you can do

template<typename T, typename A>
concept FooHelper = requires(T t, A a) { t.func(a); };

template<typename T>
concept Foo = FooHelper<T, int> || FooHelper<T, short> || FooHelper<T, char>;
    // or, use &&
Related