Loading

lambda表达式与std::function

在这篇文章中,我们将探讨 lambda 在不同方面的表现。然后我们将研究 std::function 及其工作原理。

什么是lambda

如果你还没用过C++11最强大的特征之一——lambda,我就来做一个简短的介绍:

Lambda是匿名函数的别称。从本质上讲,它们是一种在代码的逻辑位置编写函数(比如回调函数)的简单方法。

我最喜欢的C++表达式是 [](){}();,它声明了一个空的lambda并且立即执行它。这个表达式显然没有任何功能作用,只是告诉你lambda表达式的格式。更好的一个例子是跟STL结合:

std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

与C++98方法相比,它具有以下优点:它是代码在逻辑上的位置(而不是在此范围之外定义类/函数),并且不会污染任何名称空间(尽管即使在C++98中也很容易绕过)。

lambda语法

Lambdas 分为3部分:

  1. [capture] : 捕获列表 - 捕捉列表总是出现在Lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数。捕捉列表能够捕捉上下文中的变量以供Lambda函数使用;
  2. (parameters) : 参数列表 – 与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略;
  3. {statement} : 函数体 – 内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

举个例子:

int i = 0, j = 1;
auto func = [i, &j](bool b, float f){ ++j; cout << i << ", " << b << ", " << f << endl; };
func(true, 1.0f);
  1. 第一行很简单 - 创建两个 int变量 命名为 ij.
  2. 第二行定义了一个lambda表达式:
    • 通过值传递捕捉变量 i,通过引用传递捕捉变量 j
    • 接受2个参数: bool bfloat f,
    • 调用时打印 bf
  3. 第三行用true1.0f为参数调用lambda.

我们可以把lambda表达式看做类:

  • 捕捉列表是数据成员:
    • func 的数据成员是 ij;
    • lambda可以在其代码范围内访问这些成员.
  • 创建lambda时,构造函数将捕获的变量复制到数据成员;
  • 这个类有 operator()(...) (对于 func 来说 ... 就是 bool, float);
  • 它有一个作用域生存期和一个释放成员的析构函数.

语法方面的最后一点:你还可以指定默认捕获:

  • [var]表示值传递方式捕捉变量var;

  • [=]表示值传递方式捕捉所有父作用域的变量(包括this);

  • [&var]表示引用传递捕捉变量var;

  • [&]表示引用传递方式捕捉所有父作用域的变量(包括this);

  • [this]表示值传递方式捕捉当前的this指针。

    上面提到了一个父作用域,也就是包含Lambda函数的语句块,说通俗点就是包含Lambda的“{}”代码块。上面的捕捉列表还可以进行组合,例如:

    • [=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量;
    • [&,a,this]表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量。

    不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:

    • [=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;
    • [&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。

值传递VS引用传递

上面我们提到了通过值和通过引用捕获lambda。有什么区别?下面是一个简单的代码,可以说明:

int i = 0;
auto foo = [i](){ cout << i << endl; };
auto bar = [&i](){ cout << i << endl; };
i = 10;
foo();
bar();

输出结果:

0
10

可以看出,值传递传入的是值,如果传入的是一个变量,相当于传递了一个副本,不会改变原有变量。引用传递传递的是一个指针(c++里也有引用),会改变原变量的值。

lambda作用域

所有捕捉到的变量作用域都在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 lambda

lambda的 operator()默认是const, 这意味着它不能直接修改捕捉到的变量. 要想修改的话需要添加 mutable:

int i = 1;
[&i](){ i = 1; }; // ok, 'i' 是引用传递捕捉到的.
[i](){ i = 1; }; // ERROR: 'i'是只读变量.
[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表达式的大小

因为lambda有捕获,所以lambda没有固定大小。举个例子:

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;

输出 (64位下):

1
8
100

性能

Lambda在性能方面也非常出色。因为它们是对象而不是指针,所以编译器可以很容易地内联它们,就像仿函数一样。这意味着多次调用lambda(例如使用std::sortstd::copy_if)比使用全局函数要好得多。这是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();
}

输出:

$ clang++ main.cpp -std=c++14 && ./a.out 
sizeof(f) == 32
global_f()
Lambda
Functor
posted @ 2021-11-10 20:48  柴承训  阅读(826)  评论(0编辑  收藏  举报