原文:
《Under the hood of lambdas and std::function》
本文是根据原文的翻译。
文章目录
在本文中,我们将在不同场景下探讨lambda的作用。然后进一步深入,研究std::function
和它的工作原理。
什么是lambda?
lambdas是c++11最有用的特性之一。这里先简要概述一下。
lambda是匿名函数的雅称。从本质上讲,它们是一种在代码的逻辑位置编写函数(如回调)的简单方法。
在写c++中我很喜欢这样写:[](){}()
,这就是一个空的lambda表达式,并且会马上执行。当然,这个lambda没啥用。
举个有用的例子:
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
以上可以看出lambda有以下优点:
- 调用前不需要在另外一个地方声明。lambda写的位置就是它用的位置,合理使用使代码可读性更好。
- 不会污染命名空间。
lambda的使用语法
lambda表达式有三部分:
- 1、Capture list, 这里列出在lambda表达式中复用的变量(后面介绍完用法之后比较容易理解)
- 2、参数列表。类似普通函数的参数,用于参数传递。
- 3、代码体。
下面是一个简单的例子。
int i = 0, j = 1;
auto func = [i, &j](bool b, float f){ ++j; cout << i << ", " << b << ", " << f << endl; };
func(true, 1.0f);
这个例子中:
- 第一行很简单,就是声明了两个int型变量
- 第二行声明了一个lambda表达式
- – 该lambda捕获了i的值和j的引用。此后i和j会作为
func
的参数 - – 该lambda的参数是
bool b
和float f
- – 当该lambda表达式被调用后,会打印b和f
- 第三行调用了该lambda表达式
我发现把lambda类比为一种class有助于理解。
- Captures(也就是中括号中的变量),相当于类中的成员变量
- 当lambda被创建,构造函数会把captures作为成员变量
- 这个类对
operator ()
进行了重载。(相当于伪函数) - 它有生命周期,生命周期结束后就会被析构。
语法方面最后还需要补充一点:capture有许多默认值:
[&](){ i = 0; j = 0; }
,该表达式以引用的方式捕获了i
和j
。[&]
表示该表达式中捕获的所有变量都是引用。[=](){ cout << k; }
该表达式以值的方式捕获了k
,[=]
指的是在该函数中捕获的所有变量都是值- 你也可以混用:
[&, i, j](){}
,除了i,j是按值捕获,其他变量是按引用捕获。当然也可以这样用:[=, &i, &j](){}
按值捕获和按引用捕获的对比
int i = 0;
auto foo = [i](){ cout << i << endl; };
auto bar = [&i](){ cout << i << endl; };
i = 10;
foo();
bar();
0
10
lambda表达式的类型
需要注意的是,lambda不是一个std::function
。
尽管lambda表达式可以被赋值给std::function
,但这不是它初始的类型,而是经过了类型转换。
事实上,lambdas 没有标准类型。lambda 的类型是创建这个概念时被单独定义的,而捕获一个 lambda 而不进行转换的唯一方法是使用auto
auto f2 = [](){};
然而,假如capture list为空的话,可以把lambda赋值给函数指针。
void (*foo)(bool, int);
foo = [](bool, int){};
lambda的作用域
#include <iostream>
#include <functional>
struct MyStruct {
MyStruct() { std::cout << "Constructed" << std::endl; }
MyStruct(MyStruct const&) { std::cout << "Copy-Constructed" << std::endl; }
~MyStruct() { std::cout << "Destructed" << std::endl; }
};
int main() {
std::cout << "Creating MyStruct..." << std::endl;
MyStruct ms;
{
std::cout << "Creating lambda..." << std::endl;
auto f = [ms]() {}; // note 'ms' is captured by-value
std::cout << "Destroying lambda..." << std::endl;
}
std::cout << "Destroying MyStruct..." << std::endl;
}
输出:
Creating MyStruct...
Constructed
Creating lambda...
Copy-Constructed
Destroying lambda...
Destructed
Destroying MyStruct...
Destructed
mutable lambdas
lambda的operator()
是常函数,这意味着它不可以修改captures的值。(就像常函数不能修改类的成员变量一样)
但是增加mutable修饰之后,operator()
就不是常函数了,就可以修改captures了
int i = 1;
[&i](){ i = 1; }; // ok, 'i' 是按引用捕获的。
[i](){ i = 1; }; // ERROR
[i]() mutable { i = 1; }; // ok.
假如把lambda当成类来理解,事情就很有趣了:
int i = 0;
auto x = [i]() mutable { cout << ++i << endl; }
x();
auto y = x;
x();
y();
输出:
1
2
2
lambda的大小
类似class的大小,lambda的大小是由captures决定的。
auto f1 = [](){};
cout << sizeof(f1) << endl;
std::array<char, 100> ar;
auto f2 = [&ar](){};
cout << sizeof(f2) << endl;
auto f3 = [ar](){};
cout << sizeof(f3) << endl;
输出:
1
8
100
lambda的性能表现
lambda的性能表现非常优秀。因为他们是对象而非指针,编译器很容易将其处理为内联函数。(类似仿函数)
使用lambda比使用全局函数要快很多,这是C++比C快的一个例子。
std::function
std::function
是一个对象模版,用于储存任何可被调用的数据类型。比如函数、对象、lambda表达式以及std::bind
的返回值。
举例:
#include <iostream>
#include <functional>
using namespace std;
void global_f() {
cout << "global_f()" << endl;
}
struct Functor {
void operator()() { cout << "Functor" << endl; }
};
int main() {
std::function<void()> f;
cout << "sizeof(f) == " << sizeof(f) << endl;
f = global_f;
f();
f = [](){ cout << "Lambda" << endl;};
f();
Functor functor;
f = functor;
f();
}
emmm…类似于函数指针?
std::function的大小
在 clang++上,所有std::function
的大小(不管返回值或参数如何)始终为32字节。它使用所谓的小规模优化,很像 std::string
在许多实现中所做的那样。
这基本上意味着对于较小的对象,std::function 可以将它们作为其内存的一部分,但对于较大的对象,它遵循动态内存分配。下面是64位机器上的一个例子:
#include <iostream>
#include <functional>
#include <array>
#include <cstdlib> // for malloc() and free()
using namespace std;
// 重载运算符new和delete
void* operator new(std::size_t n) {
cout << "Allocating " << n << " bytes" << endl;
return malloc(n);
}
void operator delete(void* p) throw() {
free(p);
}
int main() {
std::array<char, 16> arr1;
auto lambda1 = [arr1](){};
cout << "Assigning lambda1 of size " << sizeof(lambda1) << endl;
std::function<void()> f1 = lambda1;
std::array<char, 17> arr2;
auto lambda2 = [arr2](){};
cout << "Assigning lambda2 of size " << sizeof(lambda2) << endl;
std::function<void()> f2 = lambda2;
}
Assigning lambda1 of size 16
Assigning lambda2 of size 17
Allocating 17 bytes
阈值是17,超过这个阈值,std::函数就会恢复为动态分配(在clang上)。注意,分配的大小是17个字节,因为lambda对象需要在内存中是连续的。
(注:使用msvc并不会发生这样的分配)。