C++ STL标准库 仿函数和适配器

目录

一、仿函数 functors

二、函数适配器 

1.迭代器适配器

1.1 reverse_iterator

1.2 inserter

2.容器适配器

3.仿函数适配器

3.1 bind2nd

3.2 not1

3.3 新型适配器 bind

4.未知适配器

4.1 ostream_iterator 输出流迭代器

4.1 istream_iterator 输入流迭代器


一、仿函数 functors

  • 又称函数对象为算法提供一些自定义的函数规则(例如以特定的条件实现累计 accumulate)。
  • 规模较小,比较容易由用户自己实现,并加入标准库中作为自己应用的部分。
  • 仿函数本质是一个类(struct),类中必须实现重载小括号(operator())使用时通过加上()来成为临时对象调用

分类:

  1. 算数类 — plus、minus
  2. 逻辑运算类 — logical_and
  3. 相对关系类 — equal_to、less

标准库所提供的仿函数都有继承关系,用户定义的 functor 如果没有继承特定的类,那么就没有融入 STL 的体系结构里面

 

unary 表示取一个操作数的操作, binary 表示有两个操作数。  

继承的 typedef 这个技巧在标准库中多次出现,由 less 继承 binary_function(本身只有 typedef) 不会带来额外的开销。

仿函数能被修改,被适配(adaptable),被调整的话就要选择适当的 u / b function 来继承,才能真正融合到 STL 中。被 adapter 改造时 adapter 可能会提问这些 typedef 的问题(类比于算法提问迭代器中 typedef 的五个问题)。

二、函数适配器 

adapter 要问问题,functor 要回答问题。

adapter 就是把既有的东西先记起来 — 通过 typedef 来实现,然后考虑如何做一些改造:比如改造函数接口,三个参数的变成两个参数的,或者改变函数名称。 也就是说 adapter 修饰了什么东西,记起来了什么东西,它就是 什么适配器。

  • 迭代器适配器
  • 容器适配器
  • 仿函数适配器
  • 未知适配器

adapter 一般通过内含的方式去实现。

1.迭代器适配器

1.1 reverse_iterator

 

由默认迭代器所做的默认排序是由小到大的排序,如果要实现由大到小的排序,可以采取:

  1. 设计专门的由大到小的排序算法
  2. 使用默认的排序算法,使用反向迭代器

取 current 的值转换为 *--tmp,即对逆向迭代器取值就是对正向迭代器退一位去值。逆向迭代器的++ -- 就是 正向迭代器的 -- ++。

1.2 inserter

  • list::iterator 不是一个随机访问迭代器,所以不能直接 +3 ,实现迭代器前进三个用的是 advance(it,3)
  • copy实现之前要保证目的位置有足够的空间 储存要放入的值,而使用迭代器 inserter 将元素插入到目的地,不需要提前考虑分配空间的大小,实现的就是主观上的硬插入数据。
  • 实现这种主观的硬插入用的底层原理就是 操作符重载,inserter_iterator 中重载了 = 运算符,在 copy 中的 *result = *first 运行时调用这个重载。重载的 operation= 中,使用了 insert 算法。

2.容器适配器

stack 和 queue 作为容器是适配器都是通过内含一个 Sequence 来实现的(Sequence默认为 deque)。

实现的是内含 Sequence 后就可以通过自己的命名方式使用其方法(如 push_back 改成 push),即实现了改造。

3.仿函数适配器

3.1 bind2nd

  • count_if(vi.begin(),vi.end(),bind2nd(less<int>,40)); 要实现的功能是计数在 vi.begin() 到 vi.end() 之间,有多少元素符合小于40的条件。
  • less<int>() 函数对象原本要实现的功能是比较 x 是否小于 y,但是现在要实现的功能是比较 x 是否小于40。
  • bind2nd() 实现的功能是 绑定第二参数。bind2nd() 其实是一个辅助函数,binder2nd() 才是真正的函数主体,这种辅助函数的机制也普遍存在于 STL 中。 

