[C++ Primer Note9] 泛型算法和lambda

标准库容器定义的操作集合惊人的小。标准库并未给每个容器添加大量功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器。这些算法是通用的(generic,或称泛型的):它们可用于不同类型的容器和不同类型的元素。它们实现了一些经典算法的公共接口,比如排序和搜索。

  1. 大多数算法都定义在头文件algorithm中,标准库还在头文件numeric定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。例如,我们有一个int的vector,希望知道vector中是否包含一个特定值,我们可以调用标准库算法find
int val=42;
auto result=find(vec.cbegin(),vec.cend(),val);

该函数返回指向第一个等于给定值的元素的迭代器,否则返回第二个参数表示搜索失败。
由于find操作的是迭代器,因此我们可以用同样的find函数在任何容器中查找值。
同时,由于指针就像内置数组上的迭代器一样,我们可以用find在数组中查找值。

int arr[]={1,2,3,4,5,6,7,8,9,10};
auto result=find(begin(arr),end(arr),10);
  1. 迭代器令算法不依赖于容器,但多数算法都使用了一个或多个元素类型上的操作,例如在find函数中使用了元素类型的==运算符完成了比较。不过,大多数算法提供了一种算法,允许我们使用自定义的操作代替默认的运算符。
  2. 除了少数例外,标准库算法都对一个范围内的元素进行操作。我们将此元素范围称为“输入范围”。接受输入范围的算法总是使用前两个参数来表示此范围,两个参数分别是指向第一个元素和尾元素之后位置的迭代器。虽然大多数算法遍历输入范围的方式相似,但它们使用范围中元素的方式不同。理解算法的最基本方法就是了解它们是否读取元素,改变元素或是重排元素顺序
  3. count算法(返回给定值在序列中出现的次数)
vector<int> v1={1,2,3,4,1};
int cnt=count(v1.cbegin(),v1.cend(),1);
  1. accumulate算法(求和),第三个参数是和的初值
vector<int> v1={1,2,3};
cout<<count(v1.cbegin(),v1.cend(),2}<<endl; //结果为8
  1. equal算法,比较两个序列是否相等。第三个参数是第二个序列的首元素迭代器。第二个序列长度要大于等于第一个
  2. fill算法,给序列中元素赋值
  3. fill_n算法,接受一个单迭代器,一个计数值和一个值,将给定值赋予迭代器指向的元素开始的指定个元素。
vector<int> vec(20);
fill_n(vec.begin(),10,10);

一定不能向空vector写入数值

  1. 向目的位置迭代器写入数据的算法假定目的位置足够大。
  2. 泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作:算法永远不会改变底层容器的大小
  3. 一种保证算法有足够元素空间容纳输出数据的方法是使用插入迭代器(insert iterator)。插入迭代器是一种向容器添加元素的迭代器。
  4. back_inserter(定义在iterator)接受一个指向容器的引用,返回一个与容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back添加元素。
vector<int> vec; //空向量
auto it=back_inserter(vec);
*it=42;   //vec现在有了一个元素

我们常常使用back_inserter来创建一个迭代器并作为算法的目的位置来使用:

vector<int> vec;
fill_n(back_inserter(vec),10,0);   //添加10个元素到vec
  1. copy算法能够实现容器及内置数组的拷贝
int a1[]={1,2,3,4,5};
int a2[sizeof(a1)/sizeof(*a1)];
auto ret=copy(begin(a1),end(a1),a2);

copy返回的是目的位置迭代器(递增后)的值,此处指向a2的尾后元素

  1. 多个算法都提供所谓的拷贝版本,这些版本计算新元素的值,但不会让它们印象原输入序列,而是新建一个序列保存这些结果。
replace(ilst.begin(),ilst.end(),0,42);  //将0替换为42
replace_copy(ilst.begin,ilst.end(),back_inserter(vec),0,42); //按需要增长目标序列
  1. 某些算法会重排容器中元素的顺序,比如sort,它是利用元素类型的<运算符来实现排序的。
  2. 消除重复单词
sort(words.begin(),words.end());
//unique返回指向不重复区域之后一个位置的迭代器
auto end_unique = unique(words.begin(),words.end());
words.erase(end_unique,words.end());

此处的unique并不能通过迭代器来真正实现删除元素,还是要依赖于容器操作。

  1. 很多算法都会比较输入序列的元素,默认情况下,这些算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自定义的操作来代替默认运算符。
  2. 比如sort算法,我们希望优先按照单词长度排序,长度相等再按字典序排列。我们将使用sort的第二个版本,它接受第三个参数,此参数是一个谓词(predicate)
  3. 谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词二元谓词,接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换成谓词的参数类型。
bool isShorter(const string &s1,const string &s2){
    return s1.size()<s2.size();
}
sort(words.begin(),words.end(),isShorter);
  1. 根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。
  2. 我们可以向一个算法传递任何类别的可调用对象,目前为止,我们使用过的仅有的两种可调用对象是函数函数指针。还有两种可调用对象:重载了函数调用运算符的类,以及lambda表达式
  3. 一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类型,一个lambda具有一个返回类型,一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式:

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

其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type,paramter list,function body则与普通函数一样。但是,lambda必须使用尾置返回来指定返回类型。

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体

auto f=[]{return 42};

我们定义了一个可调用对象f,它不接受参数,返回42。
如果忽略返回类型,lambda根据函数体代码推断出返回类型

我们可以通过lambda来改写上文提到的isShorter函数:

sort(words.begin(),words.end(),
      [](const string &s1,const string &s2)
        {return s1.size()<s2.size();});

