8

I have a struct with a method called call which has a const overload. The one and only argument is a std::function which either takes a int reference or a const int reference, depending on the overload.

The genericCall method does exactly the same thing but uses a template parameter instead of a std::function as type.

struct SomeStruct {
    int someMember = 666;

    void call(std::function<void(int&)> f) & {
        f(someMember);
        std::cout << "call: non const\n";
    }

    void call(std::function<void(const int&)> f) const& {
        f(someMember);
        std::cout << "call: const\n";
    }


    template <typename Functor>
    void genericCall(Functor f) & {
        f(someMember);
        std::cout << "genericCall: non const\n";
    }

    template <typename Functor>
    void genericCall(Functor f) const& {
        f(someMember);
        std::cout << "genericCall: const\n";
    }
};

When I now create this struct and call call with a lambda and auto & as argument the std::function always deduces a const int & despite the object not being const.

The genericCall on the other hand deduces the argument correctly as int & inside the lamdba.

SomeStruct some;
some.call([](auto& i) {
    i++;  // ?? why does auto deduce it as const int & ??
});
some.genericCall([](auto& i) {
    i++;  // auto deduces it correctly as int &
});

I have no the slightest clue why auto behaves in those two cases differently or why std::function seems to prefer to make the argument const here. This causes a compile error despite the correct method is called. When I change the argument from auto & to int & everything works fine again.

some.call([](int& i) {
    i++; 
});

When I do the same call with a const version of the struct everything is deduced as expected. Both call and genericCall deduce a const int & here.

const SomeStruct constSome;
constSome.call([](auto& i) {
    // auto deduces correctly const int & and therefore it should
    // not compile
    i++;
});
constSome.genericCall([](auto& i) {
    // auto deduces correctly const int & and therefore it should
    // not compile
    i++;
});

If someone could shine some light on this I would be very grateful!

For the more curious ones who want to dive even deeper, this problem arose in the pull request: https://github.com/eclipse-iceoryx/iceoryx/pull/1324 while implementing a functional interface for an expected implementation.

elfenpiff
  • 105
  • 4

2 Answers2

3

The issue is that it's a hard error to try to determine whether your lambda is Callable with const int & returning void, which is needed to determine whether you can construct a std::function<void(const int&)>.

You need to instantiate the body of the lambda to determine the return type. That's not in the immediate context of substituting a template argument, so it's not SFINAE.

Here's an equivalent error instantiating a trait.

As @aschepler notes in the comments, specifying a return type removes the need to instantiate the body of your lambda.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • I'd explain "hard error" a bit more: Even though the problem comes up during overload resolution to determine which `call` to use, it doesn't happen while substituting template arguments into another template parameter or a default template argument or a function type, but only while implicitly instantiating the body of the generic lambda, which makes this an actual error rather than just rejecting the overload candidate. – aschepler Apr 08 '22 at 13:55
  • 1
    Aha, it's actually because the closure type's `operator()` template has return type `auto`, which means that finding the type of the specialization requires instantiating the body. Add a `-> void`, and the error is gone! – aschepler Apr 08 '22 at 14:05
  • Can be worked around by making the operators take part in SFINAE by making them templates. `template > void call(F f) &` and `template > void call(F f) const&` – Yam Marcovic Apr 11 '22 at 09:46
  • @YamMarcovic that doesn't actually use `std::function`, the lambda type is what is deduced in the call. – Caleth Apr 11 '22 at 09:55
1

The problem is that generic lambdas (auto param) are equivalent to a callable object whose operator() is templated. This means that the actual type of the lambda argument is not contained in the lambda, and only deduced when the lambda is invoked.

However in your case, by having specific std::function arguments, you force a conversion to a concrete type before the lambda is invoked, so there is no way to deduce the auto type from anything. There is no SFINAE in a non-template context.

With no specific argument type, both your call are valid overloads. Actually any std::function that can match an [](auto&) is valid. Now the only rule is probably that the most cv-qualified overload wins. You can try with a volatile float& and you will see it will still choose that. Once it choose this overload, the compilation will fail when trying to invoke.

ElderBug
  • 5,926
  • 16
  • 25
  • It's actually a failure *during* overload resolution, determining if there are any constructors of `std::function`, it hasn't chosen "call const" at that point – Caleth Apr 08 '22 at 13:51
  • @Caleth Is it really ? I'm pretty sure normal overload resolution only use the function signature to pick one. Actually the exact same error will happen even if there is no overload at all. – ElderBug Apr 08 '22 at 13:56
  • Right, the `&` overload is in fact the better match, if we skip the actual calls to `f` to avoid the error. – aschepler Apr 08 '22 at 13:56
  • @Caleth Maybe you're right, but I'm curious why it would look further, I'm no language lawyer. In any case it doesn't change much since there is no SFINAE here. – ElderBug Apr 08 '22 at 13:58
  • @ElderBug the confusing thing is why there isn't SFINAE here. `std::is_invocable` *doesn't compile* – Caleth Apr 08 '22 at 14:01
  • @Caleth There is no template deduction at any point here, so there is no SFINAE possible. Even in the lambda template, they only exist in contexts where the type is already chosen, so no deduction. – ElderBug Apr 08 '22 at 14:03
  • @ElderBug yes there is, the constructor `template std::function::function(F&&)` – Caleth Apr 08 '22 at 14:04
  • As part of checking whether the const call is a viable function, the constructors of `std::function` are considered. The template one is considered iff `std::is_invocable_v` is true, but evaluating that is somehow an error. – Caleth Apr 08 '22 at 14:08
  • @elfenpiff You should probably accept the other answer as it is more accurate. – ElderBug Apr 08 '22 at 14:26