C++ Lambdas How to Capture Variadic Parameter Pack from the Upper Scope

c++ lambdas how to capture variadic parameter pack from the upper scope

Perfect capture in C++20

template <typename ... Args>
auto f(Args&& ... args){
return [... args = std::forward<Args>(args)]{
// use args
};
}

C++17 and C++14 workaround

In C++17 we can use a workaround with tuples:

template <typename ... Args>
auto f(Args&& ... args){
return [args = std::make_tuple(std::forward<Args>(args) ...)]()mutable{
return std::apply([](auto&& ... args){
// use args
}, std::move(args));
};
}

Unfortunately std::apply is C++17, in C++14 you can implement it yourself or do something similar with boost::hana:

namespace hana = boost::hana;

template <typename ... Args>
auto f(Args&& ... args){
return [args = hana::make_tuple(std::forward<Args>(args) ...)]()mutable{
return hana::unpack(std::move(args), [](auto&& ... args){
// use args
});
};
}

It might be usefull to simplify the workaround by a function capture_call:

#include <tuple>

// Capture args and add them as additional arguments
template <typename Lambda, typename ... Args>
auto capture_call(Lambda&& lambda, Args&& ... args){
return [
lambda = std::forward<Lambda>(lambda),
capture_args = std::make_tuple(std::forward<Args>(args) ...)
](auto&& ... original_args)mutable{
return std::apply([&lambda](auto&& ... args){
lambda(std::forward<decltype(args)>(args) ...);
}, std::tuple_cat(
std::forward_as_tuple(original_args ...),
std::apply([](auto&& ... args){
return std::forward_as_tuple< Args ... >(
std::move(args) ...);
}, std::move(capture_args))
));
};
}

Use it like this:

#include <iostream>

// returns a callable object without parameters
template <typename ... Args>
auto f1(Args&& ... args){
return capture_call([](auto&& ... args){
// args are perfect captured here
// print captured args via C++17 fold expression
(std::cout << ... << args) << '\n';
}, std::forward<Args>(args) ...);
}

// returns a callable object with two int parameters
template <typename ... Args>
auto f2(Args&& ... args){
return capture_call([](int param1, int param2, auto&& ... args){
// args are perfect captured here
std::cout << param1 << param2;
(std::cout << ... << args) << '\n';
}, std::forward<Args>(args) ...);
}

int main(){
f1(1, 2, 3)(); // Call lambda without arguments
f2(3, 4, 5)(1, 2); // Call lambda with 2 int arguments
}

Here is a C++14 implementation of capture_call:

#include <tuple>

// Implementation detail of a simplified std::apply from C++17
template < typename F, typename Tuple, std::size_t ... I >
constexpr decltype(auto)
apply_impl(F&& f, Tuple&& t, std::index_sequence< I ... >){
return static_cast< F&& >(f)(std::get< I >(static_cast< Tuple&& >(t)) ...);
}

// Implementation of a simplified std::apply from C++17
template < typename F, typename Tuple >
constexpr decltype(auto) apply(F&& f, Tuple&& t){
return apply_impl(
static_cast< F&& >(f), static_cast< Tuple&& >(t),
std::make_index_sequence< std::tuple_size<
std::remove_reference_t< Tuple > >::value >{});
}

// Capture args and add them as additional arguments
template <typename Lambda, typename ... Args>
auto capture_call(Lambda&& lambda, Args&& ... args){
return [
lambda = std::forward<Lambda>(lambda),
capture_args = std::make_tuple(std::forward<Args>(args) ...)
](auto&& ... original_args)mutable{
return ::apply([&lambda](auto&& ... args){
lambda(std::forward<decltype(args)>(args) ...);
}, std::tuple_cat(
std::forward_as_tuple(original_args ...),
::apply([](auto&& ... args){
return std::forward_as_tuple< Args ... >(
std::move(args) ...);
}, std::move(capture_args))
));
};
}

capture_call captures variables by value. The perfect means that the move constructor is used if possible. Here is a C++17 code example for better understanding:

#include <tuple>
#include <iostream>
#include <boost/type_index.hpp>

// Capture args and add them as additional arguments
template <typename Lambda, typename ... Args>
auto capture_call(Lambda&& lambda, Args&& ... args){
return [
lambda = std::forward<Lambda>(lambda),
capture_args = std::make_tuple(std::forward<Args>(args) ...)
](auto&& ... original_args)mutable{
return std::apply([&lambda](auto&& ... args){
lambda(std::forward<decltype(args)>(args) ...);
}, std::tuple_cat(
std::forward_as_tuple(original_args ...),
std::apply([](auto&& ... args){
return std::forward_as_tuple< Args ... >(
std::move(args) ...);
}, std::move(capture_args))
));
};
}

