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
-
-
Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019
C++ Weekly - Ep 246 - (+[](){})() What Does It Mean?
-
Many thanks to Valery Artyukhin for proofreading.