c++c++-concepts

How to apply a concept to a member function in a concept and then use it?


Follow-up question to this question, where single argument use was not solved.

Assume the following C++ concept:

template <typename T>
concept has_set = requires(T t, std::string s) {
    { t.set(s) } -> std::same_as<void>; };
};

I cannot use a concept for parameter s, e.g.

template <typename T>
concept has_set = requires(T t, std::convertible_to<std::string_view> s) {
    { t.set(s) } -> std::same_as<void>; };
};

will fail to compile. Is there any workaround or trick which can be applied here to let the following additional code compile?

std::string use(has_set auto & f) { /* ... use f.set(...) ... */ }

Please note that part of the solution is already given in https://stackoverflow.com/a/79130496/1528210


Solution

  • I'm offering an alternative answer, because I think the accepted one will work for most real cases, but isn't a 100% match to my understanding of the requirements.

    My understanding of the requirements is: type Foo should match has_set<Foo>, if and only if it has a set method returning void, which can be called with any argument implicitly convertible to string_view. Basically, any valid set implementation should look like: void set(std::string_view).

    The best solution I could come up with

    #include <concepts>
    #include <string_view>
    
    
    template<typename T>
    concept has_set = requires {
        // Use the static cast to support classes that have multiple set overloads
        { static_cast<void(T::*)(std::string_view)>(&T::set) };
    };
    
    struct Foo {};
    static_assert(!has_set<Foo>);
    
    struct Bar {
        void set(std::string_view);
    };
    static_assert(has_set<Bar>);
    
    struct Baz {
        void set(std::string);
    };
    static_assert(!has_set<Baz>);
    
    struct FooBarBaz {
        void set(std::string_view);
        void set(int);
    };
    static_assert(has_set<FooBarBaz>);
    

    This matches the exact signature you'd like to see for set (see godbolt). Thanks to StoryTeller - Unslander Monica in the comments for suggesting the cast to deal with overload sets.

    First attempt

    template<typename T>
    concept has_set = requires(T t, std::string_view sv) {
        { t.set(sv) } -> std::same_as<void>;
    };
    

    This is easy, but not entirely correct. See:

    struct Foo {
        Foo(std::string_view) {}
    };
    struct Bar {
        void set(Foo) {}
    };
    static_assert(has_set<Bar>, "True, because Bar::set can be called with a string view");
    static_assert(requires(Bar b, std::string s) { b.set(s); }, "Fails, because string -> string_view -> Foo is more than one implicit conversion");
    

    This is clearly unexpected. I'd argue that in practice this is still good enough, because I can't imagine when this problem would come up.

    Second attempt

    namespace detail {
    struct StringViewConvertible {
        operator std::string_view();
    };
    }
    template<typename T>
    concept has_set = requires(T t, detail::StringViewConvertible svc) {
        { t.set(svc) } -> std::same_as<void>;
    };
    
    1. Since the type is in the detail namespace, the only way someone writes a function accepting this type is if they maliciously want to make your concept fail. I can't think of a way to guard against that.
      • See the comments of this answer for a neat solution to this!
    2. Since the only thing this type can do is convert to a string_view, the only way the call can be well formed is if T::set accepts a string_view.