struct A{
A(){
std::cout << " A::A()\n";
}

A(A const&){
std::cout << " A::A(A const&)\n";
}

A(A&&){
std::cout << " A::A(A&&)\n";
}

~A(){
std::cout << " A::~A()\n";
}
};

int main(){
using boost::typeindex::type_id_with_cvr;

A a;
std::cout << "create object end\n\n";

[b = a]{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "\n";
}();
std::cout << "value capture end\n\n";

[&b = a]{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "\n";
}();
std::cout << "reference capture end\n\n";

[b = std::move(a)]{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "\n";
}();
std::cout << "perfect capture end\n\n";

[b = std::move(a)]()mutable{
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "\n";
}();
std::cout << "perfect capture mutable lambda end\n\n";

capture_call([](auto&& b){
std::cout << " type of the capture value: "
<< type_id_with_cvr<decltype(b)>().pretty_name()
<< "\n";
}, std::move(a))();
std::cout << "capture_call perfect capture end\n\n";
}

Output:

  A::A()
create object end

A::A(A const&)
type of the capture value: A const
A::~A()
value capture end

type of the capture value: A&
reference capture end

A::A(A&&)
type of the capture value: A const
A::~A()
perfect capture end

A::A(A&&)
type of the capture value: A
A::~A()
perfect capture mutable lambda end

A::A(A&&)
type of the capture value: A&&
A::~A()
capture_call perfect capture end

A::~A()

The type of the capture value contains && in the capture_call version because we have to access the value in the internal tuple via reference, while a language supported capture supports direct access to the value.

Move (or copy) capture variadic template arguments into lambda

To be clear, I don't want to perfectly capture the arguments as in c++
lambdas how to capture variadic parameter pack from the upper scope I
want to move them if possible

Just using the same form:

auto lambda = [fn, ...args = std::move(args)]() mutable {
(*fn)(std::move(args)...);
};

In C++17, you could do:

auto lambda = [fn, args = std::tuple(std::move(args)...)]() mutable {
std::apply([fn](auto&&... args) { (*fn)( std::move(args)...); },
std::move(args));
};

properly forward a parameter pack in a lambda without discarding qualifiers

What happens is, here [task, args...] the references are lost; task, args... are captured by value, which, in a non-mutable lambda additionally become const, which subsequently can't bind to non-const references in the call to [&](class_a & a, class_b & b).

You can capture the references by reference. Don't worry about their lifetime - the references are collapsed, and the original ones will be captured (source).

template<typename F>
auto push_fct(F&& task) -> void {
// Do things
fcts.emplace(std::forward<F>(task));
}

template<typename F, typename... A>
auto push_fct(F& task, A&... args) -> void {
push_fct([&task, &args...]{ task(args...); });
}

How to create a variadic generic lambda?

Syntax

How do you create a variadic generic lambda ?

You can create a variadic generic lambda with the following syntax:

auto variadic_generic_lambda = [] (auto... param) {};

Basically you just add ... between auto (possibly ref qualified) and your parameter pack name.

So typically using universal references would give:

auto variadic_generic_lambda = [] (auto&&... param) {};

Usage

How do you use the parameters ?

You should consider the variadic generic parameter as having a template parameter pack type, because it is the case. This more or less implies that most if not all usage of those parameters will require templates one way or the other.

Here is a typical example:

#include <iostream>

void print(void)
{
}

template <typename First, typename ...Rest>
void print(const First& first, Rest&&... Args)
{
std::cout << first << std::endl;
print(Args...);
}

int main(void)
{
auto variadic_generic_lambda = [] (auto... param)
{
print(param...);
};

variadic_generic_lambda(42, "lol", 4.3);
}

Storage

How do you store a variadic generic lambda ?

You can either use auto to store a lambda in a variable of its own type, or you can store it in a std::function but you will only be able to call it with the fixed signature you gave to that std::function :

auto variadic_generic_lambda = [] (auto... param) {};

std::function<void(int, int)> func = variadic_generic_lambda;

func(42, 42); // Compiles

func("lol"); // Doesn't compile

What about collections of variadic generic lambdas ?

