Using lambda expressions in non-generic C ++ code

Introduced in C ++ 11, lambdas have become one of the coolest features of the new language standard, making generic code simpler and more readable. Each new version of the C ++ standard adds new features to lambdas, making generic code even easier and more readable. Did you notice that the word "generalized" was repeated twice? This is for good reason - lambdas work really well with template-based code. But when we try to use them in non-generic, type-specific code, we run into a number of problems. An article about the reasons and ways of solving these problems.





Instead of introducing

First, let's define the terminology: we call a lambda a lambda-expression, which is a C ++ expression that defines a closure object . Here is a quote from the C ++ standard:





[expr.prim.lambda.general]

A lambda-expression is a prvalue whose result object is called the closure object .

[ Note 1 : A closure object behaves like a function object. - end note ]





A closure object type is a unique, unnamed class.





[expr.prim.lambda.closure]

The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type, called the closure type, whose properties are described below.





" Untitled " in this case means that the type of closure cannot be explicitly specified in the code, but you can get it, which we will actively use below. " Unique " means that each lambda generates a new type of closure, that is, two lambdas that are absolutely identical from a syntactic point of view (we will call such lambdas homogeneous) have different types:





auto l1 = [](int x) { return x; };
auto l2 = [](int x) { return x; };
static_assert(!std::is_same_v<decltype(l1), decltype(l2)>);
      
      



Similarly, this principle applies to generic code that depends on the types of closures:





template <typename Func>
class LambdaDependent {
 public:
  explicit LambdaDependent(Func f) : f_{f} {}
 private:
  Func f_;
};

LambdaDependent ld1{l1};
LambdaDependent ld2{l2};
static_assert(!std::is_same_v<decltype(ld1), decltype(ld2)>);
      
      



This property prevents, for example, putting closure objects into containers (for example, in std :: vector <> ).





Standard solutions

std::function<>. , std::function<> :





std::function f1{l1};
std::function f2{l2};
static_assert(std::is_same_v<decltype(f1), decltype(f2)>);
      
      



, . std::function<> , , , - legacy API. , :





int api_func(int(*fp)(int), int value) {
  return fp(value);
}
      
      



, (l1 l2), :





std::cout << api_func(l1, 123) << '\n'; // 123
std::cout << api_func(l2, 234) << '\n'; // 234
      
      



, ( ) :





[expr.prim.lambda.closure]

The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type's function call operator. The conversion is to β€œpointer to noexcept function” if the function call operator has a non-throwing exception specification. The value returned by this conversion function is the address of a function F that, when invoked, has the same effect as invoking the closure type's function call operator on a default-constructed instance of the closure type. F is a constexpr function if the function call operator is a constexpr function and is an immediate function if the function call operator is an immediate function.





static_cast<>:





LambdaDependent lf1{static_cast<int(*)(int)>(l1)};
LambdaDependent lf2{static_cast<int(*)(int)>(l2)};
static_assert(std::is_same_v<decltype(lf1), decltype(lf2)>);
      
      



, static_cast<> :





LambdaDependent ls1{+l1};
LambdaDependent ls2{+l2};
static_assert(std::is_same_v<decltype(ls1), decltype(ls2)>);
      
      



- , + .





[over.built]

For every type T there exist candidate operator functions of the form

T* operator+(T*);







, , , static_cast<>.





, , ? C – void*. , .





int api_func_ctx(int(*fp)(void*, int), void* ctx, int value) {
  return fp(ctx, value);
}
      
      



:





int counter = 1;
auto const_lambda = [counter](int value) {
  return value + counter;
};

std::cout << api_func_ctx([](void* ctx, int value) {
  auto* lambda_ptr = static_cast<decltype(const_lambda)*>(ctx);
  return (*lambda_ptr)(value);
}, &const_lambda, 123) << '\n'; // 124
      
      



, , . void*, , . , mutable :





auto mutable_lambda = [&counter](int value) mutable {
  ++counter;
  return value * counter;
};

std::cout << api_func_ctx([](void* ctx, int value) {
  auto* lambda_ptr = static_cast<decltype(mutable_lambda)*>(ctx);
  return (*lambda_ptr)(value);
}, &mutable_lambda, 123) << ':' << counter << '\n'; // 246:2
      
      



, . api_func_ctx , .





, , , 2 :





  • void* ( type erasure);





  • , .





«» closure_erasure:





template <typename Ret, typename ...Args>
struct closure_erasure {
    Ret(*func)(void*, Args...);
    void* ctx;
};

      
      



: ? CTAD – . , ? operator(), . :





template<typename Lambda>
explicit closure_erasure(Ret(Lambda::*)(Args...), void* ctx) :
  func{
    [](void* c, Args ...args) {
      auto* lambda_ptr = static_cast<Lambda*>(c);
      return (*lambda_ptr)(std::forward<Args>(args)...);
    }
  },
  ctx{ctx} {}
template<typename Lambda>
explicit closure_erasure(Ret(Lambda::*)(Args...) const, void* ctx) :
  func{
    [](void* c, Args ...args) {
      auto* lambda_ptr = static_cast<Lambda*>(c);
      return (*lambda_ptr)(std::forward<Args>(args)...);
    }
  },
  ctx{ctx} {}

      
      



const – , (), mutable .





: , operator() :





auto make_closure_erasure = [](auto& lmb) {
  return closure_erasure{
    &std::remove_reference_t<decltype(lmb)>::operator(), &lmb};
};
      
      



, . , : !





, noexcept, :





template <typename Ret, bool NoExcept, typename ...Args>
struct closure_erasure {
  Ret(*func)(void*, Args...) noexcept(NoExcept);
  void* ctx;
  template<typename Lambda>
  explicit closure_erasure(Ret(Lambda::*)(Args...) noexcept(NoExcept), void* ctx) :
    func{
      [](void* c, Args ...args) noexcept(NoExcept) {
        auto* lambda_ptr = static_cast<Lambda*>(c);
        return (*lambda_ptr)(std::forward<Args>(args)...);
      }
    },
    ctx{ctx} {}
  template<typename Lambda>
  explicit closure_erasure(Ret(Lambda::*)(Args...) const noexcept(NoExcept), void* ctx) :
  func{
    [](void* c, Args ...args) noexcept(NoExcept) {
      auto* lambda_ptr = static_cast<Lambda*>(c);
      return (*lambda_ptr)(std::forward<Args>(args)...);
    }
  },
  ctx{ctx} {}
};

auto make_closure_erasure = [](auto& lmb) {
  return closure_erasure{
    &std::remove_reference_t<decltype(lmb)>::operator(), &lmb};
};

auto li = make_closure_erasure(const_lambda);
	std::cout << api_func_ctx(li.func, li.ctx, 123) << '\n'; // 124
	li = make_closure_erasure(mutable_lambda);
	std::cout << counter << ':' <<
	  api_func_ctx(li.func, li.ctx, 123) << '\n'; // 2:369
	std::cout << counter << ':' <<
	  api_func_ctx(li.func, li.ctx, 123) << '\n'; // 3:492
      
      



  1. Compiler Explorer





  2. "C++ Lambda Story"





  3. Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019





  4. C++ Weekly - Ep 246 - (+[](){})() What Does It Mean?





  5. C++





Many thanks to Valery Artyukhin for proofreading.








All Articles