第38条:遵循按值传递的原则来设计函数子类
- 不能将函数作为参数传递给另一个函数,只能传递函数指针;
void qsort(void* base, size_t nmemb, size_t size, int(*cmpfcn)(const void*, const void*));//qsort的第三参数cmpfcn是按值传递的函数指针
- 函数指针以及STL函数对象作为函数的参数和返回值时,习惯通过pass by value传递;
- pass by value的缺点:
①传递大对象效率低(空间时间都影响);
②在继承体系下,可能产生剥离问题
解决方法1: pass by reference(不行,有的STL配接器和算法在接受函数对象作为参数时,本身就使用了传引用,若再将模板参数指定为传引用形式,则会出现“引用的引用”,会造成编译错误);因此没有必要传引用,依然可以按照习惯传值,只要设法克服效率和剥离问题,使函数子以正确的方式拷贝复制;
//for_each的原型
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);
//函数子类
class DoSomething : public unary_function<int, void>{
public:
void operator()(int x){...}
};
deque<int> di;
...
//通过实例化函数模板参数DoSomething&, 强行将函数子按引用传递并返回;此处的示例for_each是可以的
for_each<deque<int>::iterator, DoSomething&>(di.begin(), di.end(), DoSomething());
//但下面这个STL配接器not1,在接收函数对象参数时const Predicate&,已经采用了引用,则实例化not1模板时不能再指定引用
template<class Predicate>
unary_negate<Predicate> not1(const Predicate& Pred);
not1<DoSomething&>(DoSomething());//则相当于传入not1的参数类型变成const DoSomething&&,“引用的引用”会编译错误;
解决方法2:优化pass by value(采用桥接模式,将该函数子类的数据和虚函数放入一个新的类,而该函数子类中只需要留一个指向新类对象的指针即可,这样本身小巧且单态,但确可访问大量数据且具备多态特性);
注:下面的桥接模式没有写拷贝构造函数,因为对桥接模式没理解,日后补上;
//效率低 + 存在剥离问题的函数子类BPFC
template<typename T>
class BPFC : public unary_function<T, void>{
private:
Widget w;
int x;
double y;
... //数据成员很多,因此按值传递BPFC函数对象效率很低;
public:
virtual void operator()(const T& val); //虚函数,存在剥离问题
...
};
//使用桥接模式优化后的函数子类BPFC
template<typename T>
class BPFCImpl : public unary_function<T, void>{
private://不懂为什么所有数据成员 + 析构函数 + operator() + friend全都放在private中;
Widget w;
int x;
double y;
... //原本BPFC的所有数据成员
virtual void operator()(const T& val)const; //判别式类的判别式函数要保证是纯函数,即operator()要声明为const,但这还不够,具体看第39条
virtual ~BPFCImpl(); //此处不懂;
friend class BPFC<T>;//允许BPFC调用private中的operator();
};
//现在优化过的BPFC class变的小巧(只含一个指针数据成员) 且 单态(不含虚函数成员);
template<typename T>
class BPFC : public unary_function<T, void>{
private:
BPFCImpl<T>* p; //只留指向新类的一个指针
public:
void operator()(const T& val){//变成了非虚函数,作为调用真正operator()的接口;
p->operator()(val);
}
};
第39条:确保判别式是“纯函数”
- 判别式:返回值为bool (或可以隐式转换为bool)的函数;
例如默认情况下,标准关联容器调用<操作符作为比较函数,当然也可以自定义一个比较函数;
判别式常作为STL算法的参数传递(但往往还需要用ptr_fun加以修饰); - 判别式类:函数子类,其operator()的返回值为bool;
判别式类的对象(即一个函数子)可以作为STL算法的参数传递; - 纯函数:指函数的返回值仅仅依赖于参数;
即该函数只能访问参数和常量,才能保证在不同时间调用该函数时,不会产生不同的结果;该函数不能访问任何可变的变量以免修改,包括各种形式的非const对象,const类的mutable数据成员等等; - 之所以要求判别式是“纯函数”,因为判别式或判别式类的对象通过pass by value的方式,在STL的各种函数,算法之间传递。在各式各样的算法和函数实现中下,都能正确的传递函数对象,保证函数对象即使在不同时间被多次调用,都不会改变什么东西以免我们使用“自以为改变后的变量”,而其实"在下一次调用时pass by value得到的那个函数对象并不是我们以为的那个改变后的函数对象";
//remove_if的某种实现,其第三参数是一个函数对象,即(可能通过ptr_fun修饰的判别式)或(判别式类的对象)
template<typename FwdIterator, typename Prediate>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Prediate p){
begin = find_if(begin, end, p);//p是传入remove_if的参数p的副本
if(begin == end) return begin;
return remove_copy_if(++next, end, begin, p);//我们以为这里传入的p是前面find_if修改过的p,其实这个p仍然是传入remove_if的参数p的副本
}
//一个错误的判别式类:其operator()函数不是“纯函数”
class BadPredicate : public unary_function<Widget, bool>{
public:
BadPredicate() : timesCalled(0){}
bool operator()(){
return ++timesCalled == 3;
}
private:
size_t timesCalled;
};
vector<Widget> vw;
...
vw.erase(remove_if(vw.begin(), vw.end(), BadPredicate()), vw.end());//此处调用,并不是我们的预期的结果
- STL中,判别式和判别式类的对象都可以互相用,只是判别式还需要用ptr_fun修饰一下,使其转化为functor;
- best practice:对于判别式 或 **判别式类的operator()**这些函数,都要设计成“纯函数”;除了判别式类的operator()要声明为const之外,保证这些函数不能访问任何可变的变量,只能访问传入的参数和常量对象;
第40条:若一个类是函数子类,则应使它可配接
- 在STL中,作为算法参数或返回类型传递的函数对象,为了使其更具通用性,应使它可配接:需要使其成为具有必要类型定义的仿函数;
- 一个具有可配接性的函数子类:
- 继承自基结构unary_funciton或binary_function,具体继承哪个视该函数子类的operator()的参数个数而定:1个参数继承unary_function,两个参数继承binary_function
- unary_funciton或binary_function使STL的模板,需要指定模板参数:operator()的参数或返回类型不含指针时,则省略常量关键字const和引用&;若含指针,则原样抄;
- 根据需要合理使用配接器not1, not2, bind1st, bind2nd,修改一下原本的函数子功能以达到我们想要的仿函数;
- 配接器的参数至少需要一个仿函数:
not1, not2的参数需是仿函数(若是普通函数,需要用ptr_fun()转化一下);
//普通函数isInteresting
bool isInteresting(const Widget* pw);
list<Widget*> widgetPtrs;
auto i = find_if(widgetPtrs.begin(), widgetPtrs.end(), isInteresting); //正确,传递一个普通函数指针
auto i = find_if(widgetPtrs.begin(), widgetPtrs.end(), not1(isInteresting)); //错误,普通数指针不能作为配接器not1的参数
auto i = find_if(widgetPtrs.begin(), widgetPtrs.end(), not1(ptr_fun(isInteresting))); //正确,ptr_fun将函数指针转化为仿函数
bind1st, bind2nd的两个参数至少一个是仿函数(可以实现将一个仿函数和另一个仿函数/一个值/一个普通函数结合得到我们想要的新仿函数)
[四种配接器](https://blog.csdn.net/jiuri1005/article/details/109692075)
- 除特殊场合,都应在每个函数子类中只写一个operator()成员函数,使其具有可配接性;
- 若一个函数子类的所有成员都是共有的,例如仅有一个operator(),则将该函数子类声明为struct;
若函数子类中还包含一些状态信息(如一些private数据成员),则往往声明为class;
第41条:理解ptr_fun, mem_fun, mem_fun_ref的来由
- 对x调用函数f的三种情况:
f是x的类型的非成员函数:① f(x);
f是x类型的成员函数:②x.f(); ③p是指向x的指针,则p->f(); - STL的算法,函数或者函数对象在被调用时,只支持非成员函数的语法形式
//下面为for_each的函数模板,可知for_each只支持f(x)形式的调用
template<typename InputIterator, typename function>
function for_each(InputIterator begin, InputIterator end, function f){
while(begin != end) f(*begin++); //对迭代器范围内的所指的元素,依次作为f的参数
}
//f是Widget的非成员函数,通过语法①直接调用
void f(Widget &){
...
}
vecotr<Widget> vw; //vector中存储Widget对象
for(vm.begin(), vw.end(), f); //①类型的调用:f(Widget),对迭代器范围内的每一个Widget对象w都调用f(w);
//f是Widget的成员函数,通过mem_fun_ref_t将语法②转化为语法①直接调用
class Widget{
public:
...
void f(){}; //*this调用其成员函数f
}
vecotr<Widget> vw; //vector中存储Widget对象
for(vm.begin(), vw.end(), &Widget::f); //错误,无法通过编译
//将Widget::f传入mem_fun_ref,返回一个类型为mem_fun_ref_t的对象;for_each通过语法①对迭代器范围内的每个Widget对//象调用mem_fun_ref_t对象,mem_fun_ref_t对象再通过语法③调用真正的Widget::f函数
for(vm.begin(), vw.end(), mem_fun_ref(&Widget::f)); //&可以省略
//f是Widget的成员函数,通过mem_fun_t将语法③转换为语法①
class Widget{
public:
...
void f(){};
}
vecotr<Widget*> vw; //vector中存储Widget*对象
for(vm.begin(), vw.end(), &Widget::f); //错误,无法通过编译
//将Widget::f传入mem_fun,返回一个类型为mem_fun_t的对象(该对象保存着该成员函数的指针,并提供operator()函数,operator()调用通过参数传递进来的Widget对象的成员函数);
//实际调用过过程:for_each通过语法①对迭代器范围内的每个Widget*对象调用mem_fun_t对象,mem_fun_t对象再通过语法③调用Widget*的成员函数Widget::f();
for(vm.begin(), vw.end(), mem_fun(&Widget::f)); //&可以省略
- mem_fun_t和 mem_fun_ref_t称为函数对象配接器;
- 每当需要将一个成员函数传递给一个STL的组件时,就要使用 mem_fun_t或 mem_fun_ref_t;用于将对成员函数的调用语法转换为对非成员函数的调用语法,以此来适应算法
- 指针容器支持多态,但对象容器不支持多态
第42条:确保less与operator<具有相同的意义
- c++中,less实际调用的operator<,这是共识,因此不要修改less的实现;
- 关键字不要乱用,如果用到了less,就要保证它的实现和调用operator<是同样的效果,这是大家默认的;
- 如果想自定义一种与less不同的排序方式,就换个名字重新写一个新的函数子类,不要试图修改less的实现以达成自己预期的排序方式,会误导其他人;