第二十一章:理解函数对象
函数对象(也叫 functor)。
函数对象与谓词的概念
从概念上说,函数对象是用作函数的对象;
但从实现上说,函数对象是实现了 operator() 的类的对象。
虽然函数和函数指针也可归为函数对象,但实现了 operator() 的类的对象才能保存状态(即类的成员属性的值),才能用于标准模板库(STL)算法。
C++程序员常用于 STL 算法的函数对象可分为下列两种类型。
• 一元函数:接受一个参数的函数,如 f(x)。如果一元函数返回一个布尔值,则该函数称为谓词。
• 二元函数:接受两个参数的函数,如 f(x, y)。如果二元函数返回一个布尔值,则该函数称为二元谓词。
所谓几元谓词,这和函数有几个参数有关,并且该函数需要返回 布尔值 才能被称为谓词。
函数对象的典型用途
如何在 C++编程中使用函数对象?
一元函数
只对一个参数进行操作的函数称为一元函数。
使用一元函数将集合的内容显示在屏幕上:
// A unary function
template <typename elementType>
void FuncDisplayElement (const elementType& element)
{
cout << element << ' ';
};
函数 FuncDisplayElement 接受一个类型为模板化类型 elementType 的参数,并使用控制台输出语句 std::cout 将该参数显示出来。该函数也可采用另一种表现形式,即其实现包含在类或结构的operator()中:
// Struct that can behave as a unary function
template <typename elementType>
struct DisplayElement
{
void operator () (const elementType& element) const
{
cout << element << ' ';
}
};
这两种实现都可用于 STL 算法 for_each(),将集合中的内容显示在屏幕上,每次显示一个元素,代码如下:
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
using namespace std;
template<typename elementType>
struct DisplayElement {
void operator()(const elementType& element) const {
cout << element << ' ';
}
};
int main() {
vector<int> numsInVec{ 0, 1, 2, 3, -1, -9, 0, -999 };
cout << "Vector of integers contains: " << endl;
for_each(numsInVec.begin(), numsInVec.end(), DisplayElement<int>());
// Display the list of characters
list<char> charInList{ 'a','z','k','d' };
cout << endl << "List of characters contains: " << endl;
for_each(charInList.begin(), charInList.end(), DisplayElement<char>());
return 0;
}
上面代码中包含了函数对象DisplayElement,它实现了 operator( ) 完成了打印任务。
一元谓词
返回布尔值的一元函数是谓词。这种函数可供 STL 算法用于判断。
谓词判断输入元素是否为初始值的整数倍:
template<typename numberType>
struct IsMultiple {
numberType Divisor;
IsMultiple(const numberType& divisor) {
Divisor = divisor;
}
bool operator() (const numberType& element) const {
// 判断这个数字是否为另一个数字的整数倍
return ((element % Divisor) == 0);
}
};
这里的 operator( )返回布尔值,可用作一元谓词。该结构有一个构造函数,它初始化除数的值。然后用保存在对象中的这个值来判断要比较的元素是否可以被它整除,如 operator( )的实现所示,它使用数学运算取模%来返回除法运算的余数。然后将余数与零进行比较,以判断被除数是否为除数的整数倍。
一元谓词被大量用于 STL 算法中。例如,算法 std::partition()使用一元谓词来划分范围,算法
stable_partition() 也使用一元谓词来划分范围,但保持元素的相对顺序不变。诸如 std::find_if( )等查找函
数以及 std::remove_if( )等删除元素的函数也使用一元谓词,其中 std::remove_if( )删除指定范围内满足谓词条件的元素。
二元函数
如果函数 f(x, y)根据输入参数返回一个值,它将很有用。这种二元函数可用于对两个操作数执行运算,如加、减、乘、除等。
二元谓词
接受两个参数并返回一个布尔值的函数是二元谓词。这种函数常常用于诸如 std::sort( )等 STL 函数中。
对字符串进行不区分大小写排序的二元谓词:
class CompareStringNoCase {
public:
bool operator() (const string& str1, const string& str2) const {
string str1LowerCase;
str1LowerCase.resize(str1.size());
//把每个字符都转换成小写
transform(str1.begin(), str1.end(), str1LowerCase.begin(), ::tolower);
string str2LowerCase;
str2LowerCase.resize(str2.size());
transform(str2.begin(), str2.end(), str2LowerCase.begin(), ::tolower);
return (str1LowerCase < str2LowerCase);
}
};
在 operator( ) 中实现的二元谓词中,首先使用 std::transform( )将输入字符串转换为小写,然后使用字符串的比较运算符 < 进行比较,并返回结果。
很多 STL 算法都使用二元谓词。例如,删除相邻重复元素的 std::unique( )、排序算法 std::sort( )、排序并保持相对顺序的 std::stable_sort( )以及对两个范围进行操作的 std::transform( ),这些 STL 算法都需要使用二元谓词。
总结
本章介绍了函数对象(也叫 functor)。在结构或类中实现函数对象时,它将比简单函数有用得多,因为它也可用于存储与状态相关的信息。本章还介绍了谓词,它是一类特殊的函数对象。另外,还通过一些实际示例说明了谓词的用途。
第二十二章:Lambda 表达式
lambda 表达式是一种定义匿名函数对象的简洁方式,这是 C++11 新增的。
lambda 表达式是什么
可将 lambda 表达式视为包含公有 operator( )的匿名结构(或类),从这种意义上说,lambda 表达式属于第 21 章介绍的函数对象。
深入分析如何编写 lambda 表达式前,先看一个在二十一章中提到过的示例程序:
// struct that behaves as a unary function
template <typename elementType>
struct DisplayElement
{
void operator () (const elementType& element) const
{
cout << element << ' ';
}
};
这个函数对象使用 cout 将 element 显示到屏幕上,通常用于 std::for_each()等算法中:
// Display every integer contained in a vector
for_each (numsInVec.cbegin (), // Start of range
numsInVec.cend (), // End of range
DisplayElement <int> ()); // Unary function object
如果使用 lambda 表达式,可将上述代码(包括函数对象的定义)简化为下述 3 行:
// Display every integer contained in a vector using lambda exp.
for_each (numsInVec.cbegin (), // Start of range
numsInVec.cend (), // End of range
[](const int& element) {cout << element << ' ';});
编译器见到下述 lambda 表达式时:
[] (const int& element) { cout << element << ' '; });
自动将其展开为类似于结构 DisplayElement的表示:
struct NoName
{
void operator () (const int& element) const
{
cout << element << ' ';
}
};
lambda 表达式也叫 lambda 函数 。
如何定义 lambda 表达式
lambda 表达式的定义必须以方括号([])打头。这些括号告诉编译器,接下来是一个 lambda 表达式。方括号的后面是一个参数列表,该参数列表与不使用 lambda 表达式时提供给 operator( )的参数列表相同。
一元函数对应的 lambda 表达式
与一元 operator(Type)对应的 lambda 表达式接受一个参数,其定义如下:
[](Type paramName) {
// lambda expression code here
}
请注意,如果您愿意,也可按引用传递参数:
[] (Type& paramName){
// lambda expression code here
}
在算法 for_each( )中使用 lambda 表达式而不是函数对象来显示容器中的元素:
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
using namespace std;
int main() {
vector<int> numsInVec{ 101, -4, 500, 21, 42, -1 };
list<char> charsList{ 'a', 'h', 'z', 'k', 'l' };
cout << "Display elements in a vector using a lambda: " << endl;
// Display the array of integers
for_each(numsInVec.cbegin(), numsInVec.cend(), [](const int& element) {cout << element << ' '; });
cout << endl;
// Display the list of characters
for_each(charsList.cbegin(), charsList.cend(), [](auto& element) {cout << element << ' '; });
return 0;
}
这里使用了两个 lambda 表达式,这两个 lambda 表达式很像,只是输入参数不同,因为根据两个容器包含的元素类型对它们进行了定制。第一个 lambda 表达式接受一个 int 参数,并使用它来显示整型 vector 中的元素,每次一个;第二个 lambda 表达式接受一个 char 参数(这是编译器自动推断出来的),并使用它来显示 std::list 中的 char 元素。
一元谓词对应的 lambda 表达式
谓词可帮助您做出决策。一元谓词是返回 bool 类型(true 或 false)的一元表达式。lambda 表达式也可返回值,例如,下面的 lambda 表达式在 num 为偶数时返回 true:
[] (int& num) { return ((num % 2) == 0); }
在这里,返回值的性质让编译器知道该 lambda 表达式的返回类型为 bool。
在算法中,可将 lambda 表达式用作一元谓词。
在算法 std::find_if( )中,将 lambda 表达式用作一元谓词,以查找集合中的偶数:
int main()
{
vector<int> numsInVec{ 25, 101, 2017, -50 };
auto evenNum = find_if(numsInVec.cbegin(),
numsInVec.cend(), // range to find in
[](const int& num){return ((num % 2) == 0); } );
if (evenNum != numsInVec.cend())
cout << "Even number in collection is: " << *evenNum << endl;
return 0;
}
输出如下:
Even number in collection is: -50
算法 find_if( )对指定范围内的每个元素调用该一元谓词;如果该谓词返回 true,find_if( )将返回一个指向相应元素的迭代器 evenNum,指出找到了一个满足条件的元素。这里的谓词是一个 lambda 表达式,当 find_if( )使用一个偶数调用它(即对 2 求模的结果为零)时,它将返回 true。
通过捕获列表接受状态变量的 lambda 表达式
在上面的程序中,您创建了一个一元谓词,它在整数能被 2 整除(即为偶数)时返回 true。如果要让它更通用,在数字能被用户指定的除数整除时返回 true,该如何办呢?为此,需要让 lambda 表达式接受该“状态”—除数:
int divisor = 2; // initial value
…
auto element = find_if (begin of a range,
end of a range,
[divisor](int dividend){return (dividend % divisor) == 0; } );
一系列以状态变量的方式传递的参数([…])也被称为 lambda 表达式的捕获列表(capture list)。
使用存储状态的 lambda 表达式来判断一个数字能否被另一个数字整除:
#include <iostream>
#include <algorithm>
#include <vector>
#include <list>
using namespace std;
int main() {
vector <int> numsInVec{ 25, 26, 27, 28, 29, 30, 31 };
cout << "The vector contains: {25, 26, 27, 28, 29, 30, 31}";
cout << endl << "Enter divisor (> 0): ";
int divisor = 2;
cin >> divisor;
// Find the first element that is a multiple of divisor
vector<int>::iterator element;
element = find_if(numsInVec.begin(),
numsInVec.end(),
[divisor](int dividend) {return (dividend % divisor) == 0; });
if (element != numsInVec.end()) {
cout << "First element in vector divisible by " << divisor;
cout << ": " << *element << endl;
}
return 0;
}
divisor 是一个状态变量,相当于第二十一章程序中的 IsMultiple::Divisor,因此状态变量类似于 C++11 之前的函数对象类中的成员。您可以将状态传递给 lambda 表达式,并根据状态的性质相应地使用它。
注意:封闭函数局部变量不能在 lambda 体中引用,除非其位于捕获列表中。
换句话说,如果上面程序中的 lambda 表达式中中括号内的 divisor 变量去掉的话,该表达式后边引用 divisor 的地方就会报错。
lambda 表达式的通用语法
lambda 表达式总是以方括号打头,并可接受多个状态变量,为此可在捕获列表([…])中指定这些状态变量,并用逗号分隔:
[stateVar1, stateVar2](Type& param) { // lambda code here; }
如果要在 lambda 表达式中修改这些状态变量,可添加关键字 multable:
[stateVar1, stateVar2](Type& param) mutable { // lambda code here; }
这样,便可在 lambda 表达式中修改捕获列表([])中指定的变量,但离开 lambda 表达式后,这些修改将无效。要确保在 lambda 表达式内部对状态变量的修改在其外部也有效,应按引用传递它们:
[&stateVar1, &stateVar2](Type& param) { // lambda code here; }
lambda 表达式还可接受多个输入参数,为此可用逗号分隔它们:
[stateVar1, stateVar2](Type1& var1, Type2& var2) { // lambda code here; }
如果要向编译器明确地指定返回类型,可使用->,如下所示:
[stateVar1, stateVar2](Type1 var1, Type2 var2) -> ReturnType
{ return (value or expression ); }
最后,复合语句({})可包含多条用分号分隔的语句,如下所示:
[stateVar1, stateVar2](Type1 var1, Type2 var2) -> ReturnType
{
Statement 1;
Statement 2;
return (value or expression);
}
如果 lambda 表达式包含多行代码,您必须显式地指定返回类型。
总之,lambda 表达式的存在目的就是为了使得代码编写整体更加简洁。
二元函数对应的 lambda 表达式
二元函数接受两个参数,还可返回一个值。与之等价的 lambda 表达式如下:
[...](Type1& param1Name, Type2& param2Name) { // lambda code here; }
二元谓词对应的 lambda 表达式
返回 true 或 false、可帮助决策的二元函数被称为二元谓词。这种谓词可用于 std::sort( )等排序算法中,这些算法对容器中的两个值调用二元谓词,以确定将哪个放在前面。与二元谓词等价的 lambda 表达式的通用语法如下:
[...](Type1& param1Name, Type2& param2Name) { // return bool expression; }
总结
本章介绍了 C++11 新增的一项非常重要的功能:lambda 表达式。lambda 是匿名的函数对象,可接受参数、存储状态、返回值以及跨越多行。您学习了如何在 find( )、sort( )、transform( )等 STL 算法中使用 lambda 表达式,而不是函数对象。lambda 表达式可提高 C++编程速度和效率,应尽可能使用它们。