从C到C++___STL入门(四)函数符和lamda表达式

STL入门(四)函数对象和lambda表达式

1. 函数对象

函数对象也叫做函数符(functor)。实际上,函数对象就是重载了operator()的类对象,这样子就可以对对象a,使用a(...),这个形式很像函数调用,所以我们称之为函数对象。总之,函数符包括函数名,函数指针,重载了()运算符的类对象

但是,在后面我们会谈lambda表达式,有些情况下,它是函数符的更好的替代品。

1.1 函数符概念

正如STL定义了容器和迭代器的概念一样,它也定义了函数符概念。

  • 生成器(generator)是不用参数就可以调用的函数符。
  • 一元函数(unary function)是用一个参数可以调用的函数符
  • 二元函数(binary function)是用两个参数可以调用的函数符

例如,for_each()第三个参数要求提供一个一元函数。

还有两个重要的概念;

  • 谓词(predicate)是指返回bool值的一元函数
  • 二元谓词(binary predicate)是指返回bool值的二元函数

例如sort()的第三个参数要求是个谓词;list的接口remove_if()的参数也是个谓词,如果函数返回true则删除。

bool WorseThan(const Review & r1,const Review & r2);
...
sort(books.begin(),books.end(),WorseThan);

bool tooBig(int n){return n>100;}
list scores;
...
scores.remove_if(tooBig);

假设要删除另一个链表种所有大于200的值,那么我们就得重新写tooBig函数,这样未免太麻烦,我们可以使用类技术

template
class TooBig
{
private:
    T cutoff;
public:
    TooBig(const T & t):cutoff(t){}
    bool operator()(const T &v){return v>cutoff;}
};
...
TooBig f200(200);
scores.remove_if(f200);

我们通过类对象的方式远比函数指针灵活。

//函数对象1.cpp
#include
#include
#include

template
class TooBig
{
private:
    T cutoff;
public:
    TooBig(const T & t):cutoff(t){}
    bool operator()(const T &v){return v>cutoff;}
};
int main()
{
    using std::list;
    using std::cout;
    using std::endl;

    TooBig f100(100);
    list yadayada={50,100,90,180,60,210,415,88,188,201};
    list etcetera(yadayada);
    cout<<"Original lists:\n";
    for(auto x:yadayada) cout<(200));
    cout<<"Trimmed lists:\n";
    for(auto x:yadayada) cout<
Original lists:
50 100 90 180 60 210 415 88 188 201
50 100 90 180 60 210 415 88 188 201
Trimmed lists:
50 100 90 60 88
50 100 90 180 60 88 188

1.2 预定义的函数符

首先来看一下transform()函数。它有两个版本。
第一个版本接受4个参数,前两个参数是指定容器区间的迭代器,第三个参数是指定结果复制到哪里的迭代器,最后一个参数是一个一元函数,它被应用于区间内所有元素,生成结果种的新元素。

vector gr8={36,39,42,45,48};
ostream_iterator out(cout," ");
transform(gr8.begin(),gr8.end(),out,sqrt);

上述代码计算每个元素的平方根,并肩结果发送到输出流。可以将上例种out替换成gr8.begin(),这样新值将覆盖原来的值。

第二个版本接受5个参数,前两个参数是指定第一个容器区间的迭代器,第三个参数是指定第二个容器区间的起始位置的迭代器,第四的参数是指定结果复制到哪里的迭代器,最后一个参数是一个二元函数,两个区间种的元素分别作为第一参数和第二参数传递给这个二元函数,然后生成的结果存储在结果容器中。

double mean(double x,double y){return (x+y)/2.0;}
...
transform(gr8.begin(),gr8.end(),m8.begin(),out,mean);

上述代码会把gr8中的元素和m8中对应的元素取平均值后,将结果发送到标准输出流。

现在,假设我们要将两个数组相加。我们显然不能使用+作为参数,因为+是内置运算符,而不是函数。但是我们可以这样做:

double add(double x,double y){return x+y;}
...
transform(gr8.begin(),gr8.end(),m8.begin(),out,add);

在头文件中定义了多个模板函数对象,其中包括plus()
我们可以使用函数符模板来帮助我们完成:

transform(gr8.begin(),gr8.end(),m8.begin(),out,plus());

这里plus()相当于创建了一个匿名的plus的对象,而且使用的是默认构造函数

对于所有内置的算术运算符、关系运算符和逻辑运算符,STL都提供 了等价的函数符。

运算符 相应的函数符
+ plus
- minus
* multiplies
/ divides
% modulus
- negate
== equal_to
!= not_equal_to
> greater
< less
>= greater_equal
<= less_equal
&& logical_and
|| logical_or
! logical_not

1.2 自适应函数符和函数适配器

上面那个表格中的每个函数符都是自适应函数符,实际上就是说它是一个函数符模板,它的模板参数接受一个类型参数,当模板实例化后就能生成我们能使用的函数符了。
自然的,我们就有5个相关概念:自适应生成器、自适应一元函数、自适应二元函数、自适应谓词、自适应二元谓词
而且每个自适应函数符中都携带了标识参数类型和返回类型的typedef成员。这些成员分别是result_typefirst_argument_typesecond_argument_type,它们的作用不言而喻。例如plus对象的返回类型是plus::result_type,这是inttypedef.

  • 函数适配器

