《C++ Primer中文版》第十章:泛型算法

 

第十章:泛型算法

泛型算法(generic algorithm): 称它们为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索:称它们是“泛型的”,是因为它们可以用于同类型的元素和多种容器类型(不仅包括标准库类型,如vector或list,还包括内置的数组类型),以及我们将看到的,还能用于其他类型的序列。

 

10.1概述

大多数算法都定义在头文件algorithm。标准库还在头文件numeric中定义了一组数值泛型算法
一般情况下, 这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
	vector <int> vec{1,2,3,4,5,6,7,8,9};
	int val = 5; //我们将查找的值
	//如果在vec中找到想要的元素,则返回结果指向它,否则返回结果为vec.cend ()
	auto result = find(vec.cbegin(), vec.cend(), val);
	//报告结果
	cout << "The value" << val
		<< (result == vec.cend() ? " is not present" : "is present") << endl;

	return 0;
}

它返回指向第一个等于给定值的元素的迭代器如果范围中无匹配元素,则find返回第二个参数来表示搜索失败。因此,我们可以通过比较返回值和第二个参数来判断搜索是否成功。我们在输出语句中执行这个检测,其中使用了条件运算符来报告搜索是否成功。

string val = "a value"; // 我们要查找的值
//此调用在list中查找string元素
auto result = find(lst.cbegin(),lst.cend(),val);


内置数组:
int ia[] = {27,210,12,47,09, 83} ;
int val = 83;
int* result = find (begin(ia),end(ia) ,val) ;

//在从ia[1]开始,直至(但不包含) ia[4]的范围内查找元素
auto  result = find(ia + 1,ia + 4,val) ;

算法如何工作
1.访问序列中的首元素。
2.比较此元素与我们要查找的值。
3.如果此元素与我们要查找的值匹配,find 返回标识此元素的值。
4.否则,find前进到下一个元素,重复执行步骤2和3。
5.如果到达序列尾,find 应停止。
6.如果find到达序列末尾,它应该返回一个指出元素未找到的值。此值和步骤3返回的值必须具有相容的类型。

迭代器令算法不依赖于容器,但算法依赖于元素类型的操作

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;

int main(int argc, char *argv[])
{
	ifstream in(argv[1]);
	if (!in) {
		cout << "打开输入文件失败!"<< endl;
		exit(1);
	}

	vector<int> vi;
	int val;
	while (in >> val)
		vi.push_back(val);

	cout << "请输入要搜索的整数: ";
	cin >> val;

	cout << "序列中包含"<< count (vi .begin(), vi.end(), val)
		<< "个"<< val;
	return 0;
}

10.2   初识泛型算法

除了少数例外,标准库算法都对一个范围内的元素进行操作。元素范围称为“输入范围”接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。

10.2.1只读算法

只读算法是accumulate,它定义在头文件numeric中

accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。

假定vec是一个整数序列,则:


//对vec中的元素求和,和的初值是0
int sum = accumulate (vec.cbegin(), vec.cend() , 0) ;

这条语句将sum设置为vec中元素的和,和的初值被设置为0。

算法和元素类型

由于string 定义了+运算符,所以我
们可以通过调用accumulate来将vector中所有string元素连接起来:

string sum = accumulate (v.cbegin(),v.cend(), string("")) ;
//错误: const char*上没有定义+运算符

操作两个序列的算法

// roster2中的元素数目应该至少与rosterl一样多
equal (roster1.cbegin(), rosterl.cend() ,roster2.cbegin()) ;

由于equal利用迭代器完成操作,因此我们可以通过调用equal来比较两个不同类型的容器中的元素。而且,元素类型也不必一样,只要我们能用==来比较两个元素类型即可。

在此例中,roster1可以是vector<string>,而roster2 是list<constchar*>。

equal基于一个非常重要的假设:它假定第二个序列至少与第一个序列一样长。此算法要处理第一个序列中的每个元素,它假定每个元素在第二个序列中都有一个与之对应的元素。

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;

int main(int argc, char *argv[])
{
	char *p[] = { "Hello", "World", "!" };
	char *q[] = { _strdup(p[0]), _strdup(p[1]), _strdup(p[2]) };
	char *r[] = { p[0],p[1], p[2] };
	cout << equal( begin(p),end(p), q) << endl;
	cout << equal( begin(p),end(p), r) << endl;
	return 0;

}

 

