C++ lambda表达式 std::function 深层详解

原文:
《Under the hood of lambdas and std::function》
本文是根据原文的翻译。

文章目录

  • 什么是lambda?
    • lambda的使用语法
    • 按值捕获和按引用捕获的对比
    • lambda表达式的类型
    • lambda的作用域
    • mutable lambdas
    • lambda的大小
    • lambda的性能表现
  • std::function
    • 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 bfloat f
  • – 当该lambda表达式被调用后,会打印b和f
  • 第三行调用了该lambda表达式

我发现把lambda类比为一种class有助于理解。

  • Captures(也就是中括号中的变量),相当于类中的成员变量
  • 当lambda被创建,构造函数会把captures作为成员变量
  • 这个类对operator ()进行了重载。(相当于伪函数)
  • 它有生命周期,生命周期结束后就会被析构。

语法方面最后还需要补充一点:capture有许多默认值:

  • [&](){ i = 0; j = 0; },该表达式以引用的方式捕获了ij[&]表示该表达式中捕获的所有变量都是引用。
  • [=](){ 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 
#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

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 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 
#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…类似于函数指针?

std::function的大小

在 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并不会发生这样的分配)。

你可能感兴趣的:(C++,c++,lambda)