web-dev-qa-db-ja.com

なぜ可変引数なしで引数をラムダ内に転送できないのですか?

以下のプログラムでは、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つの違いは何ですか?

16
bornfree

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
    ^
_

上記のスニペットでは:

  • Argsintであり、ラムダ外の型を参照します。

  • 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)...); };
}
_

godbolt.orgのライブ例


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");
}
_

Argsconst 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_を構築します。

19
Vittorio Romeo