《C++ Primer Plus》第18章:探讨 C++ 新标准(4)

Lambda 函数

见到术语 lambda 函数(也叫 lambda 表达式,常简称为 lambda)时,您可能怀疑 C++11 添加这项新功能旨在帮助编程新手。看到下面的 lambda 函数示例后,您可能坚定了自己的怀疑:

[&count] (int x) { count += (x % 13 == 0); }

但 lambda 函数并不像看起来那么晦涩难懂,它们提供了一种有用的服务,对使用函数谓词的 STL 来说尤其如此。

比较函数指针、函数符和 Lamdda 函数

来看一个示例,它使用三种方法给 STL 算法传递信息:函数指针、函数符和 lambda。出于方便的考虑,将这三种形式通称为函数对象,以免不断地重复”函数指针、函数符或 lambda”。假设您要生成一个随机整数列表,并判断其中多少个整数可被3整除,多少个整数可被13整除。

生成这样的列表很简单。一种方案是,使用 vector<int> 存储数字,并使用 STL 算法 generate() 在其中填充随机数:

#include<vector>
#include<algorithm>
#include<cmath>
...
std::vector<int> numbers(1000);
std::generate(vector.begin(), vector.end(), std::rand);

函数 generate() 接受一个区间(由前两个参数指定),并将每个元素设置为第三个参数返回的值,而第三个参数是一个不接受任何参数的函数对象。在上述示例中,该函数对象是一个指向标准函数 rand() 的指针。

通过使用算法 count_if(),很容易计算出有多少个元素可被3整除。与函数 generate() 一样,前两个参数应指定区间,而第三个参数应是一个返回 true 或 false 的函数对象。函数 count_if() 计算这样的元素数,即它使得指定的函数对象返回 true。为判断元素能否被 3 整除,可使用下面的函数定义:

bool f3(int x) { return x % 3 == 0; }

同样,为判断元素能否被13整除,可使用下面的函数定义:

bool f13(int x) { return x % 13 == 0; }

定义上述函数后,便可计算符合条件的元素数了,如下所示:

int count3 = std::count_if(numbers.begin(), numbers.end(), f3);
cout << "Count of numbers divisible by 3: " << count3 << '\n';
int count3 = std::count_if(numbers.begin(), numbers.end(), f13);
cout << "Count of numbers divisible by 13: " << count13 << "\n\n";

下面复习一下如何使用函数符来完成这个任务。第16章介绍过,函数符是一个类对象,并非只能像函数名那样使用它,这要归功于类方法 operator()()。就这个示例而言,函数符的优点之一是,可使用同一个函数符来完成这两项计数任务。下面是一种可能的定义:

class f_mod {
private:
	int dv;
public:
	f_mod(int d = 1) : dv(d) {}
	bool operator() (int x) { return x % dv == 0; }
};

这为何可行呢?因为可使用构造函数创建特定整数值的 f_mod 对象:

f_mod obj(3);		// f_mod.dv set to 3

而这个对象可使用方法 operator() 来返回一个 bool 值:

bool is_div_by_3 = obj(7);		// same as obj.operator() (7)

构造函数本身可用作诸如 count_if() 等函数的参数:

count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3) );

参数 f_mod(3) 创建一个对象,它存储了值 3;而 count_if() 使用该对象来调用 operator() (),并将参数 x 设置为 numbers 的一个元素。要计算有多少个数字可被13(而不是3)整除,只需将第三个参数设置为 f_mod(13)。

最后,来看看使用 lambda 的情况。名称 lambda 来自 lambda calculus(λ演算)—— 一种定义和应用函数的数学系统。这个系统让您能够使用匿名函数——即无需给函数命名。在 C++11 中,对于接受函数指针或函数符的函数,可使用匿名函数定义(lambda)作为其参数。与前述函数 f3() 对应的 lambda 如下:

[](int x) { return x % 3 == 0; }

这与 f3() 的函数定义很像:

bool f3(int x) { return x % 3 == 0; }

差别有两个:使用 [] 替代了函数名(这就是匿名的由来);没有声明返回类型。返回类型相当于使用 decltype 根据返回值推断得到的,这里为 bool。如果lambda 不包含返回语句,推断出的返回类型将为 void。就这个示例而言,您将以如下方式使用该 lambda:

