Search code examples
c++templateslambdac++20variadic

What is the difference between lambda capture [&args...] and [...args = std::forward<Args>(args)]


I'm writing a simple game with Entity Component System. One of my components is NativeScriptComponent. It contains the instance of my script. The idea is that I can create my NativeScriptComponent anytime and then Bind to it any class implementing Scriptable interface. After that my game's OnUpdate function will automatically instantiate all scripts in my game and will call their OnUpdate function.

My scripts can have their own, different constructors so I need to forward all the arguments to them when I bind my script.

Consider the following code:

#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;
}

1) What is the difference between option A and B

Instantiate = [&args...]() { return make_unique<T>(forward<Args>(args)...);  };

vs

Instantiate = [...args = forward<Args>(args)]() { return make_unique<T>(args...);  };
  1. Why can't I use forward<Args>(args) inside make_unique in option B?

  2. Are both A and B perfectly-forwarded?


Solution

  • For added completeness, there's more things you can do. As pointed out in the other answer:

    [&args...]() { return make_unique<T>(forward<Args>(args)...);  };
    

    This captures all the arguments by reference, which could lead to dangling. But those references are properly forwarded in the body.


    [...args=forward<Args>(args)]() { return make_unique<T>(args...);  };
    

    This forwards all the arguments into the lambda, args internally is a pack of values, not references. This passes them all as lvalues into make_unique.


    [...args=forward<Args>(args)]() mutable { return make_unique<T>(move(args)...);  };
    

    Given that, as above, args is a pack of values and we're creating a unique_ptr anyway, we probably should std::move them into make_unique. Note that the lambda has its own internal args here, so moving them does not affect any of the lvalues that were passed into this function.


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

    The most fun option. Before we'd either capture all the arguments by reference, or forward all the args (copying the lvalues and moving the rvalues). But there's a third option: we could capture the lvalues by reference and move the rvalues. We can do that by explicitly capturing a tuple and forwarding into it (note that for lvalues, the template parameter is T& and for rvalues it's T - so we get rvalues by value).

    Once we have the tuple, we apply on it internally - which gives us Args&&... back (note the && and that this is not a generic lambda, doesn't need to be).

    This is an improvement over the previous version in that we don't need to copy lvalues -- or perhaps it's worse because now we have more opportunity for dangling.

    A comparison of the four solutions:

    option can dangle? lvalue rvalue
    &args... both lvalues and rvalues 1 copy 1 move
    ...args=FWD(args) and args... no 2 copies 1 move, 1 copy
    ...args=FWD(args) and move(args)... no 1 copy, 1 move 2 moves
    args=tuple<Args...> lvalues 1 copy 2 moves