Since every lambda has a different type you cannot store their direct type in the usual homogeneous containers of the STL. The way it is done with non generic lambdas is to store them in a corresponding std::function which will have a fixed signature call and that won't restrain anything since your lambda is not generic in the first place and can only be invoked that way:

auto non_generic_lambda_1 = [] (int, char) {};
auto non_generic_lambda_2 = [] (int, char) {};

std::vector<std::function<void(int, char)>> vec;

vec.push_back(non_generic_lambda_1);
vec.push_back(non_generic_lambda_2);

As explained in the first part of this storage section if you can restrain yourself to a given fixed call signature then you can do the same with variadic generic lambdas.

If you can't you will need some form of heterogenous container like:

  • std::vector<boost::variant>
  • std::vector<boost::any>
  • boost::fusion::vector

See this question for an example of heterogenous container.

What else ?

For more general informations on lambdas and for details on the members generated and how to use the parameters within the lambda see:

  • http://en.cppreference.com/w/cpp/language/lambda
  • How does generic lambda work in C++14?
  • How to call a function on all variadic template args?
  • What is the easiest way to print a variadic parameter pack using std::ostream?

c++ - capturing perfectly forwarded vars in a lambda

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

Save template type parameter pack for later use

tl;dr:

  • Completely abstracting away the signature of the function AND still calling it in a type-safe way is impossible in C++
  • A type-based event system could be a good alternative


1. Why it's impossible to do what you're asking for

1.1 How Type-Erasure works

Type-erasure is fundamentally based on polymorphism. By defining a set of methods that all objects we want to store have in common (the interface) we don't need to know the actual type we're dealing with.

There is no way to do type-erasure without involving polymorphism.

For example, a very crude implementation of std::function could look like this:


template<class RetVal, class... Args>
class function {
public:
template<class U>
function(U u) : ptr(new impl<U>(u)) {}
~function() { delete ptr; }

RetVal operator()(Args... args) {
return ptr->call(args...);
}

private:
struct base {
virtual ~base() = default;
virtual RetVal call(Args... args) = 0;
};

template<class T>
struct impl : base {
impl(T t): t(t) {}

RetVal call(Args... args) override {
return t(args...);
}

private:
T t;
};

base* ptr;
};

template<class RetVal, class... Args>
class function<RetVal(Args...)> : public function<RetVal, Args...> {};

godbolt example

This is how std::function accomplishes to store any function object that is compatible with it's signature - it declares an interface (base) that will be used by all function objects (impl).

The interface only consists of 2 functions in this case:

  • The destructor (we need to know how to properly cleanup the function object)
  • The call() function (for invoking the actual function)

Sidenote 1: A real std::function implementation would need a couple more interface functions, e.g. for copying / moving the callable

Sidenote 2: Your existing implementation has a small bug: struct base MUST have a virtual destructor, otherwise the destructor of struct callable would never be called, resulting in undefined behaviour.

1.2 How your callable would need to work

What you want is an object that completely erases both the function object AND the parameters that you pass.

But what should your interface then look like?

struct base {
virtual ~base() = default;

virtual ??? call(???); // how should this work?
};

This is the underlying problem you're facing - it's impossible to define an interface for your callable - because you don't know what the arguments are gonna be.

This is what @Yakk - Adam Nevraumont implied with "non-uniform" objects - there is no definition of call() that can handle all potential function types.

1.3 Options

So at that point you basically have two options:

  • Don't erase the function type (like @Yakk - Adam Nevraumont suggested)
  • Sacrifice compile-time type safety and maintainability by creating an interface that can deal with arbitrary function types

The latter option is what your code currently uses - either the function parameters match or your code has undefined behaviour.

A few other ways to implement it that don't rely on undefined behaviour could be:

  • Add an interface function for each possible argument combination
    struct base {
    /* ... */

    // All possible ways a `callable` could potentially be invoked
    virtual void call(int val0) { throw std::exception("invalid call"); };
    virtual void call(std::string val0) { throw std::exception("invalid call"); };
    virtual void call(const char* val0) { throw std::exception("invalid call"); };
    virtual void call(int val0, std::string val1) { throw std::exception("invalid call"); };
    virtual void call(int val0, const char* val1) { throw std::exception("invalid call"); };
    // etc...
    }

    // then implement the ones that are sensible
    struct callable<std::string> : public base {
    /* ... */
    void call(std::string val0) override { /* ... */ }
    void call(const char* val0) override { /* ... */ }
    }
    This obviously gets out of hand rather quickly.
  • "Accept anything" interface
    struct base {
    /* ... */

    virtual void call(std::any* arr, int length);
    };

    // then implement the ones that are sensible
    struct callable<std::string> : public base {
    /* ... */
    void call(std::any* arr, int length) override {
    if(length != 1) throw new std::exception("invalid arg count");
    // will throw if first argument is not a std::string
    std::string& value = std::any_cast<std::string&>(arr[0]);
    /* ... */
    }
    };
    A bit better, but still looses compile-time type safety.

