Bug in C++20 concepts with template member functions and std::invocable

419 Views Asked by At

I was experimenting with C++20 concepts and the Eigen library, and I incurred an unexpected behavior. Specifically, consider the following concept requiring a type to be callable with either an Eigen::Matrix<double, -1, 1>> object or an Eigen::Matrix<char, -1, 1>> one:

template <class FOO_CONCEPT>
concept FooConcept = std::invocable<FOO_CONCEPT, Eigen::Matrix<double, -1, 1>> &&
    std::invocable<FOO_CONCEPT, Eigen::Matrix<char, -1, 1>>;

Then, look at the commented line (*) in the following struct:

struct Foo {
    // template <typename T>    <----    (*)
    void operator()(Eigen::Matrix<double, -1, 1>) {
    }

    void operator()(Eigen::Matrix<float, -1, 1>) {
    }
};

Note that the class Foo doesn't satisfy the requirements of FooConcept since it can't be called with an Eigen::Matrix<char, -1, 1> argument. Indeed:

std::cout << FooConcept<Foo> << std::endl;

prints 0. However, when I toggle the line comment (*), i.e., when the operator() is a template, the same code oddly prints 1. Is this a bug? I got these results both using Clang 12.0.1 and GCC 11.1.0 to compile the code on Visual Studio Code. Thank you for any help you can provide!

P.S.: the line

 std::cout << std::is_convertible<Eigen::Matrix<char, -1, 1>, Eigen::Matrix<float, -1, 1>>()
              << std::endl;

prints 1, but an Eigen::Matrix<char, -1, 1> object cannot be implicitly converted into an Eigen::Matrix<float, -1, 1>. Is this another bug? And is this correlated to the above problem somehow?


EDIT 1: I noticed that by defining

struct FooImplicit {
    void operator()(Eigen::Matrix<char, -1, 1>) {
    }
};

the FooImplicit struct actually satisfies the FooConcept, and the same happens if you replace char with double. This looks related to the convertibility of the two Eigen types -- see P.S.

How can I express the constraint I want without allowing implicit conversions? That is, FooConcept must allow only classes that overload operator() at least twice, once with Eigen::Matrix<double, -1, 1> and once with Eigen::Matrix<char, -1, 1>. Can this be done?

Also, if I define the function

void func(FooConcept auto x) {}

and I try to call it as func(Foo()); keeping the line (*) commented, I get the following compile error:

[build] [...]: note: because 'Foo' does not satisfy 'FooConcept'
[build] void func(FooConcept auto x) {
[build]               ^
[build] [...]: note: because 'std::invocable<Foo, Eigen::Matrix<char, -1, 1> >' evaluated to false

Is this because the compiler cannot choose unambiguously which overload to call? If yes, why isn't the error message more explicit? To me, it looks just like the compiler noticed that Foo has two member functions, and one is correct, whereas the other one isn't.


EDIT 2: I managed to answer half of the question in this post. However, I'm still curious about the error message I got from the compiler.

1

There are 1 best solutions below

5
On

is_convertable merely determines if the overload exists and can be found unambigouously. In Eigen's case, the overload converting between those types exists, but the body has a static_assert.

is_convertable does not instantiate the body of the conversion operation before saying it works. This is intentional, to permit C++ overload resolution to not require compiling a huge amount of code.

For your traits to work, Eigen needs to be rewitten to support "SFINAE"-friendly methods and conversion operators.

The failure in your test was because the char matrix converted to both, which is ambiguous, so the trait fails. This isn't why you thought it failed.

Adding the template<class T> means that overload isn't considered (T cannot be deduced), so converting to an array of float is unambiguously selected.