lambda表达式

匿名函数是很多高级语言都支持的概念,如lisp语言在1958年首先采用匿名函数。匿名函数有函数体,但没有函数名。C++11中引入了lambda表达式。利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。但是从本质上来讲,lambda表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现。但是它简便的语法却给C++带来了深远的影响。如果从广义上说,lamdba表达式产生的是函数对象。

相同类似功能我们也可以使用函数对象或者函数指针实现:函数对象能维护状态,但语法开销大,而函数指针语法开销小,却没法保存范围内的状态。lambda表达式正是结合了两者的优点。

✏️ 1、Lambda表达式

🖋️ 1.1、声明Lambda表达式

// 完整语法
[capture list] (params list) mutable(optional) constexpr(optional)(c++17) exception attribute -> return type { function body };

// 可选的简化语法
[capture list] (params list) -> return type {function body};  //1
[capture list] (params list) {function body};		//2
[capture list] {function body};		//3
  • capture list:捕获外部变量列表,不能省略;

  • params list:形参列表,可以省略(但是后面必须紧跟函数体);

  • mutable指示符: 可选,将lambda表达式标记为mutable后,函数体就可以修改传值方式捕获的变量;

  • constexpr:可选,C++17,可以指定lambda表达式是一个常量函数;

  • exception:异常设定, 可选,指定lambda表达式可以抛出的异常;

  • attribute:可选,指定lambda表达式的特性;

  • return type:返回类型

  • function body:函数体

标号1. 函数声明了一个const类型的表达式,此声明不可改变capture list中的捕获的值。

标号2. 函数省略了返回值,此时如果function body内含有return语句,则按return语句返回类型决定返回值类型,若无则返回值为void类型。

标号3. 函数无参数列表,意味无参函数。

vector<int> vec{1,0,9,5,3,3,7,8,2};

sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });  

🖋️ 1.2、捕获外部变量

lambda表达式最前面的方括号的意义何在?其实这是lambda表达式一个很Hong要的功能,就是闭包。这里我们先讲一下lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

Lambda表达式可以捕获外面变量,但需要我们提供一个谓词函数([capture list]在声明表达式最前)。类似参数传递方式:值传递、引入传递、指针传递。在Lambda表达式中,外部变量捕获方式也类似:值捕获、引用捕获、隐式捕获

值捕获

int a = 123;
auto f = [a] { cout << a << endl; }; 
f(); // 输出:123
a = 321;
f(); // 输出:123

值捕获和参数传递中的值传递类似,被捕获的值在Lambda表达式创建时通过值拷贝的方式传入,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。因此Lambda表达式函数体中不能修改该外部变量的值; 因为函数调用运算符的重载方法是const属性的。同样,函数体外对于值的修改也不会改变被捕获的值。 想改动传值方式捕获的值,那么就要使用mutable

auto add_x = [x](int a) mutable { x *= 2; return a + x; };  // 复制捕捉x
    
cout << add_x(10) << endl; // 输出:30

因为一旦将lambda表达式标记为mutable,那么实现的函数调用运算符是非const属性的。

引用捕获

int a = 123;
auto f = [&a] { cout << a << endl; }; 
a = 321;
f(); // 输出:321

引用捕获的变量使用的实际上就是该引用所绑定的对象,因此引用对象的改变会改变函数体内对该对象的引用的值。 对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。

隐式捕获

隐式捕获有两种方式,分别是 [=]:以值补获的方式捕获外部所有变量 [&]:表示以引用捕获的方式捕获外部所有变量

int a = 123, b=321;
auto df = [=] { cout << a << b << endl; };    // 值捕获
auto rf = [&] { cout << a << b << endl; };    // 引用捕获

其他捕获方式

捕获外部变量形式

[ ]

不捕获任何变量(无参函数)

[变量1,&变量2, …]

值(引用)形式捕获指定的多个外部变量

[this]

值捕获this指针

[=, &x]

变量x以引用形式捕获,其余变量以传值形式捕获

[*this]

通过传值方式捕获当前对象

[&, x]

默认以引用捕获所有变量,但是x是例外,通过值捕获

既然只使用一次,那直接写全代码不久醒了,为啥要函数呢?——因为lambda可以捕获局部变量

在上面的捕获方式中,注意最好不要使用[=][&]默认捕获所有变量。首先说默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的声明周期:

std::function<int(int)> add_x(int x)
{
    return [&](int a) { return x + a; };
}

