climb4
climb4

Reputation: 314

c++ - capturing perfectly forwarded vars in a lambda

I'm pretty new with c++ and am currently writing an inversion of control container for my first project, expanding on this blog post by adding registering on base classes and forwarding additional arguments to the constructor.

It works pretty good for now but when I instantiate a lambda multiple times the captured values seem to get overridden.

Example:

struct A{
    short a;
    explicit A(short a_) : a(a_) {}
};
struct IC{
    virtual unsigned C() = 0;
};
struct CImpl : public IC{
    explicit CImpl(unsigned c_) : IC(), c(c_) {}
    unsigned C() override{return c;}
private:
    unsigned c;
};
template<class T, typename...TArgs> 
std::function<T*()> AsMinimalAsItGets(TArgs&&...args)
{ 
    return [&args...]() mutable -> T* 
    { 
        return new T(std::forward<TArgs>(args)...); 
    }; 
} 
auto aFactory = AsMinimalAsItGets<A>(3);
auto cFactory = AsMinimalAsItGets<CImpl>(5);
auto aInst = aFactory();//aInst->a should be 3 but is 5
auto cInst = cFactory();//cInst->C() is 5

A gets instantiated with 5 instead of 3.

I tried this as solution, but it didn't fix the problem.

So how do I correctly capture the variables when instantiating the lambda? I need to capture in a way that enables me to use perfect forwarding in the lambda

Upvotes: 4

Views: 1448

Answers (1)

lubgr
lubgr

Reputation: 38287

Don't try to avoid a copy when you actually need one. In your case, you try to preserve the value categories of the arguments by std::forward them. But when you return a factory function std::function<T*()>, this closure must own the data it uses to perform the delayed construction. Otherwise, you end up with dangling references, as the arguments passed to AsMinimalAsItGets only outlive the scope of the function call.

The fix is easy:

template<class T, typename...TArgs> 
std::function<T*()> AsMinimalAsItGets(TArgs&&...args)
{ 
    return [args...]() mutable -> T* 
    //      ^^^^^^^ (1) Copy the arguments into the closure
    { 
        return new T(args...); 
        //           ^^^^^^^ (2) Pass them as is to the ctor
    }; 
} 

Note that as @HolyBlackCat pointed out, this does not perfectly forward the arguments into the lambda capture. As shown in this answer, in C++20, you can

return [...args = std::forward<TArgs>(args)]() mutable -> T* 
{ 
    return new T(args...); 
}; 

while in C++17, you need this workaround:

return [args = std::make_tuple(std::forward<TArgs>(args)...)]() mutable -> T* 
{ 
    return std::apply([](auto&&... args){ return new T(args...); },
        std::move(args));
}; 

Upvotes: 3

Related Questions