Seen on the CppCon whiteboard

Seen on the whiteboard outside the Aurora D conference room at CppCon 2019:

Can I write a concept that matches any type that has a member function with a given signature?

And then underneath:

template<class T>
concept Foo = requires {
    int T::foo(double);  // not real syntax
};

And then underneath that:

EDIT: Got my answer, thanks!

Which by the way is basically the most useless edit you can make to any technical query. If you’re going to claim to have gotten your answer, at least repeat that answer for posterity! This applies just as much to whiteboards as to StackOverflow.

Anyway, I wrote “can’t be done,” which generated some fun discussion just now. I’ll present the discussion separated by horizontal lines, with spoilers below each line.

I guess I should mention that another correct response is “You shouldn’t try to do that,” or “you’re asking the wrong question.” C++2a Concepts aren’t designed to discriminate based on function signatures; they’re designed to deal with syntactic well-formedness of expressions. But it’s still fun, and perhaps educational, to try to answer the question as it was originally asked.


Step one: let’s try to use valid C++2a syntax!

template<class T>
concept Foo = requires (T t) {
    t.foo(1.0) -> int;
};

GCC and Clang both complain:

    t.foo(1.0) -> int;
                  ^
error: expected unqualified-id

Do you see the problem? Spoiler below the line.


The problem with that first attempt is that C++2a syntax requires curly braces around any expression whose result type is being constrained. What we’ve got there is a simple requirement that the expression t.foo(1.0)->int must be well-formed. Good thing we didn’t want to match size_t instead of int! It would have compiled quietly and matched essentially no types. (This was one of my chest-mimic examples in “Concepts As She Is Spoke” (CppCon 2018).)

Okay, step two: use actually valid C++2a syntax.

template<class T>
concept Foo = requires (T t) {
    { t.foo(1.0) } -> int;
};

struct S {
    int foo(double);
};
static_assert(Foo<S>);

This syntax compiles, and does essentially the right thing. This is the sort of thing that I would expect ordinary users of concepts to write, and this is the sort of thing that C++2a’s designers expect you to write.

Yet I said that OP’s question “can’t be done.” Because testing syntactic validity is not the same thing as matching, quote, “any type that has a member function with a given signature.” Spoiler below the line.


struct S {
    static char (*foo)(int, ...);
};
static_assert(Foo<S>);

This S has no member functions. It has a static data member of function-pointer type. The pointed-to function has a signature whose parameters are not (double) but (int, ...). Its return type is char, not int. But it still matches our concept!

We can fix the last, smallest, problem by writing -> std::same<int> instead of simply -> int. The -> some-concrete-type syntax is essentially a trap; no Standard Library components are specified using that syntax because it is so rarely what you want. I wouldn’t be surprised if it is dropped from C++2a. (C++20 UPDATE: Reader, it was.)

Anyway, our step-two concept also accepts some types that would be rejected by a strict reading of OP’s question, even though in the real world we’d probably be happy to accept them for template-metaprogramming purposes. For example:

struct S {
    template<class A>
    int foo(A) const noexcept;
};

This S has no member functions; it has a non-static member function template. Furthermore, the signature of the function instantiated from that template is const-qualified — int S::foo(double) const, not simply int S::foo(double). Furthermore, in C++17 even the noexcept-specifier is part of the function’s signature. (Or at least it’s part of its type. I don’t know if “signature” and “type” should be considered synonymous in this context.)

Next, Vaughn Cato and Anthony Williams pointed out a sneakier way to solve OP’s problem:

template<class T, int (T::*)(double)>
struct helper {};

template<class T>
concept Foo = requires (T t) {
    { helper<T, &T::foo>{} };
};

struct S {
    int foo(double);
};
static_assert(Foo<S>);

struct S2 {
    int foo(double) const;
};
static_assert(not Foo<S2>);  // hooray!

Notice that for backward compatibility, C++17 had to make noexcept function pointers implicitly convertible to non-noexcept function pointers. So we also have this:

struct S3 {
    int foo(double) noexcept;
};
static_assert(Foo<S3>);

However, we are still testing syntactic validity, not testing for a “member function with a given signature.” Do you see how to break this version? Spoiler below the line.


Library maintainers may have smelled out the sneaky solution, because they always have to be on the lookout for explicit use of operator&. The sneaky S in this case is

struct EvilS;
struct Evil {
    constexpr auto operator&() const -> int (EvilS::*)(double) { return 0; }
};
struct EvilS {
    static constexpr Evil foo {};
};
static_assert(Foo<EvilS>);

And unfortunately we cannot replace &T::foo with std::addressof(T::foo) because C++ does not allow T::foo to exist separately from an object when T::foo is a member function.

But this suggests a further epicyclical fix we can make to our concept! Do you see it?


The fix I’m thinking of is:

template<class T>
concept NotFoo = requires (T t) {
    { helper<T, &(T::foo)>{} };
};

template<class T>
concept Foo = requires (T t) {
    { helper<T, &T::foo>{} };
} && !NotFoo<T>;

Now concept Foo correctly rejects our EvilS, because &(EvilS::foo) is permitted, whereas well-behaved models of our concept permit &T::foo but not &(T::foo).

We still have to find a way to reject member function templates which can be instantiated through type deduction to match the signature int (S::*)(double).

struct S {
    template<class T> int foo(T);
};
static_assert(Foo<S>);  // oops!

Spoiler below the line.


As far as I know, any function template that’s callable as t.foo(x) is also callable as t.foo<>(x). The angle brackets just mean “here come the template arguments, if any”; it’s totally fine to provide none. So we can test for whether foo is a template similarly to how we tested if it was a static data member:

template<class T>
concept AlsoNotFoo = requires (T t) {
    { helper<T, &T::template foo<>>{} };
};

template<class T>
concept Foo = requires (T t) {
    { helper<T, &T::foo>{} };
} && !NotFoo<T> && !AlsoNotFoo<T>;

Unfortunately, this concept is too restrictive — do you see how?


We might have both a foo member function template and a foo member function with the appropriate signature!

struct S {
    template<class T> int foo(T);
    int foo(double);
};
static_assert(not Foo<S>);  // oops!

Since &S::template foo<> is well-formed, our latest concept rejects this S; but in fact this S does have a member function with the signature int foo(double), so we shouldn’t have rejected it at all!

Can you figure out the next step?

Here’s a Godbolt of the test cases so far.


UPDATE: Circa April 2020, Jason Cobb came up with this solution that correctly accepts our S above.

template<class T, int (T::*first)(double), int (T::*second)(double)>
concept NotEq = (first != second);

template<class T>
concept Foo = requires (T t) {
    { helper<T, &T::foo>{} };
} && !NotFoo<T> && (!AlsoNotFoo<T> || NotEq<T, &T::foo, &T::template foo<>>);

This clever piece of code checks (at compile time) to see whether converting &T::foo to a member function pointer produces the same pointer as converting &T::foo<>. There are three possibilities here:

  • If &T::foo<> is ill-formed, then we have no template member at all, and we want to accept. (This is the !AlsoNotFoo<T> case.)

  • If &T::foo<> == &T::foo, then there is a template member, and &T::foo also refers to the template member, so there must be no non-template member, and so we want to reject.

  • If &T::foo<> != &T::foo, then there is a template member, but there is also a different non-template member, so we should accept.

Looking for further loopholes, I see that the original question asked to match any type that has a member function with the given signature; it’s not clear whether this is intended to include inherited member functions or not. The solution we’ve been building since the introduction of helper<T, &T::foo> has a problem: it doesn’t work for inherited member functions.

struct Base { int foo(double); };
struct Inherited : Base {};
static_assert(not Foo<Inherited>);  // oops!

Can you figure out the next step, if there is one?

Here’s a Godbolt of the test cases so far.

Posted 2019-09-18