因为参数x仅是一个临时变量,函数调用后就被销毁,但是返回的lambda表达式却引用了该变量,但调用这个表达式时,引用的是一个垃圾值,所以会产生没有意义的结果。如果通过传值的方式来解决上面的问题:

std::function<int(int)> add_x(int x)
{
    return [=](int a) { return x + a; };
}

使用默认传值方式可以避免悬挂引用问题。但是采用默认值捕获所有变量仍然有风险,看下面的例子:

class Filter
{
public:
    Filter(int divisorVal) : divisor(divisorVal){}

    std::function<bool(int)> getFilter() 
    {
        return [=](int value) {return value % divisor == 0; };
    }

private:
    int divisor;
};

这个类中有一个成员方法,可以返回一个lambda表达式,这个表达式使用了类的数据成员divisor。而且采用默认值方式捕捉所有变量。你可能认为这个lambda表达式也捕捉了divisor的一份副本,但是实际上大错特错。问题出现在哪里呢?因为数据成员divisorlambda表达式并不可见,你可以用下面的代码验证:

// 类的方法,下面无法编译,因为divisor并不在lambda捕捉的范围
std::function<bool(int)> getFilter() 
{
    return [divisor](int value) {return value % divisor == 0; };
}

那么原来的代码为什么能够捕捉到呢?仔细想想,原来每个非静态方法都有一个this指针变量,利用this指针,你可以接近任何成员变量,所以lambda表达式实际上捕捉的是this指针的副本,所以原来的代码等价于:

std::function<bool(int)> getFilter() 
{
    return [this](int value) {return value % this->divisor == 0; };
}

尽管还是以值方式捕获,但是捕获的是指针,其实相当于以引用的方式捕获了当前类对象,所以lambda表达式的闭包与一个类对象绑定在一起了,这也很危险,因为你仍然有可能在类对象析构后使用这个lambda表达式,那么类似“悬挂引用”的问题也会产生。所以,采用默认值捕捉所有变量仍然是不安全的,主要是由于指针变量的复制,实际上还是按引用传值。

🖋️ 1.3、参数

  • 参数列表中不能有默认参数

  • 不支持可变参数

  • 所有参数必须有参数名

🖋️ 1.4、返回类型

单一的return语句可以推断返回类型;多语句则默认返回void,否则报错,应指定返回类型。

// 正确,单一return语句
transform(vi.begin(),vi.end(),vi.begin(), 
    [] (int i) { 
    return i<0? -i; i;
    }
);
// 错误。不能推断返回类型
transform(vi.begin(),vi.end(),vi.begin(), 
    [] (int i) { 
        if (I<0) return -i;
        else return i;
    }
);
// 正确,尾置返回类型
transform(vi.begin(),vi.end(),vi.begin(), 
    [] (int i) ->int{ 
        if (I<0) 
            return -i;
        else 
            return i;
    }
);

🖋️ 1.5、赋值

可以使用auto 和function 接受lambda表达式的返回:

int x = 8, y = 9;
auto add = [](int a, int b) { return a + b; };
std::function<int(int, int)> Add = [=](int a, int b) { return a + b; };

cout << "add: " << add(x, y) << endl; // 17
cout << "Add: " << Add(x, y) << endl; // 17

lambda表达式产生的类不含有默认构造函数、赋值运算符、默认析构函数。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,看来与具体实现有关。还有一点要注意:lambda表达式是不能被赋值的:

auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };

a = b;   // 非法,lambda无法赋值
auto c = a;   // 合法,生成一个副本

因为禁用了赋值操作符:

ClosureType& operator=(const ClosureType&) = delete;

但是没有禁用复制构造函数,所以可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本。并且lambda表达式也可以赋值给相对应的函数指针,这也使得你完全可以把lambda表达式看成对应函数类型的指针

🖋️ 1.6、新特性

C++14中,lambda又得到了增强,一个是泛型lambda表达式,一个是lambda可以捕捉表达式。

lambda捕捉表达式

lambda表达式可以按复制或者引用捕获在其作用域范围内的变量。而有时候,我们希望捕捉不在其作用域范围内的变量,而且最重要的是我们希望捕捉右值。所以C++14中引入了表达式捕捉,其允许用任何类型的表达式初始化捕捉的变量:

// 利用表达式捕获,可以更灵活地处理作用域内的变量
int x = 4;
auto y = [&r = x, x = x + 1] { r += 2; return x * x; }();
// 此时 x 更新为6,y 为25

// 直接用字面值初始化变量
auto z = [str = "string"]{ return str; }();
// 此时z是const char* 类型,存储字符串 string