count3 = std::count_if(numbers.begin(),numbers.end(),[](int x) { return x % 3 == 0; } );

也就是说,使用整个 lambda 表达式替换函数指针或函数符构造函数。
仅当 lambda 表达式完全由一条返回语句组成时,自动类型推断才管用;负责需要使用新增的返回类型后置语法:

[](double x) -> double{ 
	int y = x; 
	return x - y; 
} 	// return type is double

下面的程序演示了前面讨论的各个要点:

// lambda0.cpp -- using lambda expressions
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <ctime>

const long Size1 = 39L;
const long Size2 = 100*Size1;
const long Size3 = 100*Size2;
bool f3(int x) { return x % 3 == 0; }
bool f13(int x) { return x % 13 == 0; }

int main() {
    using std::cout;
    std::vector<int> numbers(Size1);

    std::srand(std::time(0));
    std::generate(numbers.begin(), numbers.end(), std::rand);

    // using function pointers
    cout << "Sample size = " << Size1 << '\n';

    int count3 = std::count_if(numbers.begin(), numbers.end(), f3);
    cout << "Count of numbers divisible by 3: " << count3 << '\n';
    int count13 = std::count_if(numbers.begin(), numbers.end(), f13);
    cout << "Count of numbers divisible by 13: " << count13 << "\n\n";

    // increase number of numbers
    numbers.resize(Size2);
    std::generate(numbers.begin(), numbers.end(), std::rand);
    cout << "Sample size = " << Size2 << '\n';

    // using a functor
    class f_mod {
    private:
        int dv;
    public:
        f_mod(int d = 1) : dv(d) {}
        bool operator()(int x) { return x % dv == 0; }
    };

    count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3));
    cout << "Count of numbers divisible by 3: " << count3 << '\n';
    count13 = std::count_if(numbers.begin(), numbers.end(), f_mod(13));
    cout << "Count of numbers divisible by 13: " << count13 << "\n\n";

    // increase number of numbers again
    numbers.resize(Size3);
    std::generate(numbers.begin(), numbers.end(), std::rand);
    cout << "Sample size = " << Size3 << '\n';

    // using lambdas
    count3 = std::count_if(numbers.begin(), numbers.end(), [](int x){return x%3==0;});
    cout << "Count of numbers divisible by 3: " << count3 << '\n';
    count13 = std::count_if(numbers.begin(), numbers.end(), [](int x){return x%13==0;});
    cout << "Count of numbers divisible by 13: " << count13 << '\n';

    return 0;

}

下面是该程序的输出示例:

Sample size = 39
Count of numbers divisible by 3: 15
Count of numbers divisible by 13: 2

Sample size = 3900
Count of numbers divisible by 3: 1318
Count of numbers divisible by 13: 321

Sample size = 390000
Count of numbers divisible by 3: 129943
Count of numbers divisible by 13: 29912

输出表明,样本很小时,得到的统计数据并不可靠。

为何使用 lambda

您可能会问,除那些表达式狂热爱好者,谁会使用 lambda 呢?下面从 4 个方面探讨这个问题:距离、简洁、效率和功能。

很多程序员认为,让定义位于使用的地方附近很有用。这样,就无需翻阅多页的源代码,以了解函数调用 count_if() 的第三个参数了。另外,如果需要修改代码,涉及的内容都将在附近;而剪切并粘贴代码以便在其他地方使用时,涉及的内容也在一起。从这种角度看,lambda 是理想的选择,因为其定义和使用是在同一个地方进行的;而函数是最糟糕的选择,因为不能在函数内部定义其他函数,因此函数的定义可能离使用它的地方很远。函数符是不错的选择,因为可在函数内部定义类(包含函数符类),因此定义离使用地点可以很近。

从简洁的角度看,函数符代码比函数和 lambda更繁琐。函数和 lambda 的简洁程度相当,一个显而易见的例外是,需要使用同一个 lambda 两次:

count1 = std::count_if(n1.begin(), n1.end(), [](int x) { return x % 3 == 0; });
count2 = std::count_if(n12.begin(), n2.end(), [](int x) { return x %3 == 0; });

但并非必须编写 lambda 两次,而可给 lambda 指定一个名称,并使用该名称两次:

auto mod3 = [](int x){ return x % 3 == 0; } 	// mod3 a name for the lambda
count1 = std::count_if(n1.begin(), n1.end(), mod3);
count2 = std::count_if(n2.begin(), n2.end(), mod3);

