Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't argument be forwarded inside lambda without mutable?

In the below program, when mutable is not used, the program fails to compile.

#include <iostream>
#include <queue>
#include <functional>

std::queue<std::function<void()>> q;

template<typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
    //q.emplace([=]() {                  // this fails
    q.emplace([=]() mutable {             //this works
        func(std::forward<Args>(args)...);
    });
}

int main()
{
    auto f1 = [](int a, int b) { std::cout << a << b << "\n"; };
    auto f2 = [](double a, double b) { std::cout << a << b << "\n";};
    enqueue(f1, 10, 20);
    enqueue(f2, 3.14, 2.14);
    return 0;
}

This is the compiler error

lmbfwd.cpp: In instantiation of ‘enqueue(T&&, Args&& ...)::<lambda()> [with T = main()::<lambda(int, int)>&; Args = {int, int}]’:
lmbfwd.cpp:11:27:   required from ‘struct enqueue(T&&, Args&& ...) [with T = main()::<lambda(int, int)>&; Args = {int, int}]::<lambda()>’
lmbfwd.cpp:10:2:   required from ‘void enqueue(T&&, Args&& ...) [with T = main()::<lambda(int, int)>&; Args = {int, int}]’
lmbfwd.cpp:18:20:   required from here
lmbfwd.cpp:11:26: error: no matching function for call to ‘forward<int>(const int&)’
   func(std::forward<Args>(args)...);

I am not able to understand why argument forwarding fails without mutable.

Besides, if I pass a lambda with string as argument, mutable is not required and program works.

#include <iostream>
#include <queue>
#include <functional>

std::queue<std::function<void()>> q;

template<typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
   //works without mutable
    q.emplace([=]() {
        func(std::forward<Args>(args)...);
    });
}
void dequeue()
{
    while (!q.empty()) {
        auto f = std::move(q.front());
        q.pop();
        f();
    }
}
int main()
{
    auto f3 = [](std::string s) { std::cout << s << "\n"; };
    enqueue(f3, "Hello");
    dequeue();
    return 0;
}

Why is mutable required in case of int double and not in case of string ? What is the difference between these two ?

like image 738
bornfree Avatar asked May 05 '19 14:05

bornfree


1 Answers

A non-mutable lambda generates a closure type with an implicit const qualifier on its operator() overload.

std::forward is a conditional move: it is equivalent to std::move when the provided template argument is not an lvalue reference. It is defined as follows:

template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type& t ) noexcept;

template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type&& t ) noexcept;

(See: https://en.cppreference.com/w/cpp/utility/forward).


Let's simplify your snippet to:

#include <utility>

template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
    [=] { func(std::forward<Args>(args)...); };
}

int main()
{
    enqueue([](int) {}, 10);
}

The error produced by clang++ 8.x is:

error: no matching function for call to 'forward'
    [=] { func(std::forward<Args>(args)...); };
               ^~~~~~~~~~~~~~~~~~
note: in instantiation of function template specialization 'enqueue<(lambda at wtf.cpp:11:13), int>' requested here
    enqueue([](int) {}, 10);
    ^
note: candidate function template not viable: 1st argument ('const int')
      would lose const qualifier
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    ^
note: candidate function template not viable: 1st argument ('const int')
      would lose const qualifier
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    ^

In the snippet above:

  • Args is int and refers to the type outside of the lambda.

  • args refers to the member of the closure synthesized via lambda capture, and is const due to the lack of mutable.

Therefore the std::forward invocation is...

std::forward<int>(/* `const int&` member of closure */)

...which doesn't match any existing overload of std::forward. There is a mismatch between the template argument provided to forward and its function argument type.

Adding mutable to the lambda makes args non-const, and a suitable forward overload is found (the first one, which moves its argument).


By using C++20 pack-expansion captures to "rewrite" the name of args, we can avoid the mismatch mentioned above, making the code compile even without mutable:

template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
    [func, ...xs = args] { func(std::forward<decltype(xs)>(xs)...); };
}

live example on godbolt.org


Why is mutable required in case of int double and not in case of string ? What is the difference between these two ?

This is a fun one - it works because you're not actually passing a std::string in your invocation:

enqueue(f3, "Hello");
//          ^~~~~~~
//          const char*

If you correctly match the type of the argument passed to enqueue to the one accepted by f3, it will stop working as expected (unless you use mutable or C++20 features):

enqueue(f3, std::string{"Hello"});
// Compile-time error.

To explain why the version with const char* works, let's again look at a simplified example:

template <typename T>
void enqueue(T&& func, const char (&arg)[6])
{
    [=] { func(std::forward<const char*>(arg)); };
}

int main()
{
    enqueue([](std::string) {}, "Hello");
}

Args is deduced as const char(&)[6]. There is a matching forward overload:

template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type&& t ) noexcept;

After substitution:

template< class T >
constexpr const char*&& forward( const char*&& t ) noexcept;

This simply returns t, which is then used to construct the std::string.

like image 187
Vittorio Romeo Avatar answered Sep 28 '22 11:09

Vittorio Romeo