2

I am creating a class that allows me to store lambdas that need to be executed (in order) at a point in the future.

class Promise{
private:
    //snip//
    std::vector<std::function<void()>> lchain;
public:
    //snip//
    void then(const std::function<void()> &f){
        if (this->resolved) {//If the promise is resolved we just call the newly added function, else we add it to the lchain queue that will be processed later
            f();
            return;
        }

        lchain.push_back(f);
    }
    void launch(){
        this->resolved = true;
        for (auto &fun: this->lchain)
            fun();
    }
}

It is obvious that it will only work with lambdas with a signature like [&](){} but some of the tasks need to work with an arbitrary number of parameters of arbitrary types (both, parameters and types are known in advance, when the function is added to the queue).

An example driver program that currently works is

int main(){
    Promise* p = new Promise([](){
        std::cout << "first" << std::endl;
    })->then([](){
        std::cout << "second" << std::endl;
    });
    Promise->launch(); //In my code promise chains are picked up by worker threads that will launch them.
}

An example program I would like to execute:

int main(){
        Promise* p = new Promise([](){
            return 5;
        })->then([](int n){
            return n*n;
        })->then([](int n){
            std::cout << n << std::endl; //Expected output: 25
        });
        Promise->launch();
    }

Things that I am struggling to do:

  • Storing lambdas of mixed signatures in a std::vector
  • Making the then() method call f with the arguments associated with f
  • Making the then() function return the result of f so it can be fed to the next lambda in the chain (preferably binding it before storing the lambda in the vector)

I have been searching in stackoverflow the whole day but the closest I got was this but I would like something that can be done in the then() method to simplify the program code as it would be a pain to bind every single lambda before calling the then() method.

Kranga
  • 21
  • 3
  • 3
    For proper type safety of your chain, wouldn't you want the return type from `.then` to differ depending on the return type of the lambda passed in? For example, `.then([](T x) { return 4.0; })` should return a `Promise`. Have you considered building on top of std::promise/std::future? – jtbandes Jan 09 '20 at 00:42
  • Depending on the number of different signatures you could consider using std::variant – n314159 Jan 09 '20 at 00:47
  • 2
    How would a function other than the initial one get multiple arguments? – aschepler Jan 09 '20 at 00:57
  • 1
    Would something like this save you some effort? https://github.com/Naios/continuable – parktomatomi Jan 09 '20 at 08:29
  • @parktomatomi Wow! I'm tempted to delete my own answer – Indiana Kernick Jan 09 '20 at 09:08

1 Answers1

4

I have something that I think does what you want. I'll start with an example and then introduce the implementation.

int main(){
  Promise p([] {
    return 5;
  });
  p.then([](int n) {
    return n*n;
  }).then([](int n) {
    std::cout << n << '\n';
  });
  p.launch();

  struct A { int n; };
  struct B { int n; };
  struct C { int n; };

  Promise q([](A a, int n) {
    std::cout << "A " << a.n << ' ' << n << '\n';
    return B{2};
  });
  q.then([](B b) {
    std::cout << "B " << b.n << '\n';
    return C{3};
  }).then([](C c) {
    std::cout << "C " << c.n << '\n';
  });
  q.launch(A{1}, 111);

  Promise<B(A, int)> r([](auto a, int n) {
    std::cout << "A " << a.n << ' ' << n << '\n';
    return B{5};
  });
  r.then([](auto b) {
    std::cout << "B " << b.n << '\n';
    return C{6};
  }).then([](auto c) {
    std::cout << "C " << c.n << '\n';
  });
  r.launch(A{4}, 222);
}

This outputs:

25
A 1 111
B 2
C 3
A 4 222
B 5
C 6

Some drawbacks:

  • Calling then after the promise has been resolved doesn't automatically call the function. Things get confusing in that situation and I'm not even sure if it's possible.
  • You can't call then multiple times on the same promise. You have to build a chain and call then on the result of the previous then.

If any of those drawbacks make this unusable, then you can stop reading this humongous answer.


