lambda capture [&args...] 和 [...args = std::forward<Args>(args)] 之间有什么区别

Kyr*_*iet 7 c++ lambda templates variadic c++20

我正在使用实体组件系统编写一个简单的游戏。我的组件之一是NativeScriptComponent. 它包含我的脚本的实例。我的想法是,我可以NativeScriptComponent随时创建我的Bind任何类实现Scriptable接口。之后我的游戏的 OnUpdate 函数将自动实例化游戏中的所有脚本并调用它们的 OnUpdate 函数。

我的脚本可以有自己的不同构造函数,因此在绑定脚本时我需要将所有参数转发给它们。

考虑以下代码:

#include <iostream>
#include <memory>
#include <functional>

using namespace std;

struct Scriptable
{
    virtual void OnUpdate() {};
};
struct MyScript : Scriptable 
{ 
    MyScript(int n) : value(n) {}
    void OnUpdate() override {cout << value;} 
    int value;
};

struct NativeScriptComponent
{
    unique_ptr<Scriptable> Instance;
    function<unique_ptr<Scriptable>()> Instantiate;

    template<typename T, typename ... Args>
    void Bind(Args&&... args)
    {
        // (A)
        Instantiate = [&args...]() { return make_unique<T>(forward<Args>(args)...);  };
        // (B) since C++20
        Instantiate = [...args = forward<Args>(args)]() { return make_unique<T>(args...);  };
    }
};

int main()
{
    NativeScriptComponent nsc;
    nsc.Bind<MyScript>(5);
    
    // [..] Later in my game's OnUpdate function:
    if (!nsc.Instance)
        nsc.Instance = nsc.Instantiate();
    nsc.Instance->OnUpdate(); // prints: 5

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

1)选项A和B有什么区别

Instantiate = [&args...]() { return make_unique<T>(forward<Args>(args)...);  };
Run Code Online (Sandbox Code Playgroud)

Instantiate = [...args = forward<Args>(args)]() { return make_unique<T>(args...);  };
Run Code Online (Sandbox Code Playgroud)
  1. 为什么我不能在选项 B 中使用forward<Args>(args)inside ?make_unique

  2. A 和 B 都完美转发吗?

Bar*_*rry 7

为了增加完整性,您可以执行更多操作。正如另一个答案中指出的:

[&args...]() { return make_unique<T>(forward<Args>(args)...);  };
Run Code Online (Sandbox Code Playgroud)

这通过引用捕获所有参数,这可能导致悬空。但这些引用在正文中正确转发。


[...args=forward<Args>(args)]() { return make_unique<T>(args...);  };
Run Code Online (Sandbox Code Playgroud)

这会将所有参数转发到 lambda 中,args内部是一组,而不是引用。这会将它们作为左值全部传递到make_unique.


[...args=forward<Args>(args)]() mutable { return make_unique<T>(move(args)...);  };
Run Code Online (Sandbox Code Playgroud)

如上所述,考虑到这args是一组值,并且我们正在创建一个unique_ptr值,我们可能应该将std::move它们放入make_unique. 请注意,lambdaargs在这里有其自己的内部,因此移动它们不会影响传递到该函数中的任何左值。


[args=tuple<Args...>(forward<Args>(args)...)]() mutable {
    return std::apply([](Args&&... args){
        return std::make_unique<T>(forward<Args>(args)...);
    }, std::move(args));
};
Run Code Online (Sandbox Code Playgroud)

最有趣的选择。在我们通过引用捕获所有参数或转发所有参数(复制左值并移动右值)之前。但还有第三种选择:我们可以通过引用捕获左值并移动右值。我们可以通过显式捕获一个元组并转发到其中来做到这一点(请注意,对于左值,模板参数是,T&对于右值,它是T- 所以我们通过值获取右值)。

一旦我们有了元组,我们就apply在内部使用它 - 这会给我们Args&&...返回(注意&&和 这不是一个通用的 lambda,也不需要是)。

这是对以前版本的改进,因为我们不需要复制左值 - 或者可能更糟糕,因为现在我们有更多的机会进行悬空。

四种解决方案的比较:

选项 可以悬挂吗? 左值 右值
&args... 左值和右值 1份 1 步
...args=FWD(args)args... 2份 1 次移动,1 次复制
...args=FWD(args)move(args)... 1份副本,1次移动 2 步
args=tuple<Args...> 左值 1份 2 步