Reference
- Back to Basics: Lambdas from Scratch - Arthur O’Dwyer - CppCon 2019
- Back to Basics: Lambdas - Nicolai Josuttis - CppCon 2021
- C++ Lambda Idioms - Timur Doumler - CppCon 2022
- Back To Basics: Lambda Expressions - Barbara Geller & Ansel Sermersheim - CppCon 2020
- C++ Weekly - Ep 332 - C++ Lambda vs std::function vs Function Pointer
Lambdas is just a Class
Lambdas 的實現原理就是 compiler 幫你把他轉成一個 Class 而已,考慮以下的例子:
auto foo = [g=10](){ return g + 1;};
我們可以用一個 class 做到完全相同的事情,這裡我們透過 cppinsight 來觀察上面的 lambdas function 會被 compiler 轉換成什麼:
class __lambda_1_12
{
public:
inline /*constexpr */ int operator()() const
{
return g + 1;
}
private:
int g;
public:
__lambda_1_12(const int & _g)
: g{_g}
{}
};
__lambda_1_12 foo = __lambda_1_12{10};
可以看到 compiler 其實只是將 lambdas function 的 capture list 裡面的 variable 換成 class 的 private member variable,然後 overload operator()
,所以當 __lambda_1_12{10}
被建立出來並 assign 給 foo
後,若 foo()
被執行,其實就是呼叫了被 overload 的 ()
operator。
Immediately Invoked Function Expressions(IIFE)
A weird way to print out hello world.
#include <iostream>
int main() {
[](){std::cout << "hello world" << std::endl;}();
}
什麼時候 IIFE 有用?看下面的例子,若今天要 initialize foo
的 if else 的判斷很複雜,通常我們就會想要抽 function,但是這樣就必須把 if else 要判斷的任何參數從 local 傳進 function parameter,而 lambda 可以直接 capture 這些必要的參數,且將判斷邏輯留在 local,在 trace code 時更好被理解。
int main() {
// some code ...
const Foo foo = [&] {
if (hasDatabase) {
return getFooFromDatabase();
} else {
return getFooFromElsewhere()
}
}();
// another example:
std::vector<Foo> foos;
foos.emplace_back([&]{
if (hasDatabase) {
return getFooFromDatabase();
} else {
return getFooFromElsewhere()
}
}());
}
如果覺得 ()
invoke lambda function 的方式不太明顯,看的人會誤會的話,可以使用 std::invoke
:
int main() {
// another example:
std::vector<Foo> foos;
foos.emplace_back(std::invoke([&]{
if (hasDatabase) {
return getFooFromDatabase();
} else {
return getFooFromElsewhere()
}
};
}
Capture List
#include <iostream>
int g = 10;
auto foo = [](){ return g + 1;};
auto bar = [g=g](){ return g + 1;};
int main() {
g = 20;
std::cout << foo() << std::endl; // 11
std::cout << bar() << std::endl; // 21
return 0;
}
為何一個會印出 11 一個則是 21?我們一樣透過 cppinsight 來觀察:
#include <iostream>
int g = 10;
class __lambda_4_12
{
public:
inline /*constexpr */ int operator()() const
{
return g + 1;
}
using retType_4_12 = int (*)();
inline constexpr operator retType_4_12 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ int __invoke()
{
return __lambda_4_12{}.operator()();
}
public:
// /*constexpr */ __lambda_4_12() = default;
};
__lambda_4_12 foo = __lambda_4_12{};
class __lambda_5_12
{
public:
inline /*constexpr */ int operator()() const
{
return g + 1;
}
private:
int g;
public:
__lambda_5_12(int & _g)
: g{_g}
{}
};
__lambda_5_12 bar = __lambda_5_12{g};
int main()
{
g = 20;
std::cout.operator<<(foo.operator()()).operator<<(std::endl);
std::cout.operator<<(bar.operator()()).operator<<(std::endl);
return 0;
}
可以觀察到 bar
在 class 被 initialize 時,global 的 g
有被當作參數傳進他的 constructor,而 foo
則沒有。
在 class foo
會在執行當下抓目前的 g
是多少,因此會抓到 20
。
Static Variable in lambdas
lambdas 的 static variable 和一般的 function static variable 不一樣!說到底還是因為 lambdas 的底層是 class!
#include <iostream>
auto counter = [](){ static int i; return ++i;};
/*
class counter {
public:
int operator() cosnt {
static int i; return ++i;
}
}
*/
int main() {
auto c1 = counter;
auto c2 = counter;
std::cout << c1() << c1() << c1() << "\n"; // 123
std::cout << c2() << c2() << c2() << "\n"; // 456
}
因為 i
是同一個 class 的 static member,而 c1 & c2 是同一個 lambdas,因此對應到同一個 class。
使用 cppinsights 觀察可以更加得清楚,底下是 compiler 針對上面的 code 所產生的。
#include <iostream>
class __lambda_3_16
{
public:
inline int operator()() const
{
static int i;
return ++i;
}
using retType_3_16 = int (*)();
inline constexpr operator retType_3_16 () const noexcept
{
return __invoke;
}
private:
static inline int __invoke()
{
return __lambda_3_16{}.operator()();
}
public:
// /*constexpr */ __lambda_3_16() = default;
};
__lambda_3_16 counter = __lambda_3_16{};
int main()
{
std::cout.operator<<(counter.operator __lambda_3_16::retType_3_16()).operator<<(std::endl);
return 0;
}
注意到 compiler 會將 operator()()
設為 const!
inline int operator()() const
{
static int i;
return ++i;
}
也因此,若想要改變 i 的值,就必須 specify mutable
!
#include <iostream>
auto counter = [i = 0]() mutable {return ++i;};
int main() {
auto c1 = counter;
auto c2 = counter;
std::cout << c1() << c1() << c1() << "\n"; // 123
std::cout << c2() << c2() << c2() << "\n"; // 123
}
Lambdas is constexpr by default
auto lam = [](int x) {return x + 1;};
static_assert(lam(42) == 43); // ok!
Generic Lambdas
Which is just templates under the hood.
#include <iostream>
auto Plus = [value=1](auto x) {return x + value;};
int main() {
std::cout << Plus(3) << "\n";
}
Using cppinsights again, and there it is, the templates.
#include <iostream>
class __lambda_3_13
{
public:
template<class type_parameter_0_0>
inline /*constexpr */ auto operator()(type_parameter_0_0 x) const
{
return x + value;
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<int>(int x) const
{
return x + value;
}
#endif
private:
int value;
public:
__lambda_3_13(const int & _value)
: value{_value}
{}
};
__lambda_3_13 Plus = __lambda_3_13{1};
int main()
{
std::operator<<(std::cout.operator<<(Plus.operator()(3)), "\n");
return 0;
}
“this” in lambda
In lambda, the this keyword does not mean the lambda itself, but whatever the lambda is used on. This is a designed choice:
class Widget {
void work(int);
void stnchronous_foo(int x) {
this->work(x);
}
void asynchronous_foo(int x) {
fire_and_forget([=]() {
this->work(x);
});
}
}
In the above example, if this refers to the lambda itself, we’re in trouble.
the unary operatoron lambdas
don’t use this in production code!
int main() {
auto* fptr = [](){ return 1;}; // error: unable to deduce 'auto*' from '<lambda closure object>main()::<lambda()>()'
auto* fptr2 = +[](){ return 1;}; // ok!
return 0;
}
因為 unary operator 只作用在 pointer type 上,所以 compliler 會幫我們把 lambdas 轉成 pointer to function,而 + 在 function pointer 上等於 nop。
Only Called Once
#include <iostream>
#include <functional>
struct X {
X(){
static auto _ = std::invoke([](){std::cout << "called once!" << std::endl; return 0;});
}
};
int main() {
X x1;
X x2;
X x3;
return 0;
}