3

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.

Amir Kirsh
  • 12,564
  • 41
  • 74
  • 1
    `Z::foo` is not SFINAE-friendly. No matter how you define the concept, you can't get around that. – Patrick Roberts Jun 20 '23 at 03:49
  • The two problems are the same, yes. It's just that in the latter, you have a non-deduced return type, so checking whether calling `foo` on `Z` is valid doesn't require parsing the body to deduce the return type. In the former, it's a deduced return type, so it has to parse the body and that's when it encounters a substitution failure that causes a hard error because the failure was neither part of the function declaration nor the class template declaration. – Patrick Roberts Jun 20 '23 at 04:04
  • You're now asking three separate but related questions. It's an interesting topic to be sure, but you may find more success in garnering answers if you pare it back down to one question, which judging by your post, the thing you seem most interested in is finding a solution to the problem expressed in the very last example. – Patrick Roberts Jun 20 '23 at 04:13

3 Answers3

3

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();
  }
};
康桓瑋
  • 33,481
  • 5
  • 40
  • 90
2

Following the comments by @PatrickRoberts, we cannot SFINAE the template or check the existence of a function where the existence is relying on internal implementation of the template, this may lead to either compilation error, because of the need to parse the template which may encounter a substitution failure that would cause a hard error, or to a wrong answer -- wrong in the sense of the if constexpr at the end of the question.

As it seems, the only one that can save us from the above is the one who implements the template itself.

For example, we can implement Z as the following:

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

template<typename T>
struct Z {};

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

Now we can safely work with Z:

struct X {
    void foo() {}
};

struct Y {};

static_assert(HasFoo<X>);
static_assert(!HasFoo<Y>);

// static_assert(HasFoo<Z<Y>>); // would fail, justifiably
static_assert(!HasFoo<Z<Y>>); // passes

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

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>{}); // ok, doesn't call foo inside
}

Code link


Now what happens if we want to inquire for more than one member? We should probably go for some kind of Mixin, like that:

template<HasFoo T>
struct Fooable {
    void foo() {
        T().foo();
    }
};

template<HasMoo T>
struct Mooable {
    void moo() {
        T().moo();
    }
};

template<typename T>
struct Z {};

template<HasFoo T>
struct Z<T>: Fooable<T> {};

template<HasMoo T>
struct Z<T>: Mooable<T> {};

template<typename T>
    requires HasFoo<T> && HasMoo<T>
struct Z<T>: Fooable<T>, Mooable<T> {};

Which now allows:

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

int main() {
    lets_foo_and_moo(X{}); // ok, calls foo inside
    lets_foo_and_moo(Y{}); // ok, calls nothing
    lets_foo_and_moo(Z<X>{}); // ok, calls foo inside
    lets_foo_and_moo(Z<Y>{}); // ok, calls nothing
}

Code link

Anyway, the burden is on the template implementer, assuming that they are aware of this requirement.


After posting my answer I see now the solution proposed by @康桓瑋 which I must say is more simple and elegant (even though, the Mixin approach might be useful for some situations).

Amir Kirsh
  • 12,564
  • 41
  • 74
1

C++ does not require compilers to fully instantiate all code before a substitution failure occurs.

This is because doing so is considered hard by compiler writers.

In general, a failure within the bodies of methods results in a hard compile time error. No method to detect such failures exists without modifying the method itself, and this is intentional. The error is detected while compiling the body of the method, and the compilation can halt immediately with no fallbacks.

When SFINAE (substitution failure is not an error) was the only technique to do this kind of thing, methods that didn't fail-hard where called "SFINAE-friendly".

In the concept era, it looks like:

  void foo() requires HasFoo<T> {

instead of

  void foo() {

Pre-concepts, you might do a manual check, or use a macro like:

#define RETURNS(...) -> decltype(__VA_ARGS__) { return __VA_ARGS__; }

then do

  auto foo() RETURNS( T().foo() }

which does something somewhat similar.

This problem -- of hard errors in methods -- occurred in the std library. vector's operator<, for example, was defined to simply invoke < on its contained value - and if it failed, the error was a hard one.

I have fixed this in the past with per-std-container specializations that detect the requirements of the contained types and deduce (manually) the container properties.

The only way to avoid such manual work is cooperation of the types you are interacting with.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524