以下のプログラムでは、mutable
が使用されていない場合、プログラムはコンパイルに失敗します。
#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;
}
これはコンパイラエラーです
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)...);
mutable
なしで引数の転送が失敗する理由を理解できません。
さらに、引数として文字列を含むラムダを渡す場合、mutable
は必要なく、プログラムは機能します。
#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;
}
string
ではなくint double
の場合に可変が必要なのはなぜですか?これら2つの違いは何ですか?
非mutable
ラムダは、そのoperator()
オーバーロードに暗黙的なconst
修飾子を持つクロージャタイプを生成します。
_std::forward
_は条件付きの移動です。指定されたテンプレート引数が左辺値参照ではない場合、_std::move
_と同等です。次のように定義されます。
_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;
_
(参照: https://en.cppreference.com/w/cpp/utility/forward )。
スニペットを単純化してみましょう:
_#include <utility>
template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
[=] { func(std::forward<Args>(args)...); };
}
int main()
{
enqueue([](int) {}, 10);
}
_
_clang++ 8.x
_によって生成されるエラーは次のとおりです。
_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 ^
_
上記のスニペットでは:
Args
はint
であり、ラムダ外の型を参照します。
args
は、ラムダキャプチャを介して合成されたクロージャのメンバーを指し、const
がないためmutable
です。
したがって、_std::forward
_の呼び出しは...
_std::forward<int>(/* `const int&` member of closure */)
_
...これは_std::forward
_の既存のオーバーロードと一致しません。 forward
に提供されたテンプレート引数とその関数引数タイプの間に不一致があります。
ラムダにmutable
を追加すると、args
が非const
になり、適切なforward
オーバーロードが見つかります(最初の引数は引数を移動します)。
C++ 20 pack-expansionキャプチャを使用してargs
の名前を「書き換え」ることにより、上記の不一致を回避し、mutable
がなくてもコードをコンパイルできるようにします。
_template <typename T, typename... Args>
void enqueue(T&& func, Args&&... args)
{
[func, ...xs = args] { func(std::forward<decltype(xs)>(xs)...); };
}
_
mutable
ではなく_int double
_の場合にstring
が必要なのはなぜですか?これら2つの違いは何ですか?
これは楽しいものです-呼び出しで実際に_std::string
_を渡していないので機能します:
_enqueue(f3, "Hello");
// ^~~~~~~
// const char*
_
enqueue
に渡された引数のタイプを_f3
_で受け入れられる引数のタイプと正しく一致させると、期待どおりに機能しなくなります(mutable
またはC++ 20機能を使用する場合を除く) ):
_enqueue(f3, std::string{"Hello"});
// Compile-time error.
_
_const char*
_を使用したバージョンが機能する理由を説明するために、簡単な例をもう一度見てみましょう。
_template <typename T>
void enqueue(T&& func, const char (&arg)[6])
{
[=] { func(std::forward<const char*>(arg)); };
}
int main()
{
enqueue([](std::string) {}, "Hello");
}
_
Args
はconst char(&)[6]
として推定されます。一致するforward
オーバーロードがあります:
_template< class T >
constexpr T&& forward( typename std::remove_reference<T>::type&& t ) noexcept;
_
置換後:
_template< class T >
constexpr const char*&& forward( const char*&& t ) noexcept;
_
これは単にt
を返し、それを使用して_std::string
_を構築します。