10.2.2写容器元素的算法

例如,算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数.fill将给定的这个值赋予输入序列中的每个元素。

fill (vec . begin(), vec.end(), 0); // 将每个元素重置为0
//将容器的一个子序列设置为10
fill (vec.begin(), vec.begin() + vec.size()/2, 10) ;

由于fill向给定输入序列中写入数据,因此,只要我们传递了一个有效的输入序列,写入操作就是安全的。

算法不检查写操作
一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。

vector<int> vec; //空vector

//使用vec,赋予它不同值
fill_n(vec.begin(), vec.size(), 0); //将所有元素重置为0

函数fill_n假定写入指定个元素是安全的。即,如下形式的调用
fill_n(dest, n, val) 
fill_n假定dest指向一个元素,而从dest开始的序列至少包含n个元素。
vector<int> vec; // 空向量
//灾难:修改vec中的10个(不存在)元素

fill_n( vec.begin() ,10,0) ;
这个调用是一场灾难。我们指定了要写入10个元素,但vec中并没有元素一它是空的。
这条语句的结果是未定义的。

介绍back_inserter
一种保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器( insert .iterator)。插入迭代器是一种向容器中添加元素的迭代器。通常情况,当我们通过一个迭代器向容器元素赋值时,值被赋予迭代器指向的元素。而当我们通过一个插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。
back_ inserter, 它是定义在头文件iterator中的一个函数
back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:

vector<int> vec;  //空向量
auto it = back_inserter(vec); //通过它赋值会将元素添加到vec中
*it = 42;    // vec 中现在有一个元素,值为42
我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。例如:

vector<int> vec; // 空向量

//正确: back_inserter 创建一个插入迭代器,可用来向vec添加元素
fill_n (back_inserter(vec),10,0); //添加10个元素到vec

拷贝算法
拷贝(copy)算法是另一个向目的位置迭代器指向的输出序列中的元素写入数据的算法。此算法将输入范围中的元素拷贝到目的序列中。传递给copy的目的序列至少要包含与输入序列一样多的元素

int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(al)/sizeof(*a1)]; // a2与al大小一样

// ret指向拷贝到a2的尾元素之后的位置
auto ret = copy (begin(al), end(al) , a2); // 把al的内容拷贝给a2

copy返回的是其目的位置迭代器(递增后)的值。ret恰好指向拷贝到a2的尾元素之后的位置。


多个算法都提供所谓的“拷贝”版本。这些算法计算新元素的值,但不会将它们放置在输入序列的末尾,而是创建一个新序列保存这些结果。
例如,replace 算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。

此算法接受4个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值:

//将所有值为0的元素改为42
replace (ilst.begin(),ilst.end(), 0,42);

此调用将序列中所有的0都替换为42。如果我们希望保留原序列不变,可以调用replace_copy。 此算法接受额外第三个迭代器参数,指出调整后序列的保存位置:

//使用back_inserter按需要增长目标序列
replace_copy (ilst.cbegin(), ilst.cend() , back_inserter(ivec), 0,42) ;

此调用后,ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中值为0的元素在ivec中都变为42。

要求目标序列至少要包含与源序列一样多的元素:

而此程序中,vec:进行缺省初始化,它是空的,copy 无法进行。如需改变容器大小,需要使用一类特殊的称为插入器的迭代器。我们可以将第三个参数改为back_inserter(vec),通过它,copy算法即可将lst中元素的拷贝插入到vec的末尾

(b)这段程序仍然是错误的。粗看起来,reserve为vec分配了至少能容纳10个int的内存空间,调用fill_n时,vec已有足够空间。但泛型算法对于容器的要求并不是有足够的空间,而是足够的元素。此时vec仍然为空,没有任何元素。而算法又不具备向容器添加元素的能力,因此fill_ n仍然失败。这里,需要back_ inserter 来让fill_ n有能力向vec添加元素。其实,只有0有能力做到这一点,空间大小并不是问题,容器都能根据需要自动扩容。

