022 参数绑定

对于那种只在一两个地方使用的简单操作, lambda 表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的 lambda 表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。

如果 lambda 的捕获列表为空,通常可以用函数来代替它。例如我们既可以用一个 lambda,也可以用函数来实现将 vector 中的单词按长度排序。类似的,对于打印 vector 内容的 lambda,编写一个函数来替换它也是很容易的事情,这个函数只需接受一个 string 并在标准输出上打印它即可。

但是,对于捕获局部变量的 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,  args_list);

其中,newCallable 本身是一个可调用对象,args_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。即,当我们调用 newCallable 时,newCallable 会调用 callable,并传递给它 args_list 中的参数。

args_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数。这些参数是“占位符”,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的“位置”。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,依此类推。

绑定 check_size 的 sz 参数

作为一个简单的例子,我们将使用 bind 生成一个调用 check_size 的对象,如下所示,它用一个定值作为其大小参数来调用 check_size:

// check6 是一个可调用对象,接受一个 string 类型的参数
// 并用此 string 和值 6 来调用 check
auto check6 = bind(check_size, _1, 6);

此 bind 调用只有一个占位符,表示 check6 只接受单一参数。占位符出现在 args_list 的第一个位置,表示 check6 的此参数对应 check_size 的第一个参数。此参数是一个 const string&。因此,调用 check6 必须传递给它一个 string 类型的参数,check6会将此参数传递给 check_size。

string s = "hello";
bool b1 = check6(s); // check6(s) 会调用 check_size(s, 6)

使用 bind,我们可以将原来基于 lambda 的 find_if 调用:

auto wc = find_if(words.begin(), words.end(),
               [sz](const  string  &a)

替换为如下使用 check_size 的版本:

auto wc = find_if(words.begin(), words.end(),
                bind(check_size, _1, sz));

此 bind 调用生成一个可调用对象,将 check_size 的第二个参数绑定到 sz 的值。当 find_if 对 words 中的 string 调用这个对象时,这些对象会调用 check_size,将给定的 string 和 sz 传递给它。因此,find_if 可以有效地对输入序列中每个 string 调用 check_size,实现 string 的大小与 sz 的比较。

使用 placeholders 名字

名字 _n 都定义在一个名为 placeholders 的命名空间中,而这个命名空间本身定义在 std 命名空间中。为了使用这些名字,两个命名空间都要写上。与我们的其他例子类似,对 bind 的调用代码假定之前己经恰当地使用了 using 声明。例如,_1 对应的 using 声明为:

using std::placeholders::_1;

此声明说明我们要使用的名字 _1 定义在命名空间 placeholders 中,而此命名空间又定义在命名空间 std 中。

对每个占位符名字,我们都必须提供一个单独的 using 声明。编写这样的声明很烦人,也很容易出错。可以使用另外一种不同形式的 using 语句,而不是分别声明每个占位符,如下所示:

using namespace namespace_name;

这种形式说明希望所有来自 namespace_name 的名字都可以在我们的程序中直接使用。例如:

using  namespace  std::placeholders;

使得由 placeholders 定义的所有名字都可用。与 bind 函数一样,placeholders 命名空间也定义在 functional 头文件中。

bind 的参数

如前文所述,我们可以用 bind 修正参数的值。更一般的,可以用 bind 绑定给定可调用对象中的参数或重新安排其顺序。例如,假定 f 是一个可调用对象,它有 5 个参数则下面对bind的调用:

// g 是一个有两个参数的可调用对象
auto g = bind(f,  a,  b, _2, c, _1);

生成一个新的可调用对象,它有两个参数,分别用占位符 _2 和 _1 表示。这个新的可调用对象将它自己的参数作为第三个和第五个参数传递给 f。f 的第一个、第二个和第四个参数分别被绑定到给定的值 a、b 和 c 上。

传递给 g 的参数按位置绑定到占位符。即,第一个参数绑定到 _1,第二个参数绑定到 _2。因此,当我们调用 g 时,其第一个参数将被传递给f作为最后一个参数,第二个参数将被传递给f作为第三个参数。实际上,这个 bind 调用会将

g(_1, _2)

映射为

f(a,  b, _2, c, _1);

即,对 g 的调用会调用 f,用 g 的参数代替占位符,再加上绑定的参数 a、b 和 c。例如调用 g(X, Y) 会调用 f(a, b, Y, c, X)

用 bind 重排参数顺序

下面是用 bind 重排参数顺序的一个具体例子,我们可以用 bind 颠倒 isShroter 的含义:

// 按单词长度由短至长排序
sort(words.begin(), words.end(), isShorter);
// 按单词长度由长至短排序
sort(words.begin(), words.end(),  bind(isShorter, _2, _1));

在第一个调用中,当 sort 需要比较两个元素 A 和 B 时,它会调用 isShorter(A, B)。在第二个对 sort 的调用中,传递给 isShorter 的参数被交换过来了。因此,当 sort 比较两个元素时,就好像调用 isShorter(B, A) 一样。

绑定引用参数

默认情况下,bind 的那些不是占位符的参数被拷贝到 bind 返回的可调用对象中。但是,与 lambda 类似,有时对有些绑定的参数我们希望以引用方式传递,或是要绑定参数的类型无法拷贝。

例如,为了替换一个引用方式捕获 ostream 的 lambda:

// os 是一个局部变量,引用一个输出流
// c 是一个局部变量,类型为 char
for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; });

可以很容易地编写一个函数,完成相同的工作:

ostream  &print(ostream &os, const string &s, char c) {
    return os << s << c;
}

但是,不能直接用 bind 来代替对 os 的捕获

// 错误:不能拷贝os
for_each(words.begin(), words.end(), bind(print, os, _1, ' '));

原因在于 bind 拷贝其参数,而我们不能拷贝一个 ostream。如果我们希望传递给 bind 一个对象而又不拷贝它,就必须使用标准库\color{RED}{ref} 函数:

for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));

函数 ref 返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个 \color{RED}{cref} 函数,生成一个保存 const 引用的类。与 bind 一样,函数 ref 和 cref 也定义在头文件 functional 中。

向后兼容:参数绑定

旧版本 C++ 提供的绑定函数参数的语言特性限制更多,也更复杂。标准库定义了两个分
别名为 bind1st 和 bind2nd 的函数。类似 bind,这两个函数接受一个函数作为参数,
生成一个新的可调用对象,该对象调用给定函数,并将绑定的参数传递给它。但是,这
些函数分别只能绑定第一个或第二个参数。由于这些函数局限太强,在新标准中已被弃
用(deprecated)。所谓被弃用的特性就是在新版本中不再支持的特性。新的 C++ 程序应
该使用 bind。

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

推荐阅读更多精彩内容

  • //Clojure入门教程: Clojure – Functional Programming for the J...
    葡萄喃喃呓语阅读 3,660评论 0 7
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,754评论 2 9
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,563评论 0 5
  • 函数只定义一次,但可能被执行或调用任意次。JS函数是参数化的,函数的定义会包括一个称为形参的标识符列表,这些参数在...
    PySong阅读 526评论 0 0
  • 函数只定义一次,但可能被执行或调用任意次。JS函数是参数化的,函数的定义会包括一个称为形参的标识符列表,这些参数在...
    PySong阅读 853评论 0 0