4

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.

fdev
  • 127
  • 12
  • "*prints 1, but an Eigen::Matrix object cannot be implicitly converted into an Eigen::Matrix.*" What makes you so sure about that? C++ seems to think it is, and it's generally correct about the code you give it. – Nicol Bolas May 03 '21 at 20:56
  • I tested it personally; Eigen makes you explicitly cast between different matrix types! – fdev May 03 '21 at 21:06
  • 5
    `std::is_convertible` reports `true` - but `Eigen` has a `static_assert` preventing the conversion, so `Eigen` is not SFINAE friendly in that sense. – Ted Lyngmo May 03 '21 at 21:08

1 Answers1

4

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I think I understood the first part of your answer, and it seems reasonable, so thanks! "*The failure i 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.*" I'm not sure about this statement, please refer to the EDIT in the main question. "*Adding the template means that overload isn't considered (T cannot be deduced), so converting to an array of float is unambiguously selected.*" This statement looks correct if the previous one is. Please refer once again to the EDIT in the main question! – fdev May 04 '21 at 07:09
  • As a follow-up comment, when I add *both* the `template` and a template specialization `template <> void operator()(Eigen::Matrix) {}`, I still get that `Foo` satisfies the `FooConcept`, which is something I wouldn't expect. The overload selection by the compiler should be ambiguous also in this case, shouldn't it? – fdev May 04 '21 at 07:18
  • 2
    @fdev I am pretty sure that template specializations do not play a role in overload resolution. Hence, adding one does not make a difference. – j6t May 04 '21 at 07:25
  • Would you kindly elaborate? Thanks! – fdev May 05 '21 at 06:28
  • OK, I assume you're referring to this [concept](https://www.ibm.com/docs/en/zos/2.4.0?topic=only-overloading-function-templates-c). Nice! – fdev May 05 '21 at 07:50