23.示例:打印长度大于等于某个值的字符串

void printBig(vector<string> vec,string::size_type sz){
    sort(vec.begin(),vec.end(),[](const string &s1,const string &s2)
    {return s1.size()<s2.size();});
  //find_if接受一个谓词,筛选出第一个返回非0的迭代器
    auto it=find_if(vec.begin(),vec.end(),[sz](const string& s)
    {return s.size()>=sz;});
    while(it!=vec.end()){
        cout<<*it++<<" ";
    }
    cout<<endl;
}

此处我们使用捕获列表捕获了局部变量sz,使得算法不再受制于谓词的参数个数。

  1. for_each算法,接受一个可调用对象,并对输入序列每个元素调用此对象。我们可以利用它改写上述代码过程:
for_each(it,vec.end(),[](const string &s){cout<<s<<endl;});
  1. 当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名)类类型。目前,可以这样理解,当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象。默认情况下,从lambda生成的类都包含一个对应该lamdda所捕获的变量的数据成员,类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
  2. 变量的捕获可以是值或引用。与传值参数类似,采用值捕获的前提是对象可以拷贝。与参数不同,捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。因此随后对被捕获变量的修改不会影响到lambda内对应的值
  3. 引用捕获
void fun2(){
  size_t v1=42;
  auto f2=[&v1]{return v1;};
 }

当采用引用捕获时,同样要保证在lambda执行时变量存在

  1. 我们也可以从一个函数返回lambda
  2. 我们还可以隐式捕获,在捕获列表中写一个&=,告诉编译器采用引用捕获还是值捕获方式。具体规则此处不赘述。
  3. 当一个lambda体包含return以外的任何语句,则编译器假定此lambda返回void,我们需要显式指明
  4. 除了使用lambda以外,我们还可以使用定义在functionalbind函数,这个函数可以实现对可调用对象参数的动态绑定,利用它同样可以实现谓词的参数扩展,此处也不赘述,该函数的设计理念已经隐隐进入函数式编程的范畴。
  5. 除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外集中迭代器:
  • 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素
  • 流迭代器:被绑定到输入或输出流,可用来遍历关联IO流
  • 反向迭代器:向后而不是向前移动
  • 移动迭代器:这些专用的迭代器不是拷贝其中元素,而是移动它们
  1. 插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
    插入器有三种类型,差异在于元素插入的位置
  • back_inserter :总是插入到最后一个元素之后
  • front_inserter :总是插入到第一个元素之前,仅用于容器支持push_front
  • inserter:接受第二个参数,这个参数必须是一个迭代器,元素将被插入到给定迭代器所表示的元素之前,指向不变
  1. 虽然iostream类型不是容器,但标准库定义了可用于这些IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写数据。这些迭代器将它们对应的流当做一个特定类型的元素序列来处理,我们可以用泛型算法从流对象读取数据以及向其写入数据。
  2. 当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。当创建一个istream_iterator,我们将其绑定到一个流,我们还可以默认初始化流迭代器,这样就创建了一个可以作为尾后值使用的迭代器:
istream_iterator<int> int_it(cin);
istream_iterator<int> int_eof;
ifstream in("filename");
istream_iterator<string> str_it(in);

下面是一个用istream_iterator从标准输入读取数据,存入vector的例子:

vector<int> vec;
istream_iterator<int> it(cin);
istream_iterator<int> eof;
while(it!=eof){
    vec.push_back(*it++);
}

或者

istream_iterator<int> it(cin);
istream_iterator<int> eof;
vector<int> vec(it,eof);
  1. 当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。标准库中的实现保证的是在我们第一次解引用之前,读取操作已经完成了。
  2. 我们可以对任何具有输出操作符<<的类型定义ostream_iterator。我们可以提供第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。
vector<int> vec={1,2,3};
ostream_iterator<int> out(cout," ");
for(auto num:vec){
    *out++=num;
}

或者

vector<int> vec={1,2,3};
ostream_iterator<int> out(cout," ");
copy(vec.begin(),vec.end(),out);
  1. 我们可以为任何定义了输入运算符的类型创建istream_iterator和ostream_iterator对象
  2. 任何算法的最基本的特征是它要求其迭代器提供哪些操作,某些算法,比如find,只要求通过迭代器访问元素,递增迭代器以及比较迭代器是否相等这些能力。算法所要求的迭代器操作可以分为5个迭代器类别,每个算法都会对它的每个迭代器参数指名需要提供哪些迭代器:
  • 输入迭代器:只读不写,只能递增
  • 输出迭代器:只写不读,只能递增
  • 前向迭代器:可读写;多遍扫描,只能递增
  • 双向迭代器:可读写;多遍扫描,可递增递减
  • 随机访问迭代器:可读写;多遍扫描,支持全部迭代器运算
  1. 一些算法使用重载形式传递一个谓词,比如sort
  2. 接受一个元素值的算法通常有另一个不同名的版本,该版本接受一个谓词代替元素值,接受谓词的算法都有附加的_if后缀,比如find和find_if
  3. 区分拷贝元素的版本和不拷贝的版本,默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中。写到额外目的空间的算法都在名字后面附加一个_copy
  4. 与其他容器不同,链表类型listforward_list定义了几个成员函数形式的算法,特别是它们定义了独有的sort,merge,remove,reverse和unique。通用版本的sort要求随机访问迭代器,而它们分别提供双向迭代器和前向迭代器。对于list和forward_list,应该优先使用成员函数版本的算法。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容