type deduction for std::function argument types with auto adds const

558 Views Asked by At

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.

2

There are 2 best solutions below

9
On BEST ANSWER

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.

4
On

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.