练习10.8: 本节提到过,标准库算法不会改变它们所操作的容器的大小。为什么使用back inserter 不会使这一断言失效? 
[出题思路]

深入理解泛型算法的这一特点。
[解答]
严格来说,标准库算法根本不知道有“容器”这个东西。它们只接受迭代器参数,运行于这些迭代器之上,通过这些迭代器来访问元素。
因此,当你传递给算法普通迭代器时,这些迭代器只能顺序或随机访问容器中的元素,造成的效果就是算法只能读取元素、改变元素值、移动元素,但无法添加或删除元素。
但当我们传递给算法插入器,例如back_ inserter 时,由于这类迭代器能调用下层容器的操作来向容器插入元素,造成的算法执行的效果就是向容器中添加了元素。
因此,关键要理解:标准库算法从来不直接操作容器,它们只操作迭代器,从而间接访问容器。能不能插入和删除元素,不在于算法,而在于传递给它们的迭代器是否具有这样的能力。
 

10.2.3 重排容器元素的算法

输入:

我们的程序应该生成如下vector :

void elimDups (vector<string> &words)
{
    //按字典序排序words,以便查找重复单词
    sort (words.begin(),words.end() ) ;
    // unique 重排输入范围,使得每个单词只出现一次
    //排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique (words.begin(),words.end()) ;
    //使用向量操作erase删除重复单词
    words.erase (end_unique, words.end()) ;
}

排序整个vector.  完成sort后,words的顺序如下所示:

unique算法重排输入序列,将相邻的重复项“消除”,并返回一个指向不重复值范围末尾的迭代器。调用unique后,vector将变为:

unique 返回的迭代器指向最后一个不重复元素之后的位置。此位置之后的元素仍然存在,但我们不知道它们的值是什么。

使用容器操作删除元素

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
inline void output_words(vector<string> &words)
{
	for (auto iter = words.begin(); iter != words.end(); iter++)
		cout << *iter << " ";
	cout << endl;

}

void elimDups(vector<string> &words)
{
	output_words(words);

	sort(words.begin(),words.end());
	output_words(words);

	auto end_unique = unique(words.begin(),words.end());
	output_words(words);

	words.erase(end_unique, words.end());
	output_words(words);
}

int main()
{
	vector<string> words = {"ilove" ,"he" ,"ilove","he","kl","kk"};

	elimDups(words);

}

10.3 定制操作

sort 算法默认使用元素类型的<运算符。但可能我们希望的排序顺序与<所定义的顺序不同或是我们的序列可能保存的是未定义<运算符的元素类型在这两种情况下,都需要重载sort的默认行为。

10.3.1向算法传递函数
谓词一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词 (unary predicate, 意味着它们只接受单一参数)二元谓词( binary predicate,意味着它们有两个参数)接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
接受一个二元谓词参数的sort版本用这个谓词代替<来比较元素。我们提供给sort的谓词必须满足条件。

//比较函数,用来按长度排序单词
bool isShorter (const string &s1, const string &s2)
{
     return s1.size() < s2.size () ;
}


//按长度由短至长排序words
sort (words.begin(), words.end(), isShorter) ;

排序算法
在我们将words按大小重排的同时,还希望具有相同长度的元素按字典序排列。为了保持相同长度的单词按字典序排列,可以使用stable_ sort 算法。这种稳定排序算法维持相等元素的原有顺序。

elimDups (words); // 将words按字典序重排,并消除重复单词
//按长度重新排序,长度相同的单词维持字典序
stable_sort (words.begin(),words.end() , isShorter) ;
for (const auto &s : words) // 无须拷贝字符串
     cout<<s<<" ";//打印每个元素,以空格分隔
cout << endl ;

练习10.13:标准库定义了名为partition的算法,它接受一个谓词,对容器内容进行划分,使得谓词为true的值会排在容器的前半部分,而使谓词为false的值会排在后半部分。算法返回一个迭代器,指向最后一个使谓词为true的元素之后的位置。

编写函数,接受一个string,返回一个bool值,指出string是否有5个或更多字符。使用此函数划分words。打印出长度大于等于5的元素。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

inline void output_words(vector<string>::iterator beg, vector <string>::iterator end)
{
	for (auto iter = beg; iter != end; iter++)
		cout << *iter << " ";
    cout << endl;
}