您甚至可以像使用常规函数那样使用有名称的 lambda:

bool result = mod(z);		// result is true if z % 3 == 0

然而,不同于常规函数,可在函数内部定义有名称的 lambda。mod3 的实际类型随实现而异,它取决于编译器使用什么类型来跟踪 lambda。

这三种方法的相对效率取决于编译器内联哪些东西。函数指针方法阻止了内联,因为编译器传统上不会内联其地址被获取的函数,因为函数地址的概念意味着非内联函数。而函数符和 lambda 通常不会阻止内联。

最后,lambda 有一些额外的功能。具体地说,lambda 可访问作用域内的任何动态变量;要捕获要使用的变量,可将其名称放在中括号内。如果只指定了变量名,如[z],将按值访问变量;如果在名称前加上 &,如 [&count],将按引用访问变量。[&] 让您能够按引用访问所有动态变量,而 [=] 让您能够按值访问所有动态变量。还可混合使用这两种方式,例如,[ted,&ed] 让您能够按值访问 ted 以及按引用访问 ed, [&, ted] 让您能够安置访问 ted 以及按引用访问其他所有动态变量。在之前的程序中,可将下述代码:

int count13;
...
count13 = std::count_if(numbers.begin(), numbers.end(), [](int x) {return x % 13 == 0; } );

替换为如下代码:

int count13;
...
count13 = std::for_each(numbers.begin(), numbers.end(), [&count13](int x) {count13 += x % 13 == 0;} );

[&count13] 让 lambda 能够在其代码中使用 count13。由于 count13 是按引用捕获的,因此在 lambda 对 count13 所做的任何修改都将影响原始 count13。如果 x 能被 13 整除,则表达式 x % 13 == 0 将为 true,添加到 count13 中时,true 将被转换为 1。同样,false 将被转换为 0。因此,for_each() 将 lambda 应用于 numbers 的每个元素后,count13 将为能被 13 整除的元素数。

通过利用这种技术,可使用一个 lambda 表达式计算可被 3 整除的元素数和可被 13 整除的元素数:

int count3 = 0;
int count3 = 0;
std::for_each(numbers.begin(), numbers.end(), [&](int x) { count3 += x % 3 == 0; count13 += x % 13 == 0; });

在这里,[&] 让您能够在 lambda 表达式中使用所有的自动变量,包括 count3 和 count13。

下面的程序演示了如何使用这些技术。

// lambda1.cpp -- use captured variables
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <ctime>

const long Size = 390000L;

int main() {
    
    using std::cout;
    std::vector<int> numbers(Size);

    std::srand(std::time(0));
    std::generate(numbers.begin(), numbers.end(), std::rand);
    cout << "Sample size = " << Size << '\n';

    // using lambdas
    int count3 = std::count_if(numbers.begin(), numbers.end(), [](int x){ return x % 3 == 0; });
    cout << "Count of numbers divisible by 3: " << count3 << '\n';
    int count13 = 0;
    std::for_each(numbers.begin(), numbers.end(), [&count13](int x){count13 += x % 13 == 0 ;});
    cout << "Count of numbers divisible by 13: " << count13 << '\n';

    // using a single lambda
    count3 = count13 = 0;
    std::for_each(numbers.begin(), numbers.end(), 
        [&](int x){count3 += x % 3 == 0; count13 += x % 13 == 0;});
    
    cout << "Count of numbers divisible by 3: " << count3 << '\n';
    cout << "Count of numbers divisible by 13: " << count13 << '\n';

    return 0;
}

下面是该程序的输出:

Sample size = 390000
Count of numbers divisible by 3: 130073
Count of numbers divisible by 13: 29996
Count of numbers divisible by 3: 130073
Count of numbers divisible by 13: 29996

输出表明,该程序使用的两种方法(两个独立的 lambda 和 单个 lambda)的结果相同。
在 C++ 中引入 lambda 的主要目的是,让您能够将类似于函数的表达式用作接受函数指针或函数符的函数的参数。因此,典型的 lambda 是测试表达式或比较表达式,可编写为一条返回语句。这使得 lambda 简洁而易于理解,且可自动推断返回类型。然而,有创意的 C++ 程序员可能开发出其他用法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值