The first thing we need is a way of getting the signature of a lambda. This is only used for the deduction guide so it isn't strictly necessary for the core concept to work.

template <typename Func>
struct signature : signature<decltype(&Func::operator())> {};

template <typename Func>
struct signature<Func *> : signature<Func> {};

template <typename Func>
struct signature<const Func> : signature<Func> {};

template <typename Ret, typename... Args>
struct signature<Ret(Args...)> {
  using type = Ret(Args...);
};

template <typename Class, typename Ret, typename... Args>
struct signature<Ret (Class::*)(Args...)> : signature<Ret(Args...)> {};

template <typename Class, typename Ret, typename... Args>
struct signature<Ret (Class::*)(Args...) const> : signature<Ret(Args...)> {};

template <typename Func>
using signature_t = typename signature<Func>::type;

The next thing we need is a base class. We know the next promise must accept the return type of the current promise as an argument. So we know the argument type of the next promise. However, we don't know what the next promise will return until then is called so we need a polymorphic base to refer to the next promise.

template <typename... Args>
class PromiseBase {
public:
  virtual ~PromiseBase() = default;
  virtual void launch(Args...) = 0;
};

Now we have the Promise class itself. You can construct a promise with a function. As I alluded to above, a promise stores a pointer to the next promise in the chain. then constructs a promise from the given function and stores a pointer to it. There is only one next pointer so you can only call then once. There's an assertion to make sure this doesn't happen. launch calls the stored function and passes the result to the next promise in the chain (if there is one).

template <typename Func>
class Promise;

template <typename Ret, typename... Args>
class Promise<Ret(Args...)> : public PromiseBase<Args...> {
public:
  template <typename Func>
  explicit Promise(Func func)
    : handler{func} {}

  template <typename Func>
  auto &then(Func func) {
    assert(!next);
    if constexpr (std::is_void_v<Ret>) {
      using NextSig = std::invoke_result_t<Func>();
      auto nextPromise = std::make_unique<Promise<NextSig>>(func);
      auto &ret = *nextPromise.get();
      next = std::move(nextPromise);
      return ret; 
    } else {
      using NextSig = std::invoke_result_t<Func, Ret>(Ret);
      auto nextPromise = std::make_unique<Promise<NextSig>>(func);
      auto &ret = *nextPromise.get();
      next = std::move(nextPromise);
      return ret;
    }
  }

  void launch(Args... args) override {
    if (next) {
      if constexpr (std::is_void_v<Ret>) {
        handler(args...);
        next->launch();
      } else {
        next->launch(handler(args...));
      }
    } else {
      handler(args...);
    }
  }

private:
  using NextPromise = std::conditional_t<
    std::is_void_v<Ret>,
    PromiseBase<>,
    PromiseBase<Ret>
  >;
  std::unique_ptr<NextPromise> next;
  std::function<Ret(Args...)> handler;
};

Finally, we have a deduction guide.

template <typename Func>
Promise(Func) -> Promise<signature_t<Func>>;

Here's an online demo.

Indiana Kernick
  • 5,041
  • 2
  • 20
  • 50
  • Nice piece of code, but unfortunately it won't work with generic lambdas... – florestan Jan 09 '20 at 07:27
  • @florestan Yeah. You can't get the signature of a generic lambda. I probably should have mentioned that – Indiana Kernick Jan 09 '20 at 07:31
  • 1
    But since you know what will be the output of the previous node, you may (should) be able to remove the dependency of the lambda parameter, I think... – florestan Jan 09 '20 at 08:00
  • 1
    `template class PromiseBase {virtual void launch(Args...) = 0;}` would allow to get rid of `void` specialization. – Jarod42 Jan 09 '20 at 09:24
  • @Jarod42 Thanks for the tip. I updated my answer. As a side effect, the first function can now have multiple arguments. We still have to explicitly handle `void` in a few places which kind of sucks. – Indiana Kernick Jan 09 '20 at 10:03