C++泛型算法定制操作之突破参数限制的方法

C++提供了很多泛型算法,可以对各个容器使用,如sort对迭代器范围内的容器元素排序、unique把不重复的元素排列到容器前列去、copy复制范围内的容器元素、find寻找符合条件的容器元素等等。

在最基本的使用方法下,会调用默认的相关操作,比如sort会对容器内使用默认的排序方法,比如如果容器内是int型的话,就会比较大小,是string型的话,就会比较字符串内容字符的顺序等等。

但有时候我们希望自己来决定如何比较大小,或者更直观的,对于find_if算法,我们当然会想要自己决定寻找的条件是什么。

C++允许我们自己决定算法的操作方式,这就叫做定制操作。但是定制操作有一个限制。

通常我们提供给算法的自己定制的操作叫做“谓词”,该操作一般返回一个能作为条件的值,供算法使用。这个谓词最直观的表现形式就是你写的函数了。但是谓词对于其参数数量是有限制的,这取决于具体使用它的算法,但允许的参数数量只能使一个或者两个,相应的谓词也就叫“一元谓词”或“二元谓词”。

为什么一般只允许传递一到两个参数呢?这是因为算法就是对容器内元素做操作的,我们只用提供容器内要操作的范围,以及操作函数,至于如何调用,算法会自动帮我们完成,这就要求操作函数必须是正好按照算法的含义接受容器内的元素作为操作对象,比如sort算法,必定是比较容器内某两个元素,所以操作函数一定是个二元谓词,不能多不能少,而find_if算法,用来判断一个元素是否满足操作函数设定的条件,那操作函数一定是个一元谓词,一次只处理判断一个元素,因此这都必须限制好,否则无法正常工作。

明白了谓词的参数量限制后,举一个简单的例子,假设我们要将sort算法按照string的长度来排序,那么可以自己编写一个函数来改变sort算法的默认方式:

bool longer(std::string s1, std::string s2) {
    return s1.size() >= s2.size();
}

vector<string> vec = {……};
sort(vec.begin(), vec.end(), longer);

如代码所示,sort算法只需要确定排序的范围,然后提供一个二元谓词即可,这里的二元谓词是longer函数。

再看一个一元谓词的例子:

bool longThan(std::string s) {
    return s.size() >= 6;
}

vector<string> vec = {……};
find_if(vec.begin(), vec.end(), longThan);

该代码的目的是寻找容器内第一个字符串长度大于6的元素,由于find_if算法会对元素一个个判断,所以只能接受一元谓词,因此,这里的长度条件6是写死在函数中的。

但是,如果我们不想写死呢?毕竟这种操作很不“优雅”,应用起来很不灵活,但一元谓词的限制又摆在那里,只能传递一个参数,怎么办呢?

此时可以使用lambda表达式。lambda表达式可以看做一种特殊的函数,不像一般的函数一样需要单独写函数体,lambda表达式可以直接在函数体内声明,其内包括捕获列表、参数列表、返回类型、函数体,形式如下:

[捕获列表] (参数列表) -> 返回类型 { 函数体; }

如果是初识lambda表达式,除了对形式不太习惯外,可能最不好理解的就是捕获列表是个什么东西。其实捕获列表就是另一种形式的参数,总觉得这是在犯规,既然参数列表有限制,那就用捕获列表来获取想要的东西吧,这种做法不就是变着形式绕过限制么。。

总的来说,捕获列表内可以填写lambda表达式所在的上下文中的变量值,然后就可以在其函数体中使用了,和参数的区别不大,如下:

int sz = 6;
vector<string> vec = {……};
find_if(vec.begin(), vec.end(), [sz](const string &s)->bool { return s.size() >= sz; });

可以看到,代码中把原本放一元谓词的地方改成了lambda表达式,同时成功地传递了两个“参数”,一个是字符串,另一个就是自己定义的一个长度变量,然后通过捕获列表传递到函数体中使用。

另外,这种函数体只有一个return语句的,可以省略返回类型的说明(“->bool”部分),编译器可以自己推断出来,但如果有多条语句,那就必须写清楚返回类型了,否则编译器会设为void。如果没有捕获变量和参数,中括号和小括号内可以不写内容,但括号本身都不能省略。

要说捕获变量和参数有什么不同,就是对变量的操作方面了。首先,捕获变量会在声明lambda表达式(不是使用时,就是声明的时候)时复制捕获变量的值进去,此后你可以改变外在的捕获变量本身的值,都不影响lambda表达式函数体内的变量值,如果要传递的东西无法复制(比如cout),也可以使用引用的方式,但是要确保lambda表达式调用过程中该变量始终存在。