bool five_or_more(const string &s1)
{
	return s1.size() >= 5;
}


int main()
{
	vector<string> words;
	string word;
	while (cin >> word)
		words.push_back(word);
	output_words(words.begin(),words.end());
	auto iter = partition( words.begin(),words.end(),five_or_more);
	output_words(words.begin(),iter);

	return 0;
}

10.3.2  lambda表达式

void biggies (vector<string> &words , vector<string>::size_type sz)
{
    elimDups ( words ); // 将words按字典序排序,删除重复单词
    //按长度排序,长度相同的单词维持字典序.
    stable_sort ( words.begin() ,  words.end() , isShorter) ;
    //获取一个迭代器,指向第一个满足size()>= sz的元素
    //计算满足size >= sz的元素的数目
    // 打印长度大于等 于给定值的单词,每个单词后面接一个空格
}


结尾有完整的代码...

 

介绍lambda
向一个算法传递任何类别的可调用对象( callable object)。 对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一一个或多个参数的列表。
使用过的两种可调用对象是函数和函数指针。还有两种可调用对象:重载了函数调用运算符的类,以及lambda表达式( lambda expression )
一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。

与任何函数类似,一个lambda具有—— 一个返回类型一个参数列表一个函数体。但与函数不同,lambda 可能定义在函数内部。一个lambda表达式具有如下形式

[capture list] (parameter list) - > return type { function body }

其中capturelist(捕获列表) 是一个lambda所在函数中定义的局部变量的列表(通常为空);

return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda 必须使用尾置返回来指定返回类型。我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体

auto f =[] { return 42; };
lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符:
cout << f() << endl; //打印42

在lambda中忽略括号和参数列表等价于指定一个空参数列表。当调用f时,参数列表是空的。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只是一个return语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为void.

使用捕获列表

一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引 lambda在其内部包含访问局部变量所需的信息。
lambda 会捕获z,并只有单一的string参数。其函数体会将string的大小与捕获的sz的值进行比较:

[sz] (const string &a)
     { return a.size () >= sz;};

lambda以一对[]开始,可以在其中提供一个以逗号分隔的名字列表,这些名字都是它所在函数中定义的。
lambda 不捕获words,因此不能访问此变量。代码会编译错误:

//错误: sz未捕获
[] (const string &a)
  { return a.size () >= sz;} ;

调用find_if

查找第一个长度大于等于sz 的元素:

//获取一个迭代器,指向第一个满足size()>= sz的元素
auto wc = find_if (words.begin(), words.end() ,
              [sz] (const string &a)
                         { return a.size() >= sz; }) ;

这里对find_ if的调用返回一个迭代器,指向第一个长度不小于给定参数sz的元素。如果这样的元素不存在,则返回words.end()的一个拷贝。
使用find_ if返回的迭代器来计算从它开始到words的末尾一-共有多少个元素:

//计算满足size >= sz的元素的数目.
auto count = words.end() - wc;
cout << count <<" " << make_plural (count,"word", "s")
     <<"of  length "<< sz << "or longer"<<endl;

我们的输出语句调用make_ plural 来输出“word”或“words",具体输出哪个取决于大小是否等于1。

 

for_ each 算法

//打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each ( wc , words.end() ,
   [] (const string &s) {cout << s <<" ";}) ;
cout << endl;

捕获列表为空,是因为我们只对lambda所在函数中定义的(非static)变量使用捕获列表。一个lambda可以直接使用定义在当前函数之外的名字。

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

//如果ctr的值大于1,返回word的复数形式
string make_plural(size_t ctr,const string &word ,
	const string &ending)
{
	return (ctr > 1) ? word + ending : word;
}


inline void output_words(vector<string> &words)
{
	for (auto iter = words.begin(); iter != words.end(); iter++)
		cout << *iter << " ";
	cout << endl;

}