bind2nd() 中借用函数模板可以做实参推导的概念,编译器会自动推导出类型,用模板参数 operation 来代替 less<int> 作为类型名。在 bind2nd() 中 会调用到 binder2nd() 的构造函数,操作 x 记录于 op 中,实参 y 记录于 value 中,operation 也会作为模板参数被传入。所以整个 bind2nd() 部分的返回值为 op(x,value) ,返回类型为 bool 并由这个返回值作为 count_if() 的第三参数 pred。在 count_if() 的 for 循环中,每次判断 pred() 返回的 bool 值,决定是否做计数累加,实现得到小于40 的元素有几个的功能。

  1. 其中 bind2nd 作为适配器要问的问题是 Operation::second_argument_type,回答的是 functor less<int>,结果 typedef 为 arg2_type() 并用强制类型转换将第二参数转为所需要的类型,避免了 runtime error。
  2. 加上 typename 是在编译 时期并不知道 Operation 具体指什么意义时,就让编译器识别 Operation::second_argument_type 为一个类型名,保证了编译器能正常通过。
  3. 一个 adapter 在修改了别的 functor 后,如果希望自己还能再次被修改调整,则它也要继承 unary、binary_function。

C++ 新标准中  <funtional>,bind 的提出已经取代了  <funtional>, binder1st, binder2nd, bind1st, bind2nd 的功能,不过都能正常使用。

3.2 not1

 

新标准中 not1 没有被替换。

3.3 新型适配器 bind


3.3.1 绑定 function

double my_divide(double x, double y)
{ return x / y ; }

using namespace std::placeholders;          //placeholders 占位符 例如下例中的 _1,_2....  

auto fn_five = bind(my_divide, 10, 2);      //绑定10,2为my_divide的第一二参数
cout<< fn_five() << '\n';                   //执行结果就是10/2,答案为5

//return x/2
auto fn_half = bind(my_divide, _1, 2);    //绑定2为my_divide的第二参数, 第一参数由_1占位符预留
cout<< fn_half(10) << '\n';               //调用时才设置第一参数为10,结果为5

//return y/x
auto fn_invert = bind(my_divide, _2, _1);     //绑定参数的顺序为2,1
cout<< fn_invert(10,2) << '\n';               //设置divide第一参数为2,第二为10,结果为0.2

//return int(x/y)
auto fn_rounding = bind<int>(my_divide, _1, _2);     //bind若有类型参数,表示绑定返回类型
cout<< fn_rounding(10,3) << '\n';                    //int(10/3),结果为3

bind() 若没有类型参数时,函数 my_divide() 的返回类型就默认是 bind() 的返回类型。

3.3.2 绑定 function object

//只需将上述 my_divide() 函数改造成以下函数对象即可
template <class T>
struct divides : public binary_function <T, T, bool> {
T operator(const T& x, const T& y) 
    { return x / y ;}
} my_divide;

3.3.3 绑定 member f

unctions,和 data members  _1必须是某个对象的地址

struct MyPair {
    double a, b;
    double multiply() { return a * b; }
                                              //member function 其实有个隐藏参数 this
}

using namespace std::placeholders;
MyPair ten_two {10, 2};                      //初始化一个结构体 ten_two

auto bound_memfn = bind(&MyPair::multiply, _1);    //return x.multiply(), _1指this_pointer调用时需要把this_pointer传入进去
cout << bound_memfn(ten_two) << '\n';              //20

auto bound_memdata = bind(&MyPair::a, ten_two);    //return ten_two.a
cout << bound_memdata() << '\n';                   //10

auto bound_memdata2 = bind(&MyPair::b, _1);         //return x.b
cout << bound_memdata2() << '\n';                   //2

绑定 data members 时,第一参数为 data member 的类地址,第二参数为要绑定的类对象名。

  • 测试 用 bind() 去取代 bind2nd() 。 

其中 .cbegin() 表示 const 迭代器。显然 bind 不用提前定义到底是绑定第几参数( 1st 或者 2nd ),而是由占位符 _1,_2,.......来实现的。

4.未知适配器

4.1 ostream_iterator 输出流迭代器

  • 输出流迭代器 可以这样绑定 ostream_iterator<int> out_it (std::cout, " , "); 第二参数可以作为一个分隔符号。
  • 要考虑对 输出流迭代器如何实现 * = ++ return 四种操作。
  • 传入参数为指向 cout 的指针,返回值为 *this,都是为了保证输入的 cout 确实被作用,如果传入的是形参,则作用过后原值不会被修改。

4.1 istream_iterator 输入流迭代器

输入流迭代器  istream_iterator<int> eos;和 istream_iterator<int> iit (std::cin);  没有参数传入的是抽象的输入流迭代器,作为输入结束标志使用。 

iit (std::cin) 在初始化运行构造函数时,就已经开始读取数据了。

实现了对传入的 iit 的 operation * ++ 重载

同一个 copy() 搭配不同的 adapter 时,就有不同的行为表现。

猜你喜欢

转载自blog.csdn.net/SimonxxSun/article/details/85640043