Search code examples
c++lambdaconstexprstateless

Nest captureless lambdas in C++ with functions?


Question

Is it possible to nest captureless lambdas when passing them around in C++?

From what I can gather, seems to not be the case; rather, it seems you'd have to make a template class, and define static functions, and then pass those around.

Background

Related to this post, for pybind11: Possible to have logical operations (e.g. `ndarray.__eq__`) not return `bool` in NumPy?

Basically, I want to see if I can avoid macros and still provide stateless functions (without any type-erased void* data shenanigans) to be used with the Numpy UFunc API.

I'm aiming for stateless, because it appears the API has some functions that can have the void* data, which I could use to pass another function pointer (or erased lambda with capture), but some of them appear not to.

Blind Hacking

Here's my hacking:

// Goal: Wrap a function of type `Func` without capture.
typedef void (*Func)();


// NOPE: `b` ain't stateless.
Func wrap(Func a) {
  return [a]() { a(); };
}
int main() {
  Func a = []() { cout << "A1\n"; };
  wrap(a)();
}


// KINDA: Compiles, but `a` wasn't a parameter :(
// - Could use arrays a constexpr indices to fake it :( * 2
int main() {
  static const Func a = []() { cout << "A1\n"; };
  Func b = []() { a(); };
  b();
}

// YUP-ish: Works, cannot deal with lambdas :(
template <Func a>
Func wrap() {
  return []() { a(); };
}
// - Cannot be in a class in function scope :(
void a2() { cout << "A2\n"; }
int main() {
  Func b = wrap<tmp::a2>();
  b();
}

// NOPE: Lambda doesn't have default constructor (but what else would it do???)
int main() {
  auto a = []() { cout << "A3\n"; };
  using A = decltype(a);
  Func b = []() { A a{}; a(); };
  b();
}

I see there's some new stuff coming in for constexpr lambdas, but that seems to be more about the return value being constexpr, not the definition of the lambda itself (so that it could be passed as a template argument or whatever).


Solution

  • You can copy a lambda into a static variable:

    template<typename F>
    auto wrap(F a) {
        static auto F b = std::move(a);
        return []() { b(); };
    }
    

    Each lambda has a different type, so for each lambda, a new static variable is created, since the function is templated.

    Note that it only work if the passed callable has a different type. If you only use stateless function object, you won't have any problems.

    As a safe guard, you can also ensure only captureless lambda are sent:

    template<typename F, std::void_t<
        decltype(+std::declval<F>()),
        decltype(&F::operator())
    >* = nullptr>
    auto wrap(F a) {
        static auto F b = std::move(a);
        return []() { b(); };
    }
    

    The sfinae expression looks for the unary operator+ that with catpureless lambda, and it also look for the presence of a operator() member of the lambda.