void elimDups(vector<string> &words)
{
	output_words(words);

	sort(words.begin(), words.end());
	output_words(words);

	auto end_unique = unique(words.begin(), words.end());
	output_words(words);

	words.erase(end_unique, words.end());
	output_words(words);
}
void biggies(vector<string> &words,	vector<string>::size_type sz)
{
	elimDups(words); // 将words按字典序排序,删除重复单词
	
   //按长度排序,长度相同的单词维持字典序
	stable_sort(words.begin(),words.end(),
		   [](const string &a , const string &b)
	           { return a.size() < b.size(); });
	//获取一个迭代器,指向第一个满足size()>= sz的元素
	auto wc = find_if (words.begin(), words.end() ,
		     [sz](const string &a)
	            { return a.size() >= sz; });
	//计算满足size >= sz的元素的数目
	auto count = words.end() - wc;
	cout << count << " " << make_plural(count,"word", "s")
	<< "oflength" << sz << "orlonger" << endl;
	//打印长度大于等于给定值的单词,每个单词后面接一个空格
	for_each(wc, words.end(),
		 [](const string &s) { cout << s << " "; });
	cout << endl;
}

例题:

#include <iostream>

using namespace std;
int main()
{
	auto sum = [](int a , int b) { return a + b; };
	cout << sum(1, 1) << endl;
	return 0;
}
#include <iostream>
using namespace std;
void add(int a)
{
	auto sum = [a](int b) {return a + b; };
	cout << sum(1) << endl;
}
int main(int argc, char *argv[])
{
	add(1);
	add(2);
	return 0;
}

练习10.16 : 使用lambda编写你自己版本的biggies。

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;


inline void output_words(vector<string> &words)
{
	for (auto iter = words.begin(); iter != words.end(); iter++)
		cout << *iter << " ";
	cout << endl;

}

//如果ctr的值大于1,返回word的复数形式
string make_plural(size_t ctr, const string &word,
	const string &ending)
{
	return (ctr > 1) ? word + ending : word;
}

void elimDups(vector<string> &words)
{
	output_words(words);

	sort(words.begin(), words.end());
	output_words(words);

	auto end_unique = unique(words.begin(), words.end());
	output_words(words);

	words.erase(end_unique, words.end());
	output_words(words);
}
void biggies(vector<string> &words, vector<string>::size_type sz)
{
	elimDups(words); // 将words按字典序排序,删除重复单词
	for_each(words.begin(),words.end() ,
		[](const string &s) {
		    cout << s << " ";
	});
	cout << endl;
	//获取一个迭代器,指向最后一个满足size()>= sz的元素之后的位置
	auto wc = partition(words.begin(), words.end(),
		        [sz](const string &a)
	{
		return a.size() >= sz;
	});
	//计算满足size >= sz的元素的数目.
	auto count = wc - words.begin();
	cout << count << " " << make_plural(count, "word", "s")
		<< "oflength" << sz <<"orlonger"<< endl;
		//打印长度大于等于给定值的单词,每个单词后面接一空格
}


int main(int argc, char *argv[])
{
	ifstream in(argv[1]);
	if (!in) {
		cout << "打开输入文件失败! " << endl;
		exit(1);
	}
	vector<string> words;
	string word;
	while (in >> word)
		words.push_back(word);
	biggies(words, 4);
	return 0;

}

10.3.3  lambda捕获和返回

当定义一个lambda时,编译器生成一个 与lambda对应的新的(未命名的)类类型。
目前,可以这样理解,当向一个函数传递一个lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化

值捕获

变量的捕获方式也可以是值或引用。到目前为止,我们的lambda采用值捕获的方式。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝:

void fcn1 ()
{
  size_t v1 = 42; //局部变量
  //将v1拷贝到名为f的可调用对象.
  auto f = [v1]{return v1;};
  v1 = 0;
  auto j = f();//于为42;f保存了我们创建它时v1的拷贝
}

由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。

 

引用捕获

void fcn2 ()
{
    size_t v1 = 42; // 局部变量
    //对象f2包含v1的引用
    auto f2 = [&v1] { return vl; } ;
    v1 = 0;
    auto j = f2();//j为0;f2保存v1的引用,而非拷贝
}

当我们在lambda 函数体内使用此变量时,实际上使用的是引用所绑定的对象。在本例中,当lambda返回v1时,它返回的是v1指向的对象的值。
如果我们采用引用方式捕获一个变量, 就必须确保被引用的对象在lambda执行的时候是存在的lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

