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...); };
Why can't I use
forward<Args>(args)
insidemake_unique
in option B?Are both A and B perfectly-forwarded?
CodePudding user response:
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 |
CodePudding user response:
A captures the arguments by reference and then perfectly forwards into make_unique
. Unfortunately by that point the references are (probably) dangling, so calling Instantiate
in your example has undefined behaviour.
B perfectly forwards the arguments into the data members of the closure object, then copies those data members into make_unique
. You can't forward here without marking the lambda mutable
, because the body of a lambda is a const
member function of the closure object.
A can't forward rvalues, the references to them dangle. B can't forward lvalues, it does a copy.
If you only want to instantiate one Scriptable
, then you can mark the lambda in B
as mutable
, and move in there. If you want to construct multiple Scriptable
s, you will have to do some copying.