二元函数经过函数适配器的操作可以变成一元函数。
例如,我们想要将gr8的每个元素乘以2.5,我们当然可以自己重新写一个函数符,但是我们也可以这样:
自然的,我们想要使用自适应函数符multiplies(),但是我们知道它是一个二元函数,如何让这个二元函数转换为一元函数?

  • binder1st适配器

不言而喻,该适配器会摧毁第一个参数,假设有一个二元函数f2,则我们可以创建一个binder1st对象

看一下,该适配器的源码:

template  class binder1st
  : public unary_function 
{
protected:
  Operation op;
  typename Operation::first_argument_type value;
public:
  binder1st ( const Operation& x,
              const typename Operation::first_argument_type& y) : op (x), value(y) {}
  typename Operation::result_type
    operator() (const typename Operation::second_argument_type& x) const
    { return op(value,x); }
};

下面我们创建一个乘以2.5的一员函数:

binder1st> f1(multiplies(),2.5);
transform(gr8.begin(),gr8.end(),gr8.begin(),f1);

我们也可以使用它的简化版本bind1st适配器,这个适配器不是一个类模板,它是一个函数模板,所以它会自动推断类型。

transform(gr8.begin(),gr8.end(),gr8.begin(),bind1st(multiplies(),2.5));

binder2ndbind2nd与此类,它做的只是摧毁函数的第二个参数。

//函数对象2.cpp
#include
#include
#include
#include
#include
int main()
{
    using namespace std;
    vector gr8={28,29,30,35,38,59};
    vector m8={63,65,69,75,80,99};
    cout.setf(ios_base::fixed);
    cout.precision(1);
    cout<<"gr8:\t";
    for(auto x:gr8)
    {
        cout.width(6);
        cout< sum(6);
    transform(gr8.begin(),gr8.end(),m8.begin(),sum.begin(),plus());
    cout<<"sum:\t";
    for(auto x:sum)
    {
        cout.width(6);
        cout< prod(6);
    transform(gr8.begin(),gr8.end(),prod.begin(),bind1st(multiplies(),2.5));
    cout<<"prod:\t";
    for(auto x:prod)
    {
        cout.width(6);
        cout<
gr8:      28.0   29.0   30.0   35.0   38.0   59.0 
m8:       63.0   65.0   69.0   75.0   80.0   99.0 
sum:      91.0   94.0   99.0  110.0  118.0  158.0 
prod:     70.0   72.5   75.0   87.5   95.0  147.5 

2. lambda表达式

函数对象其实什么都好,就是它的代码狠多,而且不在调用处附近,lambda表示式可以完美替代函数对象,它和函数对象一样支持内联,它的代码狠简洁,而且在调用处附近,方便修改,除此之外最重要的是,lambda表达式非常灵活,它就像一种数学表达式

[](int x){return x%3==0;}

上面这个就是lambda表达式,可以看出来它没有函数名,也没有返回类型(也可以加上函数名和返回类型),所以说它方便生成匿名函数。它的作用就是判断参数x,是否能被3整除。

当然了,如果这个lambda表达式需要复用,可以给他加上名字

auto mod3=[](int x){return x%3==0;}
//调用的时候可以这样
cout<

也可以显式指出返回类型,如果不显式指出返回类型,那么它将使用decltype函数自动推断返回类型

[](int x)->bool{return x%3==0;}

lambda表达式的优点远不止上面所说的,它还可以直接访问scope中的变量

我们可以在[]中间加上一些符号,说明我可以直接访问scope中的某些变量,例如[=]表示可以直接按值使用作用域中的所有变量,[&]表示可以直接引用作用域中的所有变量,[=,&n]表示变量名为n的变量是按引用使用的,除此之外都是按值使用,[&,y]表示变量名为y的变量是按值使用的,除此之外都是按引用使用的。

下面举一个例子来看一下:我们统计数组中可以被3整除的数字的个数和可以被13整除的数字个数

#include
#include
#include
using namespace std;
int 
main(){
    vector a(1000);
    generate(a.begin(),a.end(),rand);
    int _3count,_13count=0;
    _3count=count_if(a.begin(),a.end(),[](int x){return x%3==0;});
    auto foo=[&](int x){if(x%13==0)_13count++;};
    for(auto &x:a)
        foo(x);
    cout<<"count of numbers divisible by 3:"<<_3count<

可以看出来上面的lambda表达式相当灵活。

3小结

传统的C函数指针的缺陷在于:无法进行内联,且无法一劳永逸;函数对象的出现是为了一劳永逸,因为在很多STL算法中要求传入一个函数符或者函数指针,但是函数对象的缺陷在于代码太长;而lambda表达式的出现,完美解决了这些问题,它的显著优点在于可以直接访问作用域中的变量而不需要显式传入参数,好像是嵌入的表达式。不过典型的lambda表达式是用于比较或者测试的。

你可能感兴趣的:(C++入门,c++,c语言,数据结构)