Hard error when using std::invoke_result_t with a generic lambda

1.6k views Asked by At

I have a container-like class with a method that works similarly to std::apply. I would like to overload this method with a const qualifier, however when I try to invoke this method with a generic lambda I get a hard error from instantiation of std::invoke_result_t. I am using std::invoke_result_t to deduce a return value of the method as well as to perform a SFINAE check of the argument.

#include <type_traits>
#include <utility>

template <typename T>
class Container
{
public:
    template <typename F>
    std::invoke_result_t<F, T &> apply(F &&f)
    {
        T dummyValue;
        return std::forward<F>(f)(dummyValue);
    }

    template <typename F>
    std::invoke_result_t<F, const T &> apply(F &&f) const
    {
        const T dummyValue;
        return std::forward<F>(f)(dummyValue);
    }
};

int main()
{
    Container<int> c;
    c.apply([](auto &&value) {
        ++value;
    });
    return 0;
}

The error message while compiling with Clang 6.0:

main.cc:27:9: error: cannot assign to variable 'value' with const-qualified type 'const int &&'
        ++value;
        ^ ~~~~~
type_traits:2428:7: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<const int &>' requested here
      std::declval<_Fn>()(std::declval<_Args>()...)
      ^
type_traits:2439:24: note: while substituting deduced template arguments into function template '_S_test' [with _Fn = (lambda at main.cc:26:13), _Args = (no value)]
      typedef decltype(_S_test<_Functor, _ArgTypes...>(0)) type;
                       ^
type_traits:2445:14: note: in instantiation of template class 'std::__result_of_impl<false, false, (lambda at main.cc:26:13), const int &>' requested here
    : public __result_of_impl<
             ^
type_traits:2831:14: note: in instantiation of template class 'std::__invoke_result<(lambda at main.cc:26:13), const int &>' requested here
    : public __invoke_result<_Functor, _ArgTypes...>
             ^
type_traits:2836:5: note: in instantiation of template class 'std::invoke_result<(lambda at main.cc:26:13), const int &>' requested here
    using invoke_result_t = typename invoke_result<_Fn, _Args...>::type;
    ^
main.cc:16:10: note: in instantiation of template type alias 'invoke_result_t' requested here
    std::invoke_result_t<F, const T &> apply(F &&f) const
         ^
main.cc:26:7: note: while substituting deduced template arguments into function template 'apply' [with F = (lambda at main.cc:26:13)]
    c.apply([](auto &&value) {
      ^
main.cc:26:23: note: variable 'value' declared const here
    c.apply([](auto &&value) {
               ~~~~~~~^~~~~

I'm not sure whether std::invoke_result_t is SFINAE-friendly, but I don't think that's the problem here since I've tried replacing it with a trailing return type, e.g.:

auto apply(F &&f) const -> decltype(std::declval<F>()(std::declval<const T &>()))

and got a similar error:

main.cc:27:9: error: cannot assign to variable 'value' with const-qualified type 'const int &&'
        ++value;
        ^ ~~~~~
main.cc:16:41: note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<const int &>' requested here
    auto apply(F &&f) const -> decltype(std::declval<F>()(std::declval<const T &>()))
                                        ^
main.cc:26:7: note: while substituting deduced template arguments into function template 'apply' [with F = (lambda at main.cc:26:13)]
    c.apply([](auto &&value) {
      ^
main.cc:26:23: note: variable 'value' declared const here
    c.apply([](auto &&value) {
               ~~~~~~~^~~~~

Questions:

  1. Why does this happen? More accurately, why is lambda's body instantiated during, what seems to be, an overload resolution?
  2. How do I work around it?
2

There are 2 answers

3
Brian Bi On BEST ANSWER

Lambdas have deduced return type, unless you specify the return type explicitly. Thus, std::invoke_result_t has to instantiate the body in order to determine the return type. This instantiation is not in the immediate context, and causes a hard error.

You can make your code compile by writing:

[](auto &&value) -> void { /* ... */ }

Here, the body of the lambda won't be instantiated until the body of apply, and you're in the clear.

1
Yakk - Adam Nevraumont On

So overload resolution is a bit dumb here.

It doesn't say "well, if non-const apply works, I'll never call const apply, so I won't bother considering it".

Instead, overload resolution evalutes every possible candidate. It then eliminates those that suffer substitution failure. Only then does it order the candidates and pick one.

So both of these have F substituted into them:

template <typename F>
std::invoke_result_t<F, T &> apply(F &&f)

template <typename F>
std::invoke_result_t<F, const T &> apply(F &&f) const

I removed their bodies.

Now, what happens when you pass the lambda type of F to these?

Well, lambdas have the equivalent of an auto return type. In order to find out the actual return type when passed something, the compiler must examine the body of the lambda.

And SFINAE does not work when examining the bodies of functions (or lambdas). This is intended to make the compiler's job easier (as SFINAE is very hard for compilers, having them have to compile arbitrary code and run into arbitrary errors and then roll it back was a huge barrier).

We can avoid instanting the body of the lambda with this:

[](auto &&value) -> void { /* ... */ }

after you do that, both overloads of apply:

template <typename F>
std::invoke_result_t<F, T &> apply(F &&f)

template <typename F>
std::invoke_result_t<F, const T &> apply(F &&f) const

can have the return value evaluated (it is just void) and we get:

template <typename F=$lambda$>
void apply(F &&f)

template <typename F=$lambda$>
void apply(F &&f) const

now, note that apply const still exists. If you call apply const, you'll get the hard error caused by instantiating that lambda body.

If you want the lambda itself to be SFINAE friendly, you should need do this:

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

[](auto &&value) RETURNS(++value)

note that this lambda is slightly different, as it returns a reference to the value. We can avoid this with:

[](auto &&value) RETURNS((void)++value)

and now the lambda is both SFINAE friendly and has the same behavior as your original lambda and your original program compiles as-is with this change.

The side effect of this is that non-const apply is now eliminated from overload resolution by SFINAE. Which makes it SFINAE friendly in turn.

There was a proposal to take RETURNS and rename it =>, but last I checked it was not accepted for .