关于lambda表达式还有一些内容,比如隐式捕获、可变lambda等,不细讲了,本文主要是说明借用lambda表达式来突破算法中对谓词参数量的限制。

使用lambda虽然可以突破限制,不过对于需要频繁调用的操作,如果每次都要写一遍lambda表达式,既写起来麻烦,需要修改的时候也难保能全部改到,这时候函数的优势就体现出来了,一次编写,随时调用,且有修改需要的时候只需要改一个地方即可,而lambda可能更适合少量使用且操作简单的情况了。那有办法使用函数并且突破参数限制吗?有的,可以使用“参数绑定”,也就是bind函数。

说起来C++的开发者真的是有点缝缝补补的感觉,为了一些限制不得不创造出一些解决方法给大家使用。

bind函数其实原理就是在原本我们的操作函数之上再覆盖一层,包装成一个新的函数,然后在该包装过程中,可以把一些需要的额外的参数防止进去,同时留出空位给算法使用中要填充的容器元素,这样就可以减少参数数量了。说起来绕,直接看代码:

bool longer(std::string s, std::vector<std::string>::size_type sz) {
    return s.size() >= sz;
}

int sz = 6;
vector<string> vec = {……};
auto count = count_if(vec.begin(), vec.end(), bind(longer, std::placeholders::_1, sz));

这里用到了count_if算法,该算法是计算容器指定范围内有多少符合特定条件的元素,我们的目的是计算有多少长度大于sz(6)的字符串。count_if一次只判断一个元素,因此只能给其配置一元谓词,但是我们的longer函数有两个参数,怎么办呢?

使用bind函数,将其包装成一个新函数,bind的第一个参数为要包装的函数名,后续可以接很多个参数,其中可以有很多上下文包含的变量,这些参数类似lambda表达式中的捕获变量,不会占谓词的参数数量,同时留出空位(placeholders)给容器元素,这些空位数量才是真正的占参数数量的。把代码写得更清楚的话如下:

auto bindLogner = bind(longer, std::placeholders::_1, sz);
auto count = count_if(vec.begin(), vec.end(), bindLogner);

这样就可以清楚的看到我们包装了一个新的函数bindLonger,该函数只接受一个参数,也就是预留的std::placeholders::_1,符合一元谓词的要求。

这里的_1(前缀表示所在的命名空间)其实有讲究,除了_1,当然还有_2、_3、_4等等,这里的1/2/3/4等表示使用bind时的第几个参数会映射到创建bind时对应的原始函数的参数位置,在上面的例子中会映射到第一个参数,来看个更复杂中的例子:

auto someCallable= bind(callable, a, _2, b, _1, c);

someCallable(_1, _2);
// 等价于
callable(a, _2, b, _1, c);

// 也就是说
someCallable(X, Y);
// 等价于
callable(a, Y, b, X, c);

使用bind后的新函数是someCallable,调用时需要两个参数,分别处于第一个和第二个的位置(X和Y)。实际上会映射到callable函数,其中X和Y分别对应定义bind时,其所约定的位置上,看代码应该可以理解的比较清楚。

需要注意的是bind如果想要使用参数的引用,而不是复制的话,不能简单的用&,而应该使用ref:

auto someCallable= bind(callable, ref(a), _2, b, _1, c);

这里的第一个参数a就是使用的引用。

以上就是两种突破泛型算法定制操作的方法,不知道为什么,总有点投机取巧的感觉,其实实质上是一样的,只是换了一种形式来传递“参数”,让函数体可以使用其值。


查看作者首页

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,505评论 1 51
  • 顺序容器中只定义了添加删除访问等简单操作,用户更多的需求,只能通过泛型算法实现。此类算法称之为"泛型"是因为它们可...
    saviochen阅读 1,078评论 0 4
  • 10.1 概述 #include //大部分算法定义 #include <numeric> //数值泛型算法 ...
    龙遁流阅读 571评论 0 1
  • 10.1 概述 范型算法:实现了一些经典算法的公共接口,可用于不同类型的元素、多种类型的容器、其他类型序列。 迭代...
    咸鱼翻身ing阅读 181评论 0 0
  • 身边的朋友一个个的都结婚了,朋友问,你呢?亲戚说,找个差不多的嫁了吧到了30岁就晚了。 前几年的我听到...
    一年一班阅读 888评论 1 0