Search code examples
c++templatesc++20c++-concepts

C++ concept requiring certain behavior from a template


How can we use a concept to require behavior (e.g. existence of certain method) on a template?

Let's take an example:

template <typename T>
concept HasFoo = requires(T t) {
    t.foo();
};

Now we have:

struct X {
    void foo() {}
};

struct Y {};

static_assert(HasFoo<X>);  // ok, passes
static_assert(!HasFoo<Y>); // ok, passes

If we add:

template<typename T>
struct Z {
    auto foo() {
        return T().foo();
    }
};

This works as expected:

static_assert(HasFoo<Z<X>>);  // passes

But both the following fail compilation:

static_assert(HasFoo<Z<Y>>);  // static assert fails
static_assert(!HasFoo<Z<Y>>); // compilation error: no member named 'foo' in 'Y'

It's not so helpful that when the static assert can pass, we get compilation error for not having 'foo'. Is there a way to implement this concept so it will work for this case?

This is the first problem, the template seems to be instantiated in order to check the concept, which fails the compilation.

Code link


If we change Z a bit:

template<typename T>
struct Z {
    void foo() {
        T().foo();
    }
};

Then the compiler sees Z as having foo, regardless of whether its internal type implemented foo or not, thus:

static_assert(HasFoo<Z<Y>>);  // passes
static_assert(!HasFoo<Z<Y>>); // fails 

This is a second problem. (It seems that there isn't any simple solution for this one).

Code link


Are these two problems the same? Or not? Is there maybe a solution for one while not for the other?

How can we safely implement code like this:

template<typename T>
void lets_foo(T t) {
    if constexpr(HasFoo<T>) {
        t.foo();
    }
}

When it may fail for templated types:

int main() {
    lets_foo(X{}); // ok, calls foo inside
    lets_foo(Y{}); // ok, doesn't call foo inside
    lets_foo(Z<X>{}); // ok, calls foo inside
    lets_foo(Z<Y>{}); // fails to compile :(
}

A note: this question is a follow-up based on a similar but more specific question that got its specific solution: How to do simple c++ concept has_eq - that works with std::pair (is std::pair operator== broken for C++20), it seems though that the problem is broader than just a single issue.


Solution

  • The main issue here is that Z's foo() does not have any constraints, but its implementation expects the expression T().foo() to be well-formed, which will cause a hard error inside the function body when T does not have foo() because concept only checks the function's signature.

    The most straightforward way is to constrain Z::foo() to conform to its internal implementation (although this also requires T to be default constructible)

    template<typename T>
    struct Z {
      auto foo() requires HasFoo<T> {
        T().foo();
      }
    };