Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't a const mutable lambda with an auto& parameter be invoked?

#include <type_traits>  int main() {     auto f1 = [](auto&) mutable {};     static_assert(std::is_invocable_v<decltype(f1), int&>); // ok      auto const f2 = [](auto&) {};     static_assert(std::is_invocable_v<decltype(f2), int&>); // ok      auto const f3 = [](auto&) mutable {};     static_assert(std::is_invocable_v<decltype(f3), int&>); // failed } 

See demo

Why can't a const mutable lambda take a reference argument?

like image 633
xmllmx Avatar asked Dec 10 '21 19:12

xmllmx


2 Answers

There are two interesting things here.

First, a lambda's call operator (template) is const by default. If you provide mutable, then it is not const. The effect of mutable on a lambda is solely the opposite of the effect of trailing const in normal member functions (it does not affect lambda capture, etc.)

So if you look at this:

auto const f3 = [](auto&) mutable {}; static_assert(std::is_invocable_v<decltype(f3), int&>); // failed 

This is a const object, whose call operator template (because it's a generic lambda) is non-const. So you can't invoke it, for the same reason you can't invoke a non-const member function on a const object in any other context. See this other answer.

Second, it has been pointed out that, nevertheless, this works:

auto const f4 = [](int&) mutable {}; // changed auto& to int& static_assert(std::is_invocable_v<decltype(f4), int&>); // now ok 

This is not a compiler bug. Nor does it mean that what I just said was wrong. f4 still has a non-const call operator. Which you cannot invoke, because f4 is a const object.

However.

There's one other interesting aspect of lambdas that have no capture: they have a conversion function to a function pointer type. That is, we usually think about the lambda f4 as looking like this:

struct __unique_f4 {     auto operator()(int&) /* not const */ { } }; 

And, if that were the whole story, const __unique_f4 is indeed not invocable with int&. But it actually looks like this:

struct __unique_f4 {     auto operator()(int&) /* not const */ { }      // conversion function to the appropriate function     // pointer type     operator void(*)(int&)() const { /* ... */ } }; 

And there is this rule we have where when you invoke an object, like f(x), you not only consider f's call operators -- those members named operator() -- but you also consider any of f's surrogate call functions -- are there any function pointers that you can convert f to, to then invoke.

In this case, you can! You can convert f4 to a void(*)(int&) and that function pointer is invocable with int&.

But that still means that f4's call operator is not const, because you declared it mutable. And it doesn't say anything about whether you can have mutable lambdas take reference parameters.

like image 114
Barry Avatar answered Sep 20 '22 22:09

Barry


You get an error for this for the very same reason:

struct foo {     void operator()(){} };  int main() {     const foo f;     f(); } 

The error is:

<source>:7:5: error: no matching function for call to object of type 'const foo'     f();     ^ <source>:2:10: note: candidate function not viable: 'this' argument has type 'const foo', but method is not marked const     void operator()(){}          ^ 

Because you cannot call a non-const method on a const instance. Lambdas got the default constness right, so without mutable the operator() is const. With mutable the operator() is a non-const method that you cannot call on a const f3;

like image 33
463035818_is_not_a_number Avatar answered Sep 23 '22 22:09

463035818_is_not_a_number