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

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

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值