原文:
《Under the hood of lambdas and std::function》
本文是根据原文的翻译。
在本文中,我们将在不同场景下探讨lambda的作用。然后进一步深入,研究std::function
和它的工作原理。
lambdas是c++11最有用的特性之一。这里先简要概述一下。
lambda是匿名函数的雅称。从本质上讲,它们是一种在代码的逻辑位置编写函数(如回调)的简单方法。
在写c++中我很喜欢这样写:[](){}()
,这就是一个空的lambda表达式,并且会马上执行。当然,这个lambda没啥用。
举个有用的例子:
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
以上可以看出lambda有以下优点:
lambda表达式有三部分:
下面是一个简单的例子。
int i = 0, j = 1;
auto func = [i, &j](bool b, float f){ ++j; cout << i << ", " << b << ", " << f << endl; };
func(true, 1.0f);
这个例子中:
func
的参数bool b
和float f
我发现把lambda类比为一种class有助于理解。
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不是一个std::function
。
尽管lambda表达式可以被赋值给std::function
,但这不是它初始的类型,而是经过了类型转换。
事实上,lambdas 没有标准类型。lambda 的类型是创建这个概念时被单独定义的,而捕获一个 lambda 而不进行转换的唯一方法是使用auto
auto f2 = [](){};
然而,假如capture list为空的话,可以把lambda赋值给函数指针。
void (*foo)(bool, int);
foo = [](bool, int){};
#include
#include
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
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
类似class的大小,lambda的大小是由captures决定的。
auto f1 = [](){};
cout << sizeof(f1) << endl;
std::array ar;
auto f2 = [&ar](){};
cout << sizeof(f2) << endl;
auto f3 = [ar](){};
cout << sizeof(f3) << endl;
输出:
1
8
100
lambda的性能表现非常优秀。因为他们是对象而非指针,编译器很容易将其处理为内联函数。(类似仿函数)
使用lambda比使用全局函数要快很多,这是C++比C快的一个例子。
std::function
是一个对象模版,用于储存任何可被调用的数据类型。比如函数、对象、lambda表达式以及std::bind
的返回值。
举例:
#include
#include
using namespace std;
void global_f() {
cout << "global_f()" << endl;
}
struct Functor {
void operator()() { cout << "Functor" << endl; }
};
int main() {
std::function f;
cout << "sizeof(f) == " << sizeof(f) << endl;
f = global_f;
f();
f = [](){ cout << "Lambda" << endl;};
f();
Functor functor;
f = functor;
f();
}
emmm…类似于函数指针?
在 clang++上,所有std::function
的大小(不管返回值或参数如何)始终为32字节。它使用所谓的小规模优化,很像 std::string
在许多实现中所做的那样。
这基本上意味着对于较小的对象,std::function 可以将它们作为其内存的一部分,但对于较大的对象,它遵循动态内存分配。下面是64位机器上的一个例子:
#include
#include
#include
#include // 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 arr1;
auto lambda1 = [arr1](){};
cout << "Assigning lambda1 of size " << sizeof(lambda1) << endl;
std::function f1 = lambda1;
std::array arr2;
auto lambda2 = [arr2](){};
cout << "Assigning lambda2 of size " << sizeof(lambda2) << endl;
std::function f2 = lambda2;
}
Assigning lambda1 of size 16
Assigning lambda2 of size 17
Allocating 17 bytes
阈值是17,超过这个阈值,std::函数就会恢复为动态分配(在clang上)。注意,分配的大小是17个字节,因为lambda对象需要在内存中是连续的。
(注:使用msvc并不会发生这样的分配)。