1

I was trying to create a static interface using C++20 concepts, and the following code seems to do the job:

template <class FOO>
concept FooConcept = requires {
    static_cast<void (FOO::*)(int)>(&FOO::operator());
    static_cast<void (FOO::*)(char)>(&FOO::operator());
};

In particular, FooConcept is satisfied by a class FOO if such class overloads operator() twice: with an int argument and a char argument, respectively.

This solution seems to work fine, but it doesn't look beautiful. Indeed, I would've liked much more the following forms:

template <class FOO>
concept FooConcept = requires (FOO foo, int i, char c) {
    { foo(i) } -> std::same_as<void>;
    { foo(c) } -> std::same_as<void>;
};

or

template <class FOO>
concept FooConcept = std::invocable<FOO, int> && std::invocable<FOO, char>;

However, these approaches don't work due to implicit conversions (see this post). Is there a better, more "semantic" way to express this constraint?

fdev
  • 127
  • 12
  • "FooConcept is satisfied by a class FOO if, and only if, such class overloads operator() twice: with an int argument and a char argument, respectively." - Not exactly: if but non only if: is satisfied also if `FOO` has a template operator (for example `template void operator() (T) { }`) – max66 May 05 '21 at 16:04
  • Sure, my mistake! I'll fix the question immediately. But still it does what I want: it behaves as a static interface! – fdev May 05 '21 at 16:07
  • Know that you're fighting against the design of concepts here. They're meant to model how you use the type. – chris May 05 '21 at 16:09
  • I'm just trying to use concepts in a convenient way. It looks like they *can* achieve static polymorphism in a nice way, but it's not as obvious *how*. All the examples I find do not actually specify a static interface: they enforce that concept-compliant objects have member functions with specific names, taking specific types as arguments *up to implicit conversions*! – fdev May 05 '21 at 20:23
  • 1
    Yes, they reflect how a template constrained on the concept would actually use the type. For example, having `t.foo(0);` means that the template is safe to do `t.foo(0)`. Whether or not the 0 is converted is pretty irrelevant in that case. You can look into the C++0x concepts history to see why the original direction of specifying an interface more the way you're trying to do isn't the direction that the current concepts took. The results of those meetings and discussions will say more than I ever could on the why. – chris May 05 '21 at 21:58
  • 1
    Well, you can always give the ugliness a name: https://godbolt.org/z/TG45c5PGe – Bob__ May 05 '21 at 23:04
  • @chris I've looked for the C++0x concept history, but I couldn't find what you're referring to: would you kindly provide a link? Anyway, I really don't get all the fuss about using concepts in a way they weren't designed for. If it seems so natural to me (and to [many others](https://www.reddit.com/r/cpp/comments/kcinhu/how_suitable_are_concepts_for_use_as_a/gfsmy88?utm_source=share&utm_medium=web2x&context=3)) that they can/should be used for static interfaces in place of, e.g., CRTP, then why do concepts not allow this possibility in a "natural" way? – fdev May 06 '21 at 07:10
  • The example I shared using `static_cast` perfectly works: the only thing I don't like is that I'm using `static_cast` and not something like "`std::member_function`". Couldn't this be added to C++20 with little effort? Or is there an alternative way I missed? This question may be hard to answer, since I could never find anything similar. – fdev May 06 '21 at 07:15
  • 1
    @fdev, I wasn't referencing any particular thing, but I know the original incarnation of concepts had quite the history. For a large feature to dead-end and then come back in a completely different form involves a lot of discussion all around, so it was more of a general reference. I wasn't trying to make a fuss so much as point out that the current incarnation is designed to model the user of the type rather than the implementor because the mental model is quite different. – chris May 06 '21 at 14:44
  • @chris Thank you for your answer, I understand your point! – fdev May 06 '21 at 15:27

1 Answers1

2

With some helper, and with non-final classes, you might do:

template <typename T>
struct DeletedOperator : T
{
    using T::operator ();

    template <typename ... Ts>
    void operator()(Ts&&...) = delete;

    template <typename ... Ts>
    void operator()(Ts&&...) const = delete;
};

template <class FOO>
concept FooConcept = requires (DeletedOperator<FOO> foo, int i, char c) {
    { foo(i) } -> std::same_as<void>;
    { foo(c) } -> std::same_as<void>;
};

Demo

I think we can get rid of the non-final constraint by adjusting the helper.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • I upvoted your idea, although I don't fully understand it! Would you mind to elaborate it? Anyway, it doesn't produce exactly the desired behavior: for instance, if the class `FOO` overloads `operator()` with a default argument (e.g., `void operator()(int i, double d = 0.);` instead of `void operator()(int i);`), then the `FooConcept` you defined won't complain, whereas the implementation I suggested rightfully will. Also, although it's not clearly stated in the question, I'd like a general way to do all this for general member functions, and not restricted to `operator()` functions! – fdev May 06 '21 at 13:42
  • 1
    Idea is that template deleted overloads avoids conversion as they are exact match. – Jarod42 May 06 '21 at 13:56