续上篇
第38条:遵循按值传递的原则来设计函数子类。
在STL中,函数对象在函数之间来回传递的时候也是像函数指针那样按值传递的。因此,你的函数对象必须尽可能的小,否则拷贝的开销会很大;其次,函数对象必须是单态的,也就是说,它们不得使用虚函数。这是因为,如果参数的类型是基类类型,而实参是派生类对象,那么在传递过程中会产生剥离问题(slicing problem):在对象拷贝过程中,派生部分可能会被去掉,而仅保留了基类部分(见第3条)。
试图禁止多态的函数子同样也是不实际的。所以必须找到一种两全其美的办法,既允许函数对象可以很大并且/或保留多态性,又可以与STL所采用的按值传递函数子的习惯保持一致。这个办法就是:将所需要的数据和虚函数从函数子中分离出来,放到一个新的类中,然后在函数子中设一个指针,指向这个新类。
第39条:确保判别式是“纯函数”。
一个判别式(predicate)是一个返回值为bool类型的函数。一个纯函数(pure function)是指返回值仅仅依赖于其参数的函数。
因为接受函数子的STL算法可能会先创建函数子对象的拷贝,然后使用这个拷贝,因此这一特性的直接反映就是判别式函数必须是纯函数。
template
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p)
{
begin=find_if(begin, end, p);//可能是p的拷贝
if(begin==end return begin;
else{
FwdIterator next=begin;
return remove_copy_if(++next, end, begin, p);//可能是p的另一个拷贝
}
}
第40条:若一个类是函数子,则应使它可配接。
4个标准的函数配接器(not1、not2、bind1st和bind2nd)都要求一些特殊的类型定义。提供了这些必要的类型定义(argument_type、first_argument_type、second_argument_type以及result_type)的函数对象被称为可配接的(adaptable)函数对象,反之,如果函数对象缺少这些类型定义,则称为不可配接的。可配接的函数对象能够与其他STL组件更为默契地协同工作。不过不同种类的函数子类所需要提供的类型定义也不尽相同,除非你要编写自定义的配接器,否则你并不需要知道有关这些类型定义的细节。这是因为,提供这些类型定义最简便的办法是让函数子从特定的基类继承,或者更准确的说,如果函数子类的operator()只有一个形参,那么它应该从 std::unary_function模板的一个实例继承;如果函数子类的operator()有两个形参,那么它应该从std:: binary_function继承。
对于unary_function,你必须指定函数子类operator()所带的参数的类型,以及返回类型;对于binary_function,你必须指定三个类型:operator()的第一个和第二个参数的类型,以及operator()的返回类型。以下是两个例子:
template
class MeetsThreshold: public std::unary_function
private:
const T threshold;
public:
MeetsThreshold(const T& threshold);
bool operator()(const Widget&) const;
...
};
struct WidgetNameCompare:
public std::binary_function
bool operator() (const Widget& lhs, const Widget& rhs) const;
};
你可能已经注意到MeetsThreshold是一个类,而WidgetNameCompare是一个结构。这是因为MeetsThreshold包含了状态信息(数据成员threshold),而类是封装状态信息的一种逻辑方式;与此相反,WidgetNameCompare并不包含状态信息,因而不需要任何私有成员。如果一个函数子的所有成员都是公有的,那么通常会将其声明为结构而不是类。究竟是选择结构还是类来定义函数子纯属个人编码风格,但是如果你正在改进自己的编码风格,并希望自己的风格更加专业一点的话,你就应该注意到,STL中所有无状态的函数子类(如less
我们在看一下WidgetNameCompare:
struct WidgetNameCompare:
public std::binary_function
bool operator() (const Widget& lhs, const Widget& rhs) const;
};
虽然operator()的参数类型都是const Widget&,但我们传递给binary_function的类型却是Widget。一般情况下,传递给unary_function或 binary_function的非指针类型需要去掉const和引用(&)部分(不要问其中的原因,如果你有兴趣,可以访问 boost,卡可能看他们在调用特性(traits)和函数对象配接器方面的工作)。
如果operator()带有指针参数,规则又有不同了。下面是WidgetNameCOmpare函数子的另一个版本,所不同的是,这次以Widget*指针作为参数:
struct PtrWidgetNameCompare:
public std::binary_function
bool operator() (const Widget* lhs, const Widget* rhs) const;
};
第41条:理解ptr_fun、mem_fun和mem_fun_ref的来由。
如果有一个函数f和一个对象x,现在希望在x上调用f,而我们在x的成员函数之外,那么为了执行这个调用,C++提供了三种不同的语法:
f(x); //语法#1:f是一个非成员函数
x.f(); //语法#2:f是一个成员函数,并且x是一个对象或一个对象引用
p->f(); //语法#3:f是成员函数,并且p是一个指向对象x的指针
现在假设有个可用于测试游戏拍卖Widget对象的函数:
void test(Widget& w);
另有一个存放Widget对象的容器:
vector
为了测试vw中的每一个Widget对象,自然可以用如下的方式来调用for_each:
for_each(vw.begin(), vw.end(), test); //调用#1 (可以通过编译)
但是,加入test是Widget的成员函数,即Widget支持自测:
class Widget{
public:
...
void test();
....
};
那么在理想情况下,应该也可以用for_each在vw中的每个对象上调用Widget::test成员函数:
for_each(vw.begin(), vw.end(), &Widget::test);//调用#2(不能通过编译)
实际上,如果真的很理想的话,那么对于一个存放Widget* 指针的容器,应该也可以通过for_each来调用Widget::test:
list
for_each(lpw.begin(), lpw.end(), &Widget::test);//调用#3(也不能通过编译)
这是因为STL中一种和普遍的惯例:函数或函数对象在被调用的时候,总是使用非成员函数的语法形式(即#1)。
现在mem_fun和mem_fun_ref之所以必须存在已经很清楚了--它们被用来调整(一般是#2和#3)成员函数,使之能够通过语法#1被调用。 mem_fun、mem_fun_ref的做法其实很简单,只要看一看其中任意一个函数的声明就清楚了。它们是真正的函数模板,针对它们所配接的成员函数的圆形的不同,有几种变化形式。我们来看其中一个声明,以便了解它是如何工作的:
template
mem_fun_t
mem_fun(R(C::*pmf) ());
mem_fun带一个指向某个成员函数的指针参数pmf,并且返回一个mem_fun_t类型的对象。mem_fun_t是一个函数子类,它拥有该成员函数的指针,并提供了operator()函数,在operator()中调用了通过参数传递进来的对象上的该成员函数。例如,请看下面一段代码:
list
...
for_each(lpw.begin(),lpw.end(),mem_fun(&Widget::test));//现在可以通过编译了
for_each接受到一个类型为mem_fun_t的对象,该对象中保存了一个指向Widget::test的指针。对于lpw中的每一个 Widget*指针,for_each将会使用语法#1来调用mem_fun_t对象,然后,该对象立即用语法#3调用Widget*指针的 Widget::test()。
(ptr_fun是多余的吗?)mem_fun是针对成员函数的配接器,mem_fun_ref是针对对象容器的配接器。
第42条:确保less
operator<不仅仅是less的默认实现方式,它也是程序员期望less所做的事情。让less不调用operator<而去坐别的事情,这会无端地违背程序员的意愿,这与“少”带给人惊奇的原则(the principle of least astonishment)完全背道而驰。这是很不好的,你应该尽量避免这样做。
如果你希望以一种特殊的方式来排序对象,那么最好创建一个特殊的函数子类,它的名字不能是less。