引用捕获有时是必要的。例如,我们可能希望biggies函数接受一个。ostream的引用,用来输出数据,并接受一个字符作 为分隔符:

void biggies (vector<string> &words,
              vector<string>::size_type sz,
               ostream &os = cout, char c = ' ')
{
    //与之前例子一样的重排words的代码
    // 打印count的语句改为打印到os
    for_each (words.begin(),words.end() ,
    [&os, c](const string &s){ os << s << c;});
}

我们不能拷贝ostream对象,因此捕获os的唯一方法就是捕获其引用(或指向os的指针)。

我们也可以从一个函数返回lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获

隐式捕获

编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。例如,我们可以重写传递给find_if的lambda:

// sz为隐式捕获,值捕获方式
wc= find_if (words .begin(), words.end() ,
               [=] (const string &s)
                    { return s.size() >= sz; }) ;

混合使用隐式捕获和显式捕获:

void biggies (vector<string> & words, vector<string>::size_type sz,
                 ostream &os = cout, char c = ' ')
{
    //其他处理与前例一样
    // os隐式捕获,引用捕获方式; c显式捕获,值捕获方式
    for_each (words.begin(), words.end() ,
                [ & , c](const string &s){ os << s << c;});
    // os显式捕获,引用捕获方式; c隐式捕获,值捕获方式
    for_each (words.begin(), words.end() ,
                [ = , &os]( const string &s ){ os << s << c ;});
}

混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是 一个&或= 此符号指定了默认捕获方式为引用或值。

当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用&。如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&

可变lambda
默认情况下,对于一个值被拷贝的变量, lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:

void fcn3 ()
{
    size_t v1 = 42; //局部变量
    // f可以改变它所捕获的变量的值
    auto f = [v1] () mutable { return ++v1; } ;
    v1 = 0;
    auto j = f();//j为43
}

一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个 const
类型还是一个非const类型:

void fcn4 ()
{
    size_t v1 = 42; //局部变量
    // v1是一个非const变量的引用
    //可以通过f2中的引用来改变它
    auto f2 = [&v1] { return ++v1; } ;
    v1=0;
    auto j = f2(); //j为1
}

指定lambda返回类型
将一个序列中的每个负数替换为其绝对值:.

transform(vi.begin(), vi.end() , vi.begin(),
                [](inti){return i < 0 ? -i : i;});

但是,如果我们将程序改写为看起来是等价的if语句,就会产生编译错误: 

//错误:不能推断lambda的返回类型
transform(vi.begin(), vi.end(), vi.begin() ,
     [](int i) { if( i <0 ) return -i; else return i;});
编译器推断这个版本的lambda返回类型为void,但它返回了一个int值。

当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型

transform(vi.begin(), vi.end(), vi.begin(),
           [] (int i) -> int
   {if ( i <  0) return -i; else return i;});

10.3.4参数绑定

对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的如果我们需要在很多地方使用相同的操作,通常应该定义一个函数, 而不是多次编写相同的lambda 表达式。如果一个操作需要很多语句才能完成,通常使用函数更好。
如果lambda 的捕获列表为空,通常可以用函数来代替它。
但是,对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。例如,我们用在find_if调用中的lambda比较一个string和一个给定大小。我们可以很容易地编写一个完成同样工作的函数:

bool check_size(const string &s,string::size_type sz)
{
    return s.size () >= sz;
}

但是,我们不能用这个函数作为find_ if的一个参数。如前文所示,find_if 接受一个一元谓词, 因此传递给find_ if的可调用对象必须接受单一参数biggies 传递给find_if的lambda使用捕获列表来保存sz。为了用check_size来代替此lambda,必须解决如何向sz形参传递一个参数的问题。
 

标准库bind函数
我们可以解决向check_ size传递个长度参数的问题,方法是使用一个新的名为bind的标准库函数,它定义在头文件functional中。可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式为:auto newCallable = bind (callable, arg_list) ;其中,newCallable 本身是一个可调用对象, arg_ list 是一个逗 号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable 会调用callable,并传
递给它arg_ list 中的参数。

arg_list 中的参数可能包含形如_ n的名字,其中n是一个整数。这些参数是“占位符”,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值