Search code examples
c++templateslambdaperfect-forwarding

How to properly generically forward a parameter pack into a lambda?


I'm trying to forward a generic parameter pack from a base function, but trouble doing so, particularly if there are non-literal non-reference types in the type list

Considering the following example:

#include <utility>
#include <iostream>
#include <future>
#include <vector>

template < typename... Args >
class BaseTemplate {
 public:
    BaseTemplate() = default;
    virtual ~BaseTemplate() = default;

    virtual std::future< void > call(Args... args) {
        return std::async(std::launch::async, [this, &args... ] {
            // ..
            first(std::forward<Args>(args)...);
            second(std::forward<Args>(args)...);
            third(std::forward<Args>(args)...);
            // ...
        });
    }

 protected:
    virtual void first(Args...) { /* ... */ }
    virtual void second(Args...) = 0;
    virtual void third(Args...) { /* ... */ }
};


class SomeType {
 public:
    explicit SomeType(std::vector< float >* data) : ptr(data) { /* ... */ }
    ~SomeType() = default;
    // ...
    // protected:
    float member = 5.6;
    std::vector< float >* ptr;
};


class Derived1 : public BaseTemplate< int, float, SomeType > {
 public:
    using Base = BaseTemplate< int, float, SomeType >;

    Derived1() : Base() { /* ... */ }
    ~Derived1() = default;

 protected:
    void second(int, float, SomeType obj) override {
        std::cout << "Derived1::" << __func__ << " (" << obj.member << ")" << std::endl;
        printf("%p\n", obj.ptr);
        for (const auto& val : *(obj.ptr)) {
            std::cout << val << std::endl;
        }
    }
};


class Derived2 : public BaseTemplate< int, float, const SomeType& > {
 public:
    using Base = BaseTemplate< int, float, const SomeType& >;

    Derived2() : Base() { /* ... */ }
    ~Derived2() = default;

 protected:
    void second(int, float, const SomeType& obj) override {
        std::cout << "Derived2::" << __func__ << " (" << obj.member << ")" << std::endl;
        printf("%p\n", obj.ptr);
        for (const auto& val : *(obj.ptr)) {
            std::cout << val << std::endl;
        }
    }
};



int main(int argc, char const *argv[]) {
    std::vector< float > data {0, 1, 2, 3};

    SomeType obj(&data);
    Derived1 foo1;
    Derived2 foo2;

    // auto bar1 = foo1.call(1, 5.6, obj);  // Segmentation fault
    auto bar2 = foo2.call(1, 5.6, obj);  // OK

    // ...

    // bar1.wait();
    bar2.wait();

    return 0;
}    

Everything works as intended if SomeType is passed by reference, but segfaults if passed by value. What is the proper way to use std::forward<>() in order to account for both cases?


Solution

  • The problem is not with std::forward calls; the program exhibits undefined behavior before it gets to them. call takes some parameters by value, but the lambda inside always captures them by reference. Thus, it ends up holding references to local variables, which are destroyed as soon as call returns - but the lambda is called later, possibly on a different thread. At that point, all those references are dangling - not just the reference to SomeType, but also to int and to float.


    One possible solution could go like this:

    virtual std::future< void > call(Args... args) {
        std::tuple<BaseTemplate*, Args...> t{this, args...};
        return std::async(std::launch::async, [t] {
            // ..
            std::apply(&BaseTemplate::first, t);
            std::apply(&BaseTemplate::second, t);
            std::apply(&BaseTemplate::third, t);
            // ...
        });
    }
    

    We store arguments in a tuple - copies of those passed by value, references to those passed by reference. Then the lambda captures this tuple by value, and uses std::apply to pass its components along to the actual function being called.

    Demo