0

I'm trying to create an "initializer" function in C++17 that curries call to the constructor of the object so it can deferred to later time, when the rest of the variadic arguments is known.

I've followed the example at c++ lambdas how to capture variadic parameter pack from the upper scope which compiles, but does not handle passing values by references.

The full running example can be found at https://repl.it/repls/IroncladToughExabyte

I copy the code here as well, so that it will be captured for future reference:

#include <iostream>
#include <functional>
#include <memory>
#include <utility>
#include <limits>

using std::cout;
using std::endl;
using std::function;
using std::move;
using std::unique_ptr;
using std::make_unique;

typedef uint8_t MyParam;

struct SomethingBig {
    int a;
    int b;
};

class GamePlayingAlgorithm {
 public:
    const MyParam &a_;
    const MyParam b_;

    GamePlayingAlgorithm(const MyParam &a, MyParam b)
        : a_(a), b_(b) {};
    virtual void play() = 0;
};

class PassByRef: public GamePlayingAlgorithm {
 public:
    SomethingBig &stuff_;
    inline explicit PassByRef(const MyParam &a, MyParam b, SomethingBig &stuff)
        : GamePlayingAlgorithm(a, b), stuff_(stuff) {}
    void play() override {
        cout << stuff_.a << endl;
    }
};

typedef function<unique_ptr<GamePlayingAlgorithm>(const MyParam &, MyParam)>
    PreparedAlgorithm;

template<typename...>
struct pack {};

template<typename T, typename Tup, typename... TArgs, std::size_t... Is>
std::unique_ptr<T>
helper(const MyParam &domain, MyParam pl, Tup &&tup, pack<TArgs...>, std::index_sequence<Is...>) {
    return std::make_unique<T>(domain, pl, static_cast<TArgs>(std::get<Is>(tup))...);
}

// use tuple packing/unpacking
template<typename T, typename... Args>
PreparedAlgorithm createInitializer1(Args &&... args) {
    return [tup = std::make_tuple(std::forward<Args>(args)...)](const MyParam &domain,
                                                                MyParam pl) mutable {
        return helper<T>(domain,
                         pl,
                         std::move(tup),
                         pack<Args &&...>{},
                         std::index_sequence_for<Args...>{});
    };
}

// use simple forwarding with reference in lambda
template<typename T, typename... Args>
PreparedAlgorithm createInitializer2(Args &&... args) {
    return [&](const MyParam &domain, MyParam pl) -> unique_ptr<GamePlayingAlgorithm> {
        return make_unique<T>(domain, pl, std::forward<Args>(args) ...);
    };
}

int main() {
    SomethingBig stuffRef1 = {100, 200};
    PreparedAlgorithm preparedRef1 = createInitializer1<PassByRef>(stuffRef1);
    auto algRef1 = preparedRef1(1, 1);
    cout << "algRef1: ";
    algRef1->play();
    stuffRef1.a = 500;
    cout << "algRef1 (after update): ";
    algRef1->play();


    SomethingBig stuffRef2 = {100, 200};
    PreparedAlgorithm preparedRef2 = createInitializer2<PassByRef>(stuffRef2);
    auto algRef2 = preparedRef2(1, 1);
    cout << "algRef2: ";
    algRef2->play();
    stuffRef2.a = 500;
    cout << "algRef2 (after update): ";
    algRef2->play();
}

The output of running this is:

algRef1: 100
algRef1 (after update): 100
algRef2: 100
algRef2 (after update): 500

The problem is that algRef1 is not updated.

algRef2 is updated, but it is using an undefined operation, and in fact it does break in my larger source code.

The question is -- how the implementation of createInitializer1 or createInitializer2 can be changed so that they are properly defined?

Thanks!

jolly
  • 311
  • 1
  • 2
  • 11

1 Answers1

0

When I add the missing virtual destructor to GamePlayingAlgorithm, the undefined behaviour starts causing much more trouble.

To debug problems of this kind, replace all references with pointers, they don't get accidentally converted to temporary copies and cause easy compilation errors instead. The real return type can be checked by making the function called templated and deliberately causing an error in it, so that the compiler would print a backtrace with deduced types.

In your case, it was that std::get<Is>(tup) didn't return a reference and that static_cast didn't fix it.

It works correctly if I edit it to use pointers instead:

template<typename T, typename Tup, typename... TArgs, std::size_t... Is>
std::unique_ptr<T>
helper(const MyParam &domain, MyParam pl, Tup &tup, pack<TArgs...>, std::index_sequence<Is...>) {
    return std::make_unique<T>(domain, pl, *std::get<Is>(tup)...);
}

// use tuple packing/unpacking
template<typename T, typename... Args>
PreparedAlgorithm createInitializer1(Args &... args) {
    return [tup = std::make_tuple<Args*...>(&args...)](const MyParam &domain,
                                                                MyParam pl) mutable {
        return helper<T>(domain,
                         pl,
                         tup,
                         pack<Args &&...>{},
                         std::index_sequence_for<Args...>{});
    };
}

Now, here's your whole code: https://repl.it/repls/TenseElectricMolecule

Dugi
  • 241
  • 2
  • 14
  • Thank you! These examples now run correctly indeed. However, they do not allow for perferct forwarding -- it's not possible to call `createInitializer` with R-value parameters, they must be declared and assigned first, turning them into L-values. See https://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c for more details. – jolly Jun 11 '19 at 08:25
  • I especially enjoyed your constructive feedback how to debug these things, it's tricky to do, so it's appreciated. – jolly Jun 11 '19 at 08:26