用法
为了开始使用Operators库,为你的类实现适用的操作符,就要包含头文件"boost/operators.hpp"
, 并从一个或多个Operator基类(它们的名字与它们所表示的概念一样)进行派生,它们都定义在名字空间 boost
中。注意,继承不一定要是公有的,私有继承也可以。在这一节,我们将看到几个使用不同概念的例子,并关注一下在C++里以及在概念上,算术操作符和关系操作符是如何工作的。作为第一个例子,我们定义一个类,some_class
, 带有一个 operator<
. 我们决定把operator<
所暗指的等价关系定义为 operator==
. 这个工作可以通过从boost::equivalent
继承而完成。
#include <iostream>
#include "boost/operators.hpp"
class some_class : boost::equivalent<some_class> {
int value_;
public:
some_class(int value) : value_(value) {}
bool less_than(const some_class& other) const {
return value_<other.value_;
}
};
bool operator<(const some_class& lhs, const some_class& rhs) {
return lhs.less_than(rhs);
}
int main() {
some_class s1(12);
some_class s2(11);
if (s1==s2)
std::cout << "s1==s2\n";
else
std::cout << "s1!=s2\n";
}
operator<
依照成员函数 less_than
实现。equivalent
基类的要求就是派生的类必须提供 operator<
。从equivalent
派生时,我们要把派生类some_class
作为模板参数传送。在 main
里,使用了Operators库为我们生成的operator==
。接下来,我们再看一来 operator<
,看看其它的关系操作符如何依照 less than 实现。
对比较操作符的支持
我们最常实现的关系操作符就是less than,也就是 operator<
. 为了要把对象存入关联容器,或者是为了要排序,我们都要提供它。然而,通常我们仅支持这一个操作符,这样会把类的使用者弄糊涂。例如,多数人知道对operator<
的结果取反就相当于 operator>=
.[1] Less than 也可以用来计算 greater than, 等等。所以,一个支持less than关系的类的使用者有充足的理由相信,支持(至少隐式地支持)其它的比较操作符也应该是类的接口的一部分。唉,如果我们仅仅支持了 operator<
而忽略了其它的,这个类就不是它可以的或者它应该的那样有用了。这里有一个类,它已经可以用于标准库容器的排序程序。
[1] 尽管也有很多人以为是
operator>
!
class thing {
std::string name_;
public:
thing() {}
explicit thing(const std::string& name):name_(name) {}
friend bool operator<(const thing& lhs, const thing& rhs) {
return lhs.name_<rhs.name_;
}
};
这个类支持排序,也可以被存入关联容器中,但它可能还不能满足用户的期望!例如,如果一个用户需要知道 thing a
是否大于 thing b
, 他就要这样写:
// is a greater than b?
if (b<a) {}
虽然这段代码是正确的,但是它未能清晰地表达作者的意图,而这对于代码的正确性是很重要的。如果用户想知道 a
是否小于或等于 b
, 他不得不这样写:
// is a less than, or equal to, b?
if (!(b<a)) {}
同样,这段代码是正确的,但它会让人糊涂;对于多数不留意的读者来说,代码的意图真的很不清晰。如果要引入等价的概念,代码将变得更令人糊涂,而我们是支持等价关系的(否则我们的类不能存入关联容器中)。
// is a equivalent to b?
if (!(a<b) && !(b<a)) {}
请注意,等价和相等是不一样的,后面的章节将展开讨论这个主题。在C++中,前面所述的所有关系特性都有不同的表示方式,它们是通过不同的操作符来明确地进行测试的。前面的例子应该是象这样(等价关系可能是个例外,但我们在这先不管它):
if (a>b) {}
if (a<=b) {}
if (a==b) {}
现在,注释是多余的了,因为代码已经讲清楚了一切。照这个样子,代码是不能编译的,因为 thing
类不支持 operator>
, operator<=
, 或 operator==
. 但是,对于具有less_than_comparable
概念的类型,这些操作符(除了 operator==
)都能被表达,Operators库可以帮助我们。我们要做的全部工作就是让 thing
派生自 boost::less_than_comparable
, 如下:
class thing : boost::less_than_comparable<thing> {
仅仅通过指定一个基类,就可以依照operator<
实现所有的操作符,thing
类现在可以按你所期望的那样工作了。如你所见,要从Operators库中的类派生出 thing
,我们必须把 thing
作为模板参数传递给基类。这种技术将在后面的章节里讨论。注意,operator==
对于支持 less_than_comparable
的类并无定义,但我们还有一个概念可用,即 equivalent
. 从 boost::equivalent
派生就可以增加 operator==
, 但是要注意,这里的 operator==
是定义为等价关系,而不是相等关系。等价意味着严格的弱序关系[2]。我们最后版本的类 thing
看起来应该是这样的:
[2] 如果你对严格弱序感到奇怪,可以跳到下一节,但是不要忘了稍后回到这里!
class thing :
boost::less_than_comparable<thing>,
boost::equivalent<thing> {
std::string name_;
public:
thing() {}
explicit thing(const std::string& name):name_(name) {}
friend bool operator<(const thing& lhs,const thing& rhs) {
return lhs.name_<rhs.name_;
}
};
这个版本在thing
的定义中仅给出了一个操作符,保持了定义的简洁,依靠派生自 less_than_comparable
和 equivalent
, 它提供了一整组有用的操作符。
bool operator<(const thing&,const thing&);
bool operator>(const thing&,const thing&);
bool operator<=(const thing&,const thing&);
bool operator>=(const thing&,const thing&);
bool operator==(const thing&,const thing&);
你肯定见过很多提供了多个操作符的类。这些类的定义很难阅读,由于有太多的操作符函数的声明/实现。通过从operators
中的概念类派生,你提供了相同的接口,但做得更清楚也更少代码。在类的定义中提及这些概念,可以使熟悉less_than_comparable
和 equivalent
的读者清楚地知道这个类支持这些关系操作符。
Barton-Nackman技巧
在前面两个例子中,我们看到从operator基类继承的方法,一个看起来怪怪的地方是,把派生类传给基类作为模板参数。这是一种著名的技巧,被称为 Barton-Nackmann 技巧[3] 或 Curiously Recurring Template Pattern[4]。这种技巧所解决的问题是循环的依赖性。考虑一下实现一个泛型类,它为另一个定义了operator<
的类提供operator==
。顺便说一下,这就是这个库(当然还有mathematics库)中称为 equivalent
的概念。很明显,任何类要利用提供了这种服务的具体实现,它都要了解提供服务的这个类,我们以这个类所实现的概念来命名它,称之为 equivalent
类。然而,我们刚刚还在说 equivalent
要了解那个它要为之定义operator==
的类!这是一种循环的依赖性,乍一看,好象没有办法可以解决。但是,如果我们把 equivalent
实现为类模板,然后指定那个要定义operator==
的类为模板的参数,这样我们就已经有效地把相关类型,也即是那个派生类,加入到 equivalent
的作用域中了。以下例子示范了如何使用这个技巧。
[3] 由John Barton 和 Lee Nackmann "发明"。
[4] 由James Coplien"发明"。
#include <iostream>
template <typename Derived> class equivalent {
public:
friend bool operator==(const Derived& lhs,const Derived& rhs) {
return !(lhs<rhs) && !(rhs<lhs);
}
};
class some_class : equivalent<some_class> {
int value_;
public:
some_class(int value) : value_(value) {}
friend bool operator<(const some_class& lhs,
const some_class& rhs) {
return lhs.value_<rhs.value_;
}
};
int main() {
some_class s1(4);
some_class s2(4);
if (s1==s2)
std::cout << "s1==s2\n";
}
基类 equivalent
接受一个要为之定义operator==
的类型为模板参数。它通过使用operator<
为该参数化类型实现泛型风格的operator==
。然后,类 some_class
想要利用 equivalent
的服务,就从它派生并把自己作为模板参数传递给 equivalent
。因此,结果就是为类型some_class
定义了 operator==
,是依照 some_class
的 operator<
实现的。这就是Barton-Nackmann技巧的全部内容。这是一种简单且非常有用的模式,相当优美。
严格弱序(Strick Weak Ordering)
在本书中,我已经两次提到了严格弱序(strict weak orderings),如果你不熟悉它,本节将离开主题一会,给你解释一下。严格弱序是两个对象间的一种关系。首先我们来一点理论的讲解,然后再具体地讨论。一个函数 f(a,b)
如果实现了一种严格弱序关系,这里的 a
和 b
是同一类型的两个对象,我们说, a
和 b
是等价的,如果f(a,b)
是 false 并且 f(b,a)
也是 false。这意味着 a
不在 b
之前,而且 b
也不在 a
之前。因此我们可以认为它们是等价的。此外,f(a,a)
必须总是 false
[5],而且如果 f(a,b)
为 true
, 则 f(b,a)
必须为 false
.[6] 还有,如果 f(a,b)
与 f(b,c)
均为 true
, 则有 f(a,c)
.[7] 最后,如果 f(a,b)
为 false
且 f(b,a)
也为 false
, 并且如果 f(b,c)
为 false
且 f(c,b)
也为 false
, 则 f(a,c)
为 false
且 f(c,a)
为 false
.[8]
[5] 即自反性。
[6] 即反称性。
[7] 即传递性。
[8] 即等价关系的传递性。
我们前面的例子(类 thing
)可以有助于澄清这个理论。thing
的小于比较是依照std::string
的小于比较实现的。也就是说,是一种字面的比较。因此,给出一个包含字符串"First"的 thing a
,和一个包含字符串"Second"的 thing b
,还有一个包含字符串"Third"的 thing c
,我们可以 assert
前面给出的定义和公理。
#include <cassert>
#include <string>
#include "boost/operators.hpp"
// Definition of class thing omitted
int main() {
thing a("First");
thing b("Second");
thing c("Third");
// assert that a<b<c
assert(a<b && a<c && !(b<a) && b<c && !(c<a) && !(c<b));
// 等价关系
thing x=a;
assert(!(x<a) && !(a<x));
// 自反性
assert(!(a<a));
// 反对称性
assert((a<b)==!(b<a));
// 传递性
assert(a<b && b<c && a<c);
// 等价关系的传递性
thing y=x;
assert( (!(x<a) && !(a<x)) &&
(!(y<x) && !(x<y)) &&
(!(y<a) && !(a<y)));
}
现在,所有这些 assert
s 都成立,因为 std::string
实现了严格弱序[9]。就象 operator<
可以定义一个严格弱序,operator>
也可以。稍后,我们将看到一个非常具体的例子,看看如果我们未能区分等价(它是一个严格弱序所要求的)与相等(它不是严格弱序所要求的)之间的不同,将会发生什么。
[9] 事实上,
std::string
定义了一个全序,全序即是严格弱序外加一个要求:等价即为相等。
避免对象膨胀
在前面的例子中,我们的类派生自两个基类:less_than_comparable<thing>
和 equivalent<thing>
. 根据你所使用的编译器,你需要为这个多重继承付出一定的代价;thing
可能要比它所需的更大。标准允许编译器使用空类优化来创建一个没有数据成员、没有虚拟函数、也没有重复基类的基类,这样在派生类的对象中只会占用零空间, 而多数现代的编译器都会执行这种优化。不幸的是,使用Operators库常常会导致从多个基类进行继承,这种情况下只有很少编译器会使用空类优化。为了 避免潜在的对象大小膨胀,Operators支持一种称为基类链(base class chaining) 的技术。每个操作符类接受一个可选的额外的模板参数,该参数来自于它的派生类。采用以下方法:一个概念类派生自另一个,后者又派生自另一个,后者又派生自 另一个…(你应该明白了吧),这样就不再需要多重继承了。这种方法很容易用。不要再从几个基类进行继承了,只要简单地把几个类链在一起就行 了,如下所示:
// Before
boost::less_than_comparable<thing>,boost::equivalent<thing>
// After
boost::less_than_comparable<thing,boost::equivalent<thing> >
这种方法避免了从多个空基类进行继承,而从多个基类继承可能会阻碍你的编译器进行空类优化,使用从一 个空基类链进行继承的方法,增加了编译器进行空类优化的机会,也减少了派生类的大小。你可以用你的编译器做一下试验,看看你可以从这个技术中获得多少好 处。注意,基类链长度是有限制的,这取决于编译器。对于程序员可以接受的基类链长度也是很有限的!这意味着对那些需要从很多operator类进行继承的类来说,我们需要把它们组合起来。更好的方法是,使用Operators库已提供的复合概念。
以我的测试来看,在某个对多重继承不执行空类优化的流行编译器上[10], 使用基类链和使用多重继承所得到的类的大小有很大的差别。使用基类链确实可以避免类型增大的负作用,而使用多重继承则会有类型的增大,对于一个普通类型, 大小将增加8个字节(无可否认,8个额外的字节对于多数应用来说并不是问题)。如果被包装的类型的大小非常小,那么多重继承带来的额外开销就不是可以接受 的了。由于基类链很容易使用,我们应该在所有情况下都使用它!
[10] 我这样说一方面是因为没有必要讲出它的名字,另一方面也因为每个人都知道我说的是 Microsoft的老编译器 (他们的新编译器可能不是)。
Operators 与不同的类型
有时候,一个操作符要包括一个以上的类型。例如,考虑一个字符串类,它支持通过operator+
和 operator+=
从字符数组进行字符串连接。这种情况下,Operators库也可以帮忙,使用双参数版本的操作符模板。这个字符串类可能拥有一个接受char*
的转换构造函数,但正如我们将看到的,这不能解决这个类的所有问题。以下是我们要用的字符串类。
class simple_string {
public:
simple_string();
explicit simple_string(const char* s);
simple_string(const simple_string& s);
~simple_string();
simple_string& operator=(const simple_string& s);
simple_string& operator+=(const simple_string& s);
simple_string& operator+=(const char* s);
friend std::ostream&
operator<<(std::ostream& os,const simple_string& s);
};
如你所见,我们为simple_string
增加两个版本的 operator+=
。一个接受 const simple_string&
, 另一个接受 const char*
. 这样,我们的类支持如下用法:
simple_string s1("Hello there");
simple_string s2(", do you like the concatenation support?");
s1+=s2;
s1+=" This works, too";
虽然前面的工作符合要求,但我们还没有提供二元的operator+
,这个疏忽肯定是类的使用者所不乐意的。注意,对于我们的simple_string
,我们可以通过忽略显式的转换构造函数来允许字符串连接。但是,这样做会导致对字符缓冲的一次额外(不必要)的复制,而仅仅节省了一个操作符的定义。
// 以下不能编译
simple_string s3=s1+s2;
simple_string s4=s3+" Why does this class behave so strangely?";
现在让我们来用Operators库来为这个类提供漏掉的操作符。注意共有三个操作符没有提供。
simple_string operator+(const simple_string&,const simple_string&);
simple_string operator+(const simple_string& lhs, const char* rhs);
simple_string operator+(const char* lhs, const simple_string& rhs);
如果手工定义这些操作符,很容易就会忘记那个接受一个 const simple_string&
和一个 const char*
的重载。而使用Operators库,你就不会忘记了,因为库已经为你实现了这些漏掉的操作符!我们想为 simple_string
做的就是加一个 addable 概念,所以我们只要简单地从boost::addable<simple_string>
派生 simple_string
就行了。
class simple_string : boost::addable<simple_string> {
但是,在这个例子中,我们还想要一个允许simple_string
和 const char*
混合使用的操作符。为此,我们需要指定两个类型,结果类型是simple_string
, 以及第二参数类型是 const char*
. 我们可以利用基类链来避免增大类的大小。
class simple_string :
boost::addable<simple_string,
boost::addable2<simple_string,const char*> > {
这就是为了支持我们想要的全部操作符所要做的全部事情!如你所见,我们用了一个不同的operator类:addable2
. 如果你用的编译器支持模板偏特化,你就不需要限定这个名字;你可以用 addable
代替 addable2
. 为了对称性,还有一个版本的类,它带有后缀"1"。它可以增加可读性,它总是明确给出参数的数量,它带给我们以下对simple_string
的派生写法:
class simple_string :
boost::addable1<simple_string,
boost::addable2<simple_string,const char*> > {
选择哪种写法,完全取决于你的品味,如果你的编译器支持模板偏特化,最简单的选择是忽略所有后缀。
class simple_string :
boost::addable<simple_string,
boost::addable<simple_string,const char*> > {
相等与等价的区别
为类定义关系操作符时,很重要的一点是分清楚相等和等价。要使用关联容器,就要求有等价关系,它通过概念LessThanComparable[11]定义了一个严格弱序。这个关系只需最小的假设,并且对于要用于标准库容器的类型来说,这是最低的要求。但是,相等与等价之间的区别有时会令人混淆,弄明白它们之间的差别是很重要的。如果一个类支持概念LessThanComparable, 通常它也就支持等价的概念。如果两个元素进行比较,没有一个比另一个小,我们称它们是等价的。但是,等价并不意味着相等。例如,有可能在一个less than关系中忽略某些特性,但并不意味着它们就是相等的[12]。为了举例说明这一点,我们来看一个类,animal
, 它同时支持等价关系和相等关系。
[11] 大写的概念,如 LessThanComparable 直接来自于C++标准。所有 Boost.Operators 中的概念使用小写名字。
[12] 这意味着一个严格弱序,但不是一个全序。
class animal : boost::less_than_comparable<animal,
boost::equality_comparable<animal> > {
std::string name_;
int age_;
public:
animal(const std::string& name,int age)
:name_(name),age_(age) {}
void print() const {
std::cout << name_ << " with the age " << age_ << '\n';
}
friend bool operator<(const animal& lhs, const animal& rhs) {
return lhs.name_<rhs.name_;
}
friend bool operator==(const animal& lhs, const animal& rhs) {
return lhs.name_==rhs.name_ && lhs.age_==rhs.age_;
}
};
请注意 operator<
和 operator==
的实现间的区别。在less than关系中仅使用了动物的名字,而在相等检查中则同时比较了名字和年龄。这种方法没有任何错误,但是它会导致有趣的结果。现在让我们把这个类的一些元素存入 std::set
. 和其它关联容器一样,set
仅依赖于概念 LessThanComparable. 以下面例子中,我们创建四个不一样的动物,然后试图把它们插入一个 set
, 完全假装我们不知道相等和等价之间的差别。
#include <iostream>
#include <string>
#include <set>
#include <algorithm>
#include "boost/operators.hpp"
#include "boost/bind.hpp"
int main() {
animal a1("Monkey", 3);
animal a2("Bear", 8);
animal a3("Turtle", 56);
animal a4("Monkey", 5);
std::set<animal> s;
s.insert(a1);
s.insert(a2);
s.insert(a3);
s.insert(a4);
std::cout << "Number of animals: " << s.size() << '\n';
std::for_each(s.begin(),s.end(),boost::bind(&animal::print,_1));
std::cout << '\n';
std::set<animal>::iterator it(s.find(animal("Monkey",200)));
if (it!=s.end()) {
std::cout << "Amazingly, there's a 200 year old monkey "
"in this set!\n";
it->print();
}
it=std::find(s.begin(),s.end(),animal("Monkey",200));
if (it==s.end()) {
std::cout << "Of course there's no 200 year old monkey "
"in this set!\n";
}
}
运行这个程序,会有以下完全荒谬的输出结果。
Number of animals: 3
Bear with the age 8
Monkey with the age 3
Turtle with the age 56
Amazingly, there's a 200 year old monkey in this set!
Monkey with the age 3
Of course there's no 200 year old monkey in this set!
问题不在于猴子的年龄——它的确不寻常——而在于没有了解这两种关系概念间的区别。首先,当四个 animal
s (a1
, a2
, a3
, a4
)被插入到 set
, 第二只猴子,a4
, 其实并没有被插入,因为 a1
和 a4
是等价的。原因是,std::set
使用表达式 !(a1<a4) && !(a4<a1)
来决定是否已有一个匹配的元素。由于这个表达式的结果为 true
(我们的 operator<
不比较年龄), 所以插入失败了[13]。然后,当我们询问这个set,使用find
查找一个200岁的猴子时,它找到了这样一只怪物。同样,这是由于animal
的等价关系,仅依赖于animal
的operator<
,因而还是不关心年龄。最后,我们再次用 find
在 set
中定位这只猴子(a1
), 但这次我们调用 operator==
来判断它是否匹配,从而没有找到匹配的猴子。通过对这些猴子的讨论,不难理解相等与等价之间的差别,但你必须知道在给定的上下文中使用的是哪一个概念。
[13] 一个set, 根据定义, 不存在重复的元素。
算术类型
定义算术类型时,Operators库尤其有用。对于一个算术类型,通常有很多操作符要定义,而手工 去做这些工作是一项令人畏缩和沉闷的任务,并很可能发生错误或疏忽。Operators库中定义的概念使这项工作变得容易,你只需为类定义最少的必须的操 作符,剩下的操作符就可以自动实现。考虑一个支持加法和减法的类。假设这个类使用一个内建类型作为实现。现在要增加适当的操作符,并确保它们不仅可以用于 这个类的实例,还可以用于可转换为这个类的内建类型。你将要提供12个不同的加法和减法操作符。当然,更容易(也是更安全)的方法是,使用addable
和 subtractable
类的双参数形式。现在假设你还需要增加一组关系操作符。你可能要自己增加10个操作符,但现在你知道了最容易的方法是使用 less_than_comparable
和 equality_comparable
. 这样做之后,你拥有了22个操作符而只定义了6个。然而,你可能也注意到了这些概念对于数值类型来说是很常见的。的确如此,作为这四个类的替换,你可以仅使用 additive
和 totally_ordered
.
我们先从四个概念类进行派生开始:addable
, subtractable
, less_than_comparable
, 和 equality_comparable
. 类limited_type
, 仅仅包装了一个内建类型并将所有操作符前转给那个类型。它限制可用操作的数量,仅提供了关系操作符和加减法。
#include "boost/operators.hpp"
template <typename T> class limited_type :
boost::addable<limited_type<T>,
boost::addable<limited_type<T>,T,
boost::subtractable<limited_type<T>,
boost::subtractable<limited_type<T>,T,
boost::less_than_comparable<limited_type<T>,
boost::less_than_comparable<limited_type<T>,T,
boost::equality_comparable<limited_type<T>,
boost::equality_comparable<limited_type<T>,T >
> > > > > > > {
T t_;
public:
limited_type():t_() {}
limited_type(T t):t_(t) {}
T get() {
return t_;
}
// 为less_than_comparable提供
friend bool operator<(
const limited_type<T>& lhs,
const limited_type<T>& rhs) {
return lhs.t_<rhs.t_;
}
// 为equality_comparable提供
friend bool operator==(
const limited_type<T>& lhs,
const limited_type<T>& rhs) {
return lhs.t_==rhs.t_;
}
// 为addable提供
limited_type<T>& operator+=(const limited_type<T>& other) {
t_+=other.t_;
return *this;
}
// 为subtractable提供
limited_type<T>& operator-=(const limited_type<T>& other) {
t_-=other.t_;
return *this;
}
};
这是一个不错的例子,示范了使用Operators库后实现变得多么容易。仅需实现几个必须实现的操 作符,就很容易地获得了全组的操作符,而类也变得更易懂以及更易于维护。(即使实现这些操作符是困难的,你也可以把注意力集中于正确地实现它们)。这个类 唯一的潜在问题就是,它派生自八个不同的operator类,使用了基类链的方式,对于某些人而言,这可能不好阅读。我们可以通过使用复合概念来大大简化 我们的类。
template <typename T> class limited_type :
boost::additive<limited_type<T>,
boost::additive<limited_type<T>,T,
boost::totally_ordered<limited_type<T>,
boost::totally_ordered<limited_type<T>,T > > > > {
这更好看,而且也减少了击键的次数。
仅在应用使用Operators时使用它
很明显operators应该仅在适当的时候使用,但出于某些原因,operators的某些"很酷 的因素"常常诱使一些人在不清楚它们的语义时就使用它们。很多情形下都需要操作符,如在同一类型的实例间存在某种关系时,又或者在创建一个算术类型时。但 也有一些不太清晰的情形,你就需要考虑使用者的真正期望,如果用户的期望是模糊的,最好还是选择用成员函数。
多年以来,Operators已经被用于一些不平常的服务。增加操作符用于连接字符串,和使用位移操作符进行I/O,就是两个操作符不再具有数学意义而被用于其它语义用途的最常见的例子。也有人对于在std::map
中使用下标操作符访问元素表示质疑(当 然其它人认为这是很自然的。他们是对的)。有时候,把操作符用于某种与内建类型规则不一致的用途是有意义的。而其它时候,它则是非常错误的,会引起混乱和 歧义。当你决定将一个操作符重载为与内建类型不一致的意义时,你必须很小心地去做。你必须确保它的意义是明显的,并且优先级是正确的。这也是在 IOStream库中使用位移操作符进行I/O的原因。位移操作符清晰地表明了将某物移向某个方向,并且位移操作符的优先级比多数操作符都低。如果你创建 一个表示汽车的类,可能发现 operator-=
很方便。但是,对于使用者这个操作符意味着什么?有些人可能认为它被用来表示在驾驶中使用的汽油。其它人可能认为它被用来表示汽车价值的贬值(当然会计师 会这样想)。增加这个操作符是错误的,因为它没有一个清晰的意图,而一个成员函数可以更清楚地为这些操作命名。不要仅仅为了它可以写出"酷"的代码而增加 操作符。要因为它们真的有用而增加它们,确认增加所有适用的操作符,并且确认使用Boost.Operators库!
弄明白它是如何工作的
我们现在来看一看这个库是如何工作的,以进一步加深你对于如何正确使用它的理解。对于Boost.Operators, 这并不难。我们来看看如何实现对 less_than_comparable
的支持。你需要了解你要增加支持的那个类,并且你要为这个类增加操作符,这个操作符将用于实现该类的其它相关操作符。less_than_comparable
要求我们提供 operator<
, operator>
, operator<=
, 和 operator>=
. 现在,你已经知道如何依照operator<
来实现 operator>
, operator<=
, and operator>=
。下面是一种可能的实现方法。
template <class T>
class less_than1
{
public:
friend bool operator>(const T& lhs,const T& rhs) {
return rhs<lhs;
}
friend bool operator<=(const T& lhs,const T& rhs) {
return !(rhs<lhs);
}
friend bool operator>=(const T& lhs,const T& rhs) {
return !(lhs<rhs);
}
};
对于 operator>
, 你只需要交换两个参数的顺序。对于 operator<=
, 注意到 a<=b
即意味着 b
不小于 a
. 因此,实现的方法就是以相反的参数顺序调用 operator<
并对结果取反。对于 operator>=
, 同样由于 a>=b
意味着 a
不小于 b
. 因此,实现方法就是对调用 operator<
的结果取反。这是一个可以工作的例子:你可以直接使用它并且它将完成正确的工作。然而,如果可以提供一个支持T
与兼容类型之间进行比较的版本就更好了,这只要简单地增加几个重载就可以了。出于对称性的考虑,你应该允许兼容类型出现在操作符的左边(这一点在手工增加操作符时很容易忘记;人们通常仅留意到操作符的右边要接受其它类型。当然,你的双类型版本 less_than
不会犯如此愚蠢的错误,对吗?)
template <class T,class U>
class less_than2
{
public:
friend bool operator<=(const T& lhs,const U& rhs) {
return !(lhs>rhs);
}
friend bool operator>=(const T& lhs,const U& rhs) {
return !(lhs<rhs);
}
friend bool operator>(const U& lhs,const T& rhs) {
return rhs<lhs;
}
friend bool operator<(const U& lhs,const T& rhs) {
return rhs>lhs;
}
friend bool operator<=(const U& lhs,const T& rhs) {
return !(rhs<lhs);
}
friend bool operator>=(const U& lhs,const T& rhs) {
return !(rhs>lhs);
}
};
这就是了!两个功能完整的 less_than
类。当然,要与Operators库中的 less_than_comparable
具有同样的功能,我们必须去掉表示使用几个类型的后缀。我们真正想要的是一个版本,或者说至少是一个名字。如果你使用的编译器支持模板偏特化,你就是幸运 的,因为基本上只要几行代码就可以实现了。但是,还有很多程序员没有这么幸运,所以我们还是要用健壮的方法来实现它,以完全避开偏特化。首先,我们知道我 们需要某个东西用来调用 less_than
, 它是一个接受一个或两个类型参数的模板。我们也知道第二个类型是可选的,我们可以给它加一个缺省类型,我们知道用户不会传递这样一个类型给这个模板。
struct dummy {};
template <typename T,typename U=dummy> class less_than {};
我们需要某种机制来选择正确版本的less_than
(less_than1
或 less_than2
);我们可以无需借助模板偏特化,而通过使用一个辅助类来做到,这个辅助类有一个类型参数,并内嵌一个接受另一个类的嵌套模板 struct
。然后,使用全特化,我们可以确保类型 U
是 dummy
时,less_than1
将被选中。
template <typename T> struct selector {
template <typename U> struct type {
typedef less_than_2<U,T> value;
};
};
前面这个版本创建了一个名为 value
的类型定义,这个类型正是我们已经创建的 模板的一个正确的实例化。
template<> struct selector<dummy> {
template <typename U> struct type {
typedef less_than1<U> value;
};
};
全特化的 selector
创建了另一个版本less_than1
的 typedef
。为了让编译器更容易做,我们将创建另一个辅助类,专门负责收集正确的类型,并把它存入适当的typedef type
.
template <typename T,typename U> struct select_implementation {
typedef typename selector<U>::template type<T>::value type;
};
这种语法看上去不讨人喜欢,因为selector
类中的嵌套模板 struct
,但类的使用者并不需要看到这段代码,所以这不是什么大问题。现在我们有了所有的因素,我们需要从中选择一个正确的实现,我们最终从select_implementation<T,U>::type
派生less_than
,前者将会是 less_than1
或 less_than2
, 这取决于用户给出了一个还是两个类型。
template <typename T,typename U=dummy> class less_than :
select_implementation<T,U>::type {};
就是它了!我们现在有了一个完全可用的 less_than
, 由于我们付出的额外努力,增加了一种检测并选择正确的实现版本的机制,用户现在可以以最容易的方式来使用它。我们还正确地了解了 operator<
如何用于创建一个less_than_comparable
类所用的其它操作符。对其它操作符完成同样的任务只需要小心行事,并弄清楚不同的操作符是如何共同组成新的概念的就行了。
剩下的事情
我们还没有讨论Operators库中的剩余部分,迭代器助手类。我不想给出示例了,因为你主要是在 定义迭代器类型时会用到它们,这需要额外的解释,这超出了本章甚至是本书的范围。我在这里之所以提及它们,是因为如果你正在定义迭代器类型而不借助于 Boost.Iterators的话,你肯定会想用它些助手类的。解引用操作符帮助你定义正确的操作符而无须顾及你是否在需要一个代理类。在定义智能指针 是它们也很有用,智能指针通常要求定义 operator->
和 operator*
. 迭代器助手类把不同类型的迭代器所需的概念组合在了一起。例如,一个随机访问迭代器必须是 bidirectional_iterable
, totally_ordered
, additive
, 和 indexable
的。定义新的迭代器类型时,更适当的做法是借助于Boost.Iterator库,不过Operators库也可以帮忙。