定制操作

向算法传递函数

谓词

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法使用的谓词分为两类:

  • 一元谓词,意味着它们只接受单一参数。
  • 二元谓词,意味着它们有两个参数。

接受谓词参数的算法对输入序列中的元素调用谓词,因此,元素类型必须能够转换为谓词的参数类型。

接受一个二元谓词参数的sort:

bool isShorter(const string &s1, const string &s2)
{
    return s1.size() < s2.size();
}

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

排序算法

将 words 按照大小重排后,希望具有相同长度的元素按照字典顺序排列,可以使用stable_sort 算法。

stable_sort(words.begin(), words.end(), isShorter);

lambda 表达式

可以向算法传递任何可调用对象,对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用对象或表达式。

  • 一个 lambda 表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数。
  • 一个 lambda 具有一个返回类型,一个参数列表和一个函数体。
  • 与函数不同,lambda 可以定义在函数内部。

lambda 表达式的具体形式如下:

[capture list](parameter list)->return type{function body}
  • capture list (补获列表)是一个 lambda 所在函数中定义的局部变量的列表。
  • return type 表示返回类型。
  • parameter list 表示参数列表。
  • function body 表示函数体。

与普通函数不同的是,lambda 必须使用尾置返回来指定返回的类型。

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

//定义一个lambda表达式,返回常量42
auto f = [] { return 42; };
//调用时使用调用运算符
cout << f() << endl;

如果忽略返回类型,lambda 会根据函数体中的代码推断出返回类型,但是如果lambda的函数体中包含任何单一return 语句之外的内容,且未指定返回类型,则返回 void

向lambda传递参数

调用一个 lambda 时会使用给定的实参来初始化形参,通常,实参类型与形参类型必须匹配。

与普通函数不同,lambda 表达式不能有默认参数,一个 lambda 调用的实参数目永远与形参数目相等。

stable_sort(words.begin(),words.end(), 
    [](const string &a, const string &b)
        {return a.size() < b.size(); });

使用补获列表

lambda 可以出现在一个函数中,通过将局部变量包含在补获列表中来指明将会使用这些变量。

auto wc = find_if(words.begin(),words.end(),
    [sz](const string &a) {return a.size() >= sz; });

find_if 调用返回第一个长度不小于给定参数 sz 的元素。如果这个参数不存在,则返回 words.end() 的一个拷贝。

打印 words 中长度大于等于 sz 的元素:

for_each(wc,words.end(),
    [](const string &s){cout<<s<<" ";});

注意:

补获列表只用于局部非静态变量,lambda 可以直接使用局部静态变量和它所在函数之外声明的名字。

lambda 补获和返回

值捕获

变量的捕获方式也可以是指或引用。

与传值参数类似,采用值捕获的前提是变量可以拷贝,与参数不同,被捕获的值是在 lambda 创建时拷贝,而不是调用时拷贝。

void func1()
{
    size_t v1 = 42;
    auto f = [v1] {return v1; };
    v1 = 0;
    auto j = f(); //j为42,f保存了创建它时的v1的拷贝
}

引用捕获

void func1()
{
    size_t v1 = 42;
    auto f2 = [&v1] {return v1; };
    v1 = 0;
    auto j = f2(); //j为0,f2保存了v1的引用而不是v1的拷贝
}

引用捕获与返回引用有着相同的问题和限制,如果采用引用捕获一个变量,就必须确保被引用的对象在 lambda执行的时候是存在的。lambda 捕获的都是局部变量,这些变量在函数结束后就不付存在了,如果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

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

注意:

  • 捕获一个普通变量,通常情况下采用简单的值捕获,在此情况下,只需关注变量在捕获时是否有我们所需的值。
  • 如果捕获一个指针或迭代器或引用,就必须确保在 lambda 执行时,绑定到迭代器、指针、引用的对象仍然存在,另外需要注意lambda创建到执行时这些值可能被修改了。
  • 一般来说,尽量减少捕获的数据量,也尽量避免捕获指针或引用。

隐式捕获

可以让编译器根据 lambda 函数体中的代码来推断使用哪些变量:

  • = 指示编译器按值捕获。
  • & 指示编译器按引用捕获。
//按值捕获,隐式捕获
wc = find_if(words.begin(), words.end(), [=]
    (const string &s) {return s.size() >= sz; })

如果希望一部分变量按值捕获,其它变量按引用捕获,可以混合使用隐式捕获和显示捕获:

