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 ?
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 ofint double
and not in case ofstring
? 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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With