2

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?

BZKN
  • 1,499
  • 2
  • 10
  • 25
joaocandre
  • 1,621
  • 4
  • 25
  • 42
  • To make it easier for other people to help you, you should pare this down to a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). For example, the `first` and `third` functions should be removed from the question since they are not relevant to the issue you are experiencing. – Brian Bi Jan 09 '22 at 22:47
  • The issue is not with forwarding the arguments to `first` et al - it's with capturing them in the lambda. You always capture by reference; whenever the parameter is passed by value, the lambda ends up holding dangling references. Not just for `SomeType`, for `int` and `float` too; they just happen not to obviously crash. – Igor Tandetnik Jan 09 '22 at 23:15

1 Answers1

4

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

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • It solves my issue, buta question regarding the solution anyway: you say *[...] the lambda inside [call()] always captures them by reference* but the tuple is itself captured by value - why though? What's the problem in capturing `args...` by value? – joaocandre Jan 09 '22 at 23:47
  • 1
    Well, `t` is itself a local variable. If I captured it by reference, I'd be right back where I started. So I copy the tuple - but, if any `Arg` is a reference type, then the tuple would store a reference for that component. That is, `t` is `std::tuple` for `Derived1`, but `std::tuple` for `Derived2`; it passes the reference along. – Igor Tandetnik Jan 09 '22 at 23:51
  • 1
    The problem with always capturing `args...` by value is that then even for `Derived2` you'd make a copy of `SomeType`. Which may not compile e.g. if `SomeType` is not copyable. – Igor Tandetnik Jan 09 '22 at 23:52
  • Also, I should have stated that in the question, but would prefer to stick to C++14 - are there any `std::apply` alternatives? – joaocandre Jan 10 '22 at 00:13
  • E.g. [this](https://stackoverflow.com/questions/687490/how-do-i-expand-a-tuple-into-variadic-template-functions-arguments) – Igor Tandetnik Jan 10 '22 at 00:36
  • Also, and sorry for adding another comment, would there be any advantage in changing the signature of `call()` to take forwarding references i.e. `call(Args&&...)`? – joaocandre Jan 11 '22 at 18:22
  • 1
    They won't be forwarding references - not unless you change the design and make `call` a function template, rather than a non-template method on a class template. The whole point of perfect forwarding is that whether the parameter is taken by lvalue or rvalue reference is deduced from the function call - but in your design, it's baked into `BaseTemplate` instantiation. – Igor Tandetnik Jan 12 '22 at 02:11