//os隐式捕获,引用捕获方式,c显示捕获,值捕获方式
for_each(words.begin(), words.end(), [&, c](const string &s) {os << s << c; });
//os显式捕获,引用捕获方式,c隐捕获,值捕获方式
for_each(words.begin(), words.end(), [=, &osc](const string &s) {os << s << c; });

注意:

  • 当使用混合捕获模式时,捕获列表中的第一个元素必须是 & 或者 = 来指定默认的捕获方式。
  • 当使用混合捕获模式时,显示捕获的变量必须与隐式捕获的变量采用不同的方式,如果隐式捕获采用引用方式,则显示捕获必须采用值方式,如果,如果隐式捕获采用值方式,则显示捕获必须采用引用方式。

可变 lambda

对于一个值拷贝的变量,默认情况下 lambda 不会改变它的值,如果希望能够改变一个被捕获的变量的值,就必须在参数列表首加上关键字 mutable

void func1()
{
    size_t v1 = 42;
    auto f = [v1] () mutable {return v1++; };
    v1 = 0;
    auto j = f();
}

一个引用捕获的变量能否修改它的值则依赖于此引用指向的是一个 const 类型还是一个非 const 类型。

void func1()
{
    size_t v1 = 42;
    auto f = [&v1] () {return v1++; };
    v1 = 0;
    auto j = f();
}

指定 lambda返回类型

默认情况下,如果一个 lambda 函数体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void。与其他返回 void 的函数类似,被推断返回 voidlambda 不能返回值。

返回一个序列中所有数据的绝对值:

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

上面的表达式是正确,但是如果改写成:

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

将不能推断返回类型,编译器将返回 void 类型。

当需要为 lambda 指定返回类型的时候,必须使用尾置返回类型:

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

参数绑定

  • 对于只在一两个地方使用的简单操作,lambda 表达式最有用,但是如果需要在很多地方使用,通常应该定义成函数,而不是多次编写 lambda 表达式。
  • 如果一个操作需要很多语句才能完成,通常使用函数更好。
  • 如果 lambda 的捕获列表为空,通常可以用函数来代替它;但是对于捕获局部变量的 lambda ,用函数来替换就不那么容易。

例如:

auto wc = find_if(words.begin(),words.end(),
    [sz](const string &a) {return a.size() >= sz; });

可以很容易写出一个函数:

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

但是不能将 check_size直接作为 find_if 的一个参数,因为 find_if 接受一个一元谓词,因此传递给 find_if 的可调用对象必须接受单一参数。为了使用 check_size 来代替此 lambda 必须解决如何向 sz 形参传递一个参数的问题。

标准库 bind 函数

bind 函数定义在头文件 functional 中,可以将 bind 函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。

bind 函数的一般形式为:

auto newCallable = bind(callable,arg_list);

当调用 newCallable 时,newCallable 会调用 callable,并传递给它 arg_list 参数。

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

绑定 check_size 的 sz 参数

auto check6 = bind(check_size,_1, 6);

bind 调用只有一个占位符,表示 check6 只接受单一参数。

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

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

如此,可以替换原来的 lambda 表达式:

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

bind 调用生成一个可调用对象,将 check_size 的第二个参数绑定到 sz 的值。

使用 placeholders 名字

名字 _n 都定义在一个名为 placeholdaers 的命名空间中,而这个命名空间本身定义在 std 命名空间。为了使用这个名字,两个命名空间都需要写:

using std::placeholders::_1;

除了对每一个使用的名字单独 using 声明外,也可以统一声明:

using namespace std::placeholders;

placeholdaers 命名空间定义在 functional 头文件中。

如果第一个调用类似:isShorter(A,B) ,则第二个调用为:isShorter(B,A)

绑定引用参数

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

for_each(words.begin(),words.edn(),[&os,c](const string) &s {os<<s<<c;});

可以编写函数替换lambda表达式:

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

但是不能直接使用 bind 来代替 os 的捕获:

//错误,ostream 不可以拷贝
for_each(words.begin(),words.edn(),bind(print,os,_1,' '));

如果想传递给bind一个对象而又不拷贝它,则必须使用标准库 ref 函数:

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

ref 函数返回一个对象,包含给定的引用,此对象是可以拷贝的,标准库还有一个 cref 函数,生成一个保存 const 引用的类。

refcref 也都定义在头文件 functional中。

猜你喜欢

转载自www.cnblogs.com/xiaojianliu/p/12497061.html