1.4 Conclusion

  • Compile-time type-safety with type-erasure is only possible if there is an uniform interface for all possible objects.
  • It is technically possible to type-erase non-uniform objects, but if you do that you'll loose compile-time type-safety (and need to do those checks at runtime instead)


2. Another Approach: Type-Based Event System

I'd like to propose a different way to handle the events that allows you to have arbitrary events without having to hard-code them into your Events class.

2.1 Basic Functionality

The main idea of this implementation is to have a class for each event you'd want to have that contains the parameters for the given event, e.g.:

struct AppClosingEvent {
const std::string message;
const int exitCode;
};

struct BananaPeeledEvent {
const std::shared_ptr<Banana> banana;
const std::shared_ptr<Person> peeler;
};

// etc...

This would then allow you to use the type of the event struct as a key for your event listeners.

A very simple implementation of this event system could look like this: (ignoring unregistration for now)

class EventBus {
private:
using EventMap = std::multimap<std::type_index, std::function<void(void*)>>;

// Adds an event listener for a specific event
template<class EvtCls, class Callable>
requires std::is_invocable_v<Callable, EvtCls&>
inline void Register(Callable&& callable) {
callbacks.emplace(
typeid(EvtCls),
[cb = std::forward<Callable>(callable)](void* evt) {
cb(*static_cast<EvtCls*>(evt));
}
);
}

// Broadcasts the given event to all registered event listeners
template<class EvtCls>
inline void Broadcast(EvtCls& evt) {
auto [first, last] = callbacks.equal_range(typeid(EvtCls));
for(auto it = first; it != last; ++it)
(it->second)(&evt);
}

private:
EventMap callbacks;
};
  • Register() takes a callable object that needs to be invocable with the given event type. Then it type-erases the callable so we can store it as a std::function<void(void*>
  • Broadcast(evt) looks up all event listeners that are registered based on the type of the event object and calls them.

Example Usage would look like this:

EventBus bus;

bus.Register<AppClosingEvent>([](AppClosingEvent& evt) {
std::cout << "App is closing! Message: " << evt.message << std::endl;
});

bus.Register<BananaPeeledEvent>([](BananaPeeledEvent& evt) {
// TODO: Handle banana peeling
});

AppClosingEvent evt{"Shutting down", 0};
bus.Broadcast(evt);

By using the type of the event as the key both Register() and Broadcast() are completely type-safe - it's impossible to register a function with incompatible function arguments.

Additionally the EventBus class doesn't need to know anything about the events it'll handle - adding a new event is as simple as defining a new class with the members you need for your event.

2.2 Adding the ability to unregister an event listener

I chose to use a multimap in this case because they guarantee to not invalidate iterators, unless the element the iterator points to itself gets removed from the multimap - which allows us to use a multimap iterator as the registration token for the event handler.

Full implementation: godbolt example

/*
EventBus - allows you to register listeners for arbitrary events via `.Register()`
and then later invoke all registered listeners for an event type with `.Broadcast()`.
Events are passed as lvalues, to allow event handlers to interact with the event, if required.
*/
class EventBus {
private:
using EventMap = std::multimap<std::type_index, std::function<void(void*)>>;
public:
/*
Represents a registered event handler on the EventBus.
Works a lot like std::unique_ptr (it is movable but not copyable)
Will automatically unregister the associated event handler on destruction.
You can call `.disconnect()` to unregister the event handler manually.
*/
class Connection {
private:
friend class EventBus;
// Internal constructor used by EventBus::Register
inline Connection(EventBus& bus, EventMap::iterator it) : bus(&bus), it(it) { }

public:
inline Connection() : bus(nullptr), it() {}
// not copyable
inline Connection(Connection const&) = delete;
inline Connection& operator=(Connection const&) = delete;

// but movable
inline Connection(Connection&& other)
: bus(other.bus), it(other.it) {
other.detach();
}

inline Connection& operator=(Connection&& other) {
if(this != &other) {
disconnect();
bus = other.bus;
it = other.it;
other.detach();
}

return *this;
}

inline ~Connection() {
disconnect();
}

// Allows to manually unregister the associated event handler
inline void disconnect() {
if(bus) {
bus->callbacks.erase(it);
detach();
}
}

// Releases the associated event handler without unregistering
// Warning: After calling this method it becomes impossible to unregister
// the associated event handler.
inline void detach() {
bus = nullptr;
it = {};
}

private:
EventBus* bus;
EventMap::iterator it;
};

// Adds an event listener for a specific event
template<class EvtCls, class Callable>
requires std::is_invocable_v<Callable, EvtCls&>
inline Connection Register(Callable&& callable) {
auto it = callbacks.emplace(
typeid(EvtCls),
[cb = std::forward<Callable>(callable)](void* evt) {
cb(*static_cast<EvtCls*>(evt));
}
);

return { *this, it };
}

// Broadcasts the given event to all registered event listeners
template<class EvtCls>
inline void Broadcast(EvtCls& evt) {
auto [first, last] = callbacks.equal_range(typeid(EvtCls));
for(auto it = first; it != last;)
(it++)->second(&evt);
}

private:
EventMap callbacks;
};

With this you can easily register listeners and unregister them later (e.g. if the class they're bound to gets destructed)

Example:

struct DispenseNachosEvent {};
struct DispenseCheeseEvent {};

class NachoMachine {
public:
NachoMachine(EventBus& bus) {
// register using std::bind
nachoEvent = bus.Register<DispenseNachosEvent>(
std::bind(
&NachoMachine::OnDispenseNachos,
this,
std::placeholders::_1
)
);

// register with lambda
cheeseEvent = bus.Register<DispenseCheeseEvent>(
[&](DispenseCheeseEvent& evt) {
OnDispenseCheese(evt);
}
);
}

// Default destructor will automatically
// disconnect both event listeners

private:
void OnDispenseNachos(DispenseNachosEvent&) {
std::cout << "Dispensing Nachos..." << std::endl;
}

void OnDispenseCheese(DispenseCheeseEvent&) {
std::cout << "Dispensing Cheese..." << std::endl;
}

private:
EventBus::Connection nachoEvent;
EventBus::Connection cheeseEvent;
};

2.3 Other benefits

  • If you want you can also allow the event handlers to modify the event object - e.g. cancel it - which allows you to return state to the piece of code that called Broadcast()
    Example:
    struct CancelableExampleEvent {
    inline void Cancel() { isCancelled = true; }
    inline bool IsCancelled() { return isCancelled; }

    CancelableExampleEvent(std::string message) : message(message) {}

    const std::string message;
    private:
    bool isCancelled = false;
    };

    // Usage:
    CancelableExampleEvent evt;
    bus.Broadcast(evt);
    if(!evt.IsCancelled()) {
    // TODO: Do something
    }
  • Event Handlers can remove themselves - this is usually tricky to implement due to iterators being invalidated, but with multimaps it's rather easy to implement:
    template<class EvtCls>
    inline void Broadcast(EvtCls& evt) {
    auto [first, last] = callbacks.equal_range(typeid(EvtCls));
    for(auto it = first; it != last;)
    (it++)->second(&evt);
    }
    By incrementing it before calling the function we make sure that it remains valid, even if the event handler chooses to unregister itself as part of its callback.
    e.g. this would work:
    EventBus::Connection con;
    con = bus.Register<SomeEvent>([&con](SomeEvent&){
    std::cout << "Received event once!" << std::endl;
    con.disconnect();
    });

2.4 Try it online!

Here's a godbolt that contains the entire code of this post to try it out.

How to create a variadic struct that takes a variadic invocable of the same arity as the ctor parameter?

std::tuple (with std::apply) might help:

template <std::derived_from<IUnknown>... Ts>
struct AutoManagedCOMObj
{
std::tuple<Ts*...> tuple_ptrs;

template<std::invocable<Ts**...> Invocable>
AutoManagedCOMObj(Invocable initializer)
{
HRESULT hr = std::apply([&](auto*&... ptrs){ return initializer(&ptrs...);},
tuple_ptrs);
if (FAILED(hr)) exit(hr);
}
~AutoManagedCOMObj()
{
std::apply([](auto*... ptrs){ (ptrs->Release(), ...); }, tuple_ptrs);
}
};


Related Topics



Leave a reply



Submit