3

I have to make an asynchronous call inside a lambda, and once the asynchronous call is terminated I have to call the lambda itself.

I try to explain my problem with code:

typedef function<void(int id)> Callback;
AsyncWork1(Callback call, int id, string)
{
    //...
    call(id);
}

AsyncWork2(Callback call, int id, double, string)
{
    //...
    call(id);
}
void AsyncWorks(Callback final_callback, int id)
{
    Callback lambda = [&lambda, final_callback, id](int next_work) -> void
        {
            if(next_work == 1)
            {
                //...
                AsyncWork1(lambda, 2, "bla bla");
            }
            else if(next_work == 2)
            {
                //...
                //the lambda variable no longer exists
                AsyncWork2(lambda, 3, 0.0, "bla bla");
            }
            else if(next_work == 3)
            {
                //...
                final_callback(id);
            }
        };

    lambda(1);
}
int main()
{
    AsyncWorks(...);

    AsyncWorks(...);

    AsyncWorks(...);

    AsyncWorks(...);

    return 0;
}

The problem is that when the code exits from "AsyncWorks(...)" function, the local variable "lambda" no longer exists.

I've read several threads that talk about lambda recursive, but I haven't found any solution.

How can I solve this problem?

  • You could probably do [this](https://stackoverflow.com/questions/37924996/lambda-with-dynamic-storage-duration) and have it delete itself at the end. – NathanOliver May 29 '19 at 14:54
  • Beware that this design has flaws. There is no way to know that all the async tasks have finished by the time you `return 0;` from `main`. It's possible to implement this, but you might as well just use `std::async` and rely on the `std::future` it provides. – François Andrieux May 29 '19 at 15:05
  • I know, this is a simple example, in the real application (based on FreeRTOS) this does not occur. – Parminder Singh May 30 '19 at 08:51

3 Answers3

5

The basic problem is that C++ doesn't expose the this pointer of a lambda to itself.

As it happens, there are many languages where during something's definition, you cannot refer to itself. This is fixed in functional languages using a technique called the "Y Combinator".

A simple y combinator in C++ looks like:

template<class F>
struct y_combinator_t {
  F f;
  template<class...Args>
  auto operator()(Args&&...args)
  -> std::result_of_t< F&( y_combinator_t<F>&, Args&&... ) >
  {
    return f( *this, std::forward<Args>(args)... );
  }
};
template<class F>
y_combinator_t<std::decay_t<F>> y_combinate( F&& f ) {
  return {std::forward<F>(f)};
}

I'm of two minds if we should f( *this or f( f, I sometimes do either.

Use:

void AsyncWorks(Callback final_callback, int id)
{
  Callback lambda = y_combinate(
    [final_callback, id]
    (auto& self, int next_work)
    -> void
    {
      if(next_work == 1) {
        //...
        AsyncWork1(self, 2, "bla bla");
      } else if(next_work == 2) {
        //...
        //the lambda variable no longer exists
        AsyncWork2(self, 3, 0.0, "bla bla");
      } else if(next_work == 3) {
        //...
        final_callback(id);
      }
    }
  );
  lambda(1);
}

basically, I added an implicit self parameter to the lambda function body. Caller of the operator() don't see this parameter.

Y combinator based off this post by myself with modifications.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    `y_combinate` -> `y_combine`? – Caleth May 29 '19 at 15:44
  • What does it mean "F&"? – Parminder Singh Jun 03 '19 at 12:13
  • @parmi F is a type -- F& is a reference to that type. `F&( y_combinator_t&, Args&&... )` is a signature of a function returning `F&` and taking `y_combinator_t&, Args&&...`. It is passed to `result_of_t`, which pretends the return type is the type of object you are passing the arguments to, and returns the type actually returned. So we get "if you call an `F&` with `y_combinator_t&, Args&&...`, whatever type that returns" is the return type of `y_combinator_t::operator()(Args&&...)`. – Yakk - Adam Nevraumont Jun 03 '19 at 13:06
  • @parmi all of which are technical details; the point is you write your lambda with `auto&self` as the first argument, and then pass it to `y_combinate`, and now you have access to a reference to yourself. – Yakk - Adam Nevraumont Jun 03 '19 at 13:52
0

Lambda can capture itself implicitly. To demo how? see below code it calculate factorial value.

#include <iostream>

int (* factorial)( const int) = []( const int number)
{
    if( number > 1)
    {
        return number* factorial( number - 1);
    }
    else
    {
        return 1;
    }
};

int main(int , char *[])
{
    int fact = factorial( 7);
    std::cout<< "7! = "<< fact<< std::endl;
}
Output 7! = 5040

If any variable is used inside lambda then lambda captures it implicitly, if it is not captured explicitly. Due to this inside lambda, name factorial referring itself, is available.
But if in place of
int (* factorial)( const int) = []( const int number){//implementation };

if auto is used like as follows,

auto factorial = []( const int number){ //implementation };

then g++ compiler gives following error,
error: use of ‘factorial’ before deduction of ‘auto’ return number* factorial( number - 1);

It is because type of factorial is not deduced, auto does not deduce for same control block. Name factorial will be available only below the declaration in case of auto.

Vikas Awadhiya
  • 290
  • 1
  • 8
  • So in conclusion, this works only if the lambda does not capture anything. – alain May 29 '19 at 16:16
  • 1
    If lambda capture anything then it is not convertible to function pointer, because it becomes a class of unique name know only to compiler and then `auto` have to use and then what will happen is explained above. But it is not the purpose of lambda, like purpose of `std::map` is to provide key/value pair not random access, similarly purpose of lambda is to make quick anonymous callback functions with capture capability. – Vikas Awadhiya May 29 '19 at 18:33
  • Your `factorial` is global too :-/ – Jarod42 May 29 '19 at 20:00
0

@Yakk To understand your answer I had to spend some time studying many c++ features, value category, rvalue, lvalue, move constructor, move assignment operator, variadic templates, variadic templates with implicit conversions, result_of_t<>, decay_t<>, forward<>.

But I don't have a thing yet, why did you put the '&' symbol here?

... std::result_of_t <F&(y_combinator_t<...

I also rewrote your solution to make it more specific for my case and easier to read and understand for me too (and for all those who are beginners with C++),

class y_combinator_t
{
public:
    function<void(y_combinator_t*, int, double, string)> callback;

    void operator()(int a, double b, string s)
    {
        this->callback(this, a, b, s);
    }
};

y_combinator_t combinator = {
    [id_work = 1]
    (y_combinator_t* _this, int a, double b, string s) mutable -> void
    {
        if(id_work == 1)
        {
            //...
            AsyncWork1(*_this, 2, 3.0, "bla bla");
            id_work = 2;
        }
        else if(id_work == 2)
        {
            //...
            AsyncWork2(*_this, 3, 0.0, "bla bla");
            id_work = 3;
        }
        else if(id_work == 3)
        {
            //...
        }
    }
};

//Start works
combinator(0, 0, "");