可以看到捕捉表达式扩大了lambda表达式的捕捉能力,有时候你可以用std::move初始化变量。这对不能复制只能移动的对象很重要,比如std::unique_ptr,因为其不支持复制操作,你无法以值方式捕捉到它。但是利用lambda捕捉表达式,可以通过移动来捕捉它:

auto myPi = std::make_unique<double>(3.1415);

auto circle_area = [pi = std::move(myPi)](double r) { return *pi * r * r; };
cout << circle_area(1.0) << endl; // 3.1415

其实用表达式初始化捕捉变量,与使用auto声明一个变量的机理是类似的。

泛型lambda表达式

C++14开始,lambda表达式支持泛型:其参数可以使用自动推断类型的功能,而不需要显示地声明具体类型。这就如同函数模板一样,参数要使用类型自动推断功能,只需要将其类型指定为auto,类型推断规则与函数模板一样。这里给出一个简单例子:

auto add = [](auto x, auto y) { return x + y; };

int x = add(2, 3);   // 5
double y = add(2.5, 3.5);  // 6.0

🖋️ 1.7、Lambda与STL

lambda表达式可以赋值给对应类型的函数指针。但是使用函数指针貌似并不是那么方便。所以STL定义在<functional>头文件提供了一个多态的函数对象封装std::function,其类似于函数指针。它可以绑定任何类函数对象,只要参数与返回类型相同。如下面的返回一个bool且接收两个int的函数包装器:

std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };

lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。其实,最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过lambda表达式给出条件,传递给count_if函数:

int value = 3;
vector<int> v {1, 3, 5, 2, 6, 10};

int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });

再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用generate函数,并辅助lambda表达式:

vector<int> v(10);
int a = 0;
int b = 1;

std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}

此外,lambda表达式还用于对象的排序准则:

class Person
{
public:
    Person(const string& first, const string& last):
        firstName(first), lastName(last) {}

    Person() = default;

    string first() const { return firstName; }
    string last() const { return lastName; }
private:
    string firstName;
    string lastName;
};

int main()
{
    vector<Person> vp;
    // 添加Person信息
    ...
    // 按照姓名排序
    std::sort(vp.begin(), vp.end(), [](const Person& p1, const Person& p2)
    { return p1.last() < p2.last() || (p1.last() == p2.last() && p1.first() < p2.first()); });

    return 0;
}

总之,对于大部分STL算法,可以非常灵活地搭配lambda表达式来实现想要的效果。

✏️ 2、Lambda与递归

对于一个求自然数1-N和的问题,我们很容易写出以下递归公式:

f(n) = n == 1 ? 1 : n + f(n - 1);

若想使用C++lambda表达式实现上述过程,我们很容易将其改写为以下代码:

const auto& f = [](const int& n) {
	return n == 1 ? 1 : n + f(n - 1);
};

发现编译无法通过。

🖋️ 2.1、使用std::function

std::function可以把lambda包装起来,相当于赋予了其一个函数名,在通过引用捕获并实现递归调用,实现如下:

const auto& sum = [](const int& n) {
		std::function<int(const int&)> s;
		s = [&](const int& n) {
			  return n == 1 ? 1 : n + s(n - 1);
		};
		return s(n);
};

std::cout << sum(100);

🖋️ 2.2、将lambda作为参数

const auto& sum = [](const int& n) {
		const auto& s = [&](auto&& self, const int& x) -> int {
			  return x == 1 ? 1 : x + self(self, x - 1);
		};
		return s(s, n);
};

std::cout << sum(100);

🖋️ 2.3、函数指针

auto sum = std::make_shared<std::unique_ptr< std::function<int(int)> >>();
*sum = std::make_unique<std::function<int(int)>>(
    [=](int n) {
        return n == 1 ? 1 : n + (**sum)(n - 1);
    }
);

std::cout << (**sum)(100);

🖋️ 2.4、使用Y组合子

构造一个Y组合子如下:

const auto& y = [](const auto& f) {
	  return [&](const auto& x) {
		    return x(x);
	  }([&](const auto& x) -> std::function<int(int)> {
		    return f([&](const int& n) {
			      return x(x)(n);
		    });
	  });
};

再实现一个求和函数的高阶函数,然后连接即可:

const auto& sum = [](const auto& f) {
		return [=](const int& n) {
			  return n == 1 ? 1 : n + f(n - 1);
		};
};

std::cout << y(sum)(100);

最后更新于