C++提供了强大且自由的操作符重载能力,可以重新定义大多数操作符的行为,使操作更加简单直观。这方面很好的例子就是标准库中的string和complex,可以像操作内置类型int、double那样对它们进行算术运算和比较运算,非常方便。
但实现重载操作却要比使用它要麻烦很多,因为很多算法具有对称性,如果定义了operator+,那么很自然需要operator-,如果有小于比较,那么也应该有小于等于、大于、大于等于比较。完全实现这些操作符的重载工作是单调乏味的,而且增加的代码量也增加了出错的可能性,还必须保证这些操作符都实现了正确的语义。
实际上很多操作符可以从其他的操作符自动推导出来,比如a!=b可以是!(a==b)。因此原则上只需要定义少量的基本操作符,其他的操作符就可以用逻辑组合实现。
在C++标准的std::rel_ops名字空间里提供了四个模板比较操作符!=、>、<=、>=
,只需要为类定义了==
和<
操作符,那么这四个操作符就可以自动实现。比如:
#include <iostream>
#include <utility>
#include <assert.hpp>
class deme_class{
public:
deme_class(int n) : x(n){
}
int x;
friend bool operator<(const deme_class& l, const deme_class & r){
return l.x < r.x;
}
};
int main() {
deme_class a(10), b(20);
using namespace std::rel_ops; // 打开 std::rel_ops 名字空间
assert(a < b); // 自定义的<操作符
assert(b >= a); // >=操作符被自动实现
return 0;
}
但std::rel_ops的解决方案过于简单,还很不够。除了比较操作符,还有很多其他的操作符重载标准库没有给出解决方案。而且使用这些操作符需要用using语句打开std::rel_ops
名字空间,很不方便,也会带来潜在的冲突风险。
由此,就产生了boost.operators库。它采用类似std::rel_ops的实现手法,允许用户在自己的类里仅定义少量的操作符,就可以自动生成其他操作符重载,而且保证正确的语义实现。
operators位于名字空间boost,需要包含头文件<boost/operators.hpp>
,即:
#include <boost/operators.hpp>
using namespace boost;
基本运算概念
由于C++可重载的操作符非常多,因此operator库是由多个类组成的,分别用来实现不同的运算概念,比如less_than_comparable
定义了<
操作符,left_shiftable
定义了<<
系列操作符。
operators中的概念很多,囊括了C++中的大部分操作符重载。下面是比较常用的:
equality_comparable
:要求提供==
,可自动实现!=
,相等语义。less_than_comparable
:要求提供<
,可自动实现>
、<=
、>=
addable
:要求提供+=
,可自动实现+
subtractable
:要求提供-=
,可自动实现-
incrementable
:要求提供前置++
,可自动实现后置++
decrementable
:要求提供前置--
,可自动实现后置--
equivalent
: 要求提供<
,可自动实现==
,等价语义
这些概念在库中以同名类的形式提供,用于需要以继承的方式来使用它们。继承的修饰符并不重要(private、public
)都可以,因为operators
库里面都是空类,没有成员函数和成员变量,仅定义了数个友元操作符函数。比如:
如果要同时实现多个运算概念则可以使用多重继承技术,把自定义类作为多个概念的子类,但多重继承在使用时存在很多问题,需要用到一些特别的技巧
算术操作符
我们用一个三维空间的点point作为operator库的示范类:
class point{
int x, y, z;
public:
explicit point(int a = 0, int b = 0, int c = 0) : x(a), y(b), z(c){
}
void print() const {
printf("x = %d, y = %d, z = %d\n", x, y, z);
}
};
我们先来实现less_than_comparable,它要求point类提供“<”操作符,并由它继承。因此,我们只需要为point增加父类,并定义less_than_comparable概念所要求的operator<:
#include <iostream>
#include <assert.hpp>
#include <boost/operators.hpp>
using namespace boost;
class point: boost::less_than_comparable<point>{
int x, y, z;
public:
explicit point(int a = 0, int b = 0, int c = 0) : x(a), y(b), z(c){
}
void print() const {
printf("x = %d, y = %d, z = %d\n", x, y, z);
}
friend bool operator<(const point& l, const point& r)
{
return (l.x*l.x + l.y*l.y +l.z*l.z <
r.x*r.x + r.y*r.y +r.z*r.z);
}
};
int main() {
point p0, p1(1,2,3), p2(3,0,5), p3(3,2,1);
assert(p0 < p1 && p1 < p2);
assert(p2 > p0);
assert(p1 <= p3);
assert(!(p1<p3)&&!(p1>p3) );
return 0;
}
boost::less_than_comparable作为基类的用法可能优点奇怪,它把子类point作为了父类的目标参数:less_than_comparable<point>
,看起来好像是个“循环继承”。实际上,point类作为less_than_comparable的模板类型参数,只是用来实现内部的比较操作符,用做操作符函数的类型,没有任何继承关系。less_than_comparable生成的代码可以理解成这样:
struct less_than_comparable{
friend bool operator>=(const point& l, const point& r){
return !(x < y)
}
};
从上面可以推导出,point类定义了一个友元operator<操作符,然后其余的>,<=,>=就由less_than_comparabel自动生成。
同样定义相等关系,可以使用equality_comparable,需要自行实现operator==:
#include <iostream>
#include <assert.hpp>
#include <boost/operators.hpp>
using namespace boost;
class point: boost::less_than_comparable<point, boost::equality_comparable<point> >{
// 使用多种继承
int x, y, z;
public:
explicit point(int a = 0, int b = 0, int c = 0) : x(a), y(b), z(c){
}
void print() const {
printf("x = %d, y = %d, z = %d\n", x, y, z);
}
friend bool operator<(const point& l, const point& r)
{
return (l.x*l.x + l.y*l.y +l.z*l.z <
r.x*r.x + r.y*r.y +r.z*r.z);
}
friend bool operator==(const point& l, const point& r)
{
return r.x == l.x && r.y == l.y && r.z == l.z; }
};
int main() {
point p0, p1(1,2,3), p2(3,0,5), p3(3,2,1);
assert(p0 < p1 && p1 < p2);
assert(p2 > p0);
assert(p1 <= p3);
assert(!(p1<p3)&&!(p1>p3) );
{
point p0, p1(1,2,3), p2(p1), p3(3,2,1);
assert(p1 == p2);
assert(p1 != p3);
}
return 0;
}
基类链
多重继承一直是C++中引发争论的话题,如果使用不当的话,会导致难以优化和砖石型继承。
operators库使用泛型编程的“基类链”技术解决了多重继承的问题,这种技术通过模板把多继承转换为链式的单继承。
前面当讨论到less_than_comparable<point>
这种用法时,我们说它不是继承,然而现在,我们将看到它可以实现继承的功能。这从一个方面展示了泛型编程的强大威力。
operators库的操作符模板类除了接受子类作为比较类型外,还可以接受另外一个类,作为它的父类,由此可以无限串联在一起(但受到编译器的模板编译能力限制),像这样:
demoe : x<demo, y<demo, z<demo, ...>>>
使用基类链技术,point类的基类部分可以是这样:
boost::less_than_comparable<point, //注意这里
boost::equality_comparable<point> > //是一个有很大模板参数列表的类
对比一下多种继承的写法:
boost::less_than_comparable<point>, //注意这里
boost::equality_comparable<point> //有两个类
代码非常相似,区别仅仅在于模板参数列表结束符号>
的位置。正是这个小小的差距,使基类链通过模板组成了一连串的单继承链表,而不是多个父类的多重继承。
比如,如果为point类在增加加法和减法定义,则继承列表就是:
基类链技术会导致代码出现一个有趣的形式:在派生类的基类声明末尾处出现一长串的>
。编写代码时需要小心谨慎以保证尖括号的匹配,使用良好的代码缩进和换行可以减少错误的发生。
复合运算的概念
基类链技术解决了多重继承的效率问题,但它也带来了新的问题,为了使用操作符概念需要写出很长的基类链代码。因此operators库使用基类链把一些简单的运算概念组合成了复杂的概念,即复合运算。复合运算不仅进一步简化了代码的编写,给出了更明确的语义,它也可以避免用户代码中基类链过长的问题。
operators库提供的常用复合运算概念如下:
totally_ordered
:全序概念,组合了equality_comparable和less_than_comparableadditive
:可加减概念,组合了addable和substractablemultiplicative
:可乘除概念,组合了multipliable和dividablearithmetic
:算术运算概念,组合了additive和multiplicativeunit_stoppable
:可步进概念,组合了incrementable和decrementable
使用复合运算概念,point类只需要很少的代码就可以很容器的获得完全的算术运算能力:
#include <iostream>
#include <assert.hpp>
#include <boost/assign.hpp>
#include <boost/operators.hpp>
using namespace boost;
class point : //boost::less_than_comparable<point,
//boost::equality_comparable<point> >
totally_ordered<point,
additive<point> >
{
int x, y, z;
public:
explicit point(int a=0, int b=0, int c=0):x(a),y(b),z(c){
}
void print()const
{
std::cout << x <<","<< y <<","<< z << std::endl; }
friend bool operator<(const point& l, const point& r)
{
return (l.x*l.x + l.y*l.y +l.z*l.z <
r.x*r.x + r.y*r.y +r.z*r.z);
}
friend bool operator==(const point& l, const point& r)
{
return r.x == l.x && r.y == l.y && r.z == l.z; }
point& operator+=(const point& r) //支持addable概念
{
x += r.x;
y += r.y;
z += r.z;
return *this;
}
point& operator-=(const point& r) //支持subtractable概念
{
x -= r.x;
y -= r.y;
z -= r.z;
return *this;
}
};
int main() {
point p0, p1(1,2,3), p2(3,0,5), p3(3,2,1);
using namespace boost::assign;
std::vector<point> v = (list_of(p0), p1, p2, p3);
auto pos = std::find(v.begin(), v.end(), point(1, 2, 3)); // find使用了==
pos->print();
(p1 + p2).print();
(p3 - p1).print();
assert((p2 - p2) == p0);
return 0;
}
相等与等价
相等(equality)与等价(equivalent)是两个极易被混淆的概念。一个简单快速的解释是:相等基于操作符==
,即x == y
;而等价基于<
,即!(x < y) && !(x > y)
,两者在语义上有很大差别
对于简单类型,比如int,相等和等级是完全一致的。但对于大多数复杂类型和自定义类型,由于==
和<
操作符是两个不同的运算,比较原则可能不同,从而两者具有不同的语义。
operators库使用equality_comparable(==
)和equivalent(<
)明确的区分了相等和等价这两个概念。令人困扰的是它们最终都提供了操作符==,表现相似但含义非常不同。
了解相等和等价的区别非常重要,特别是当自定义类被当做容器的元素的时候。标准库中的关联容器(set、map)和排序算法使用的是等价关系<
操作符,而unordered_set/map和各种find查找算法用的是相等关系操作符(==
)。
请谨慎考虑自定义类需要什么语义,如果只关系类的等价语义,那么就用equivalent
,如果想要精确的比较两个对象的值,就使用equality_comparable
解引用操作符
operators
库使用dereferenceable提供了对解引用操作符*
、->
的支持,其类摘要如下:
dereferenceable有三个模板参数:
- 第一个参数T是要实现operator->的子类,它的含义与算法操作符类相同
- 第二个参数P是operator->所返回的类型,也就是指针类型,通常应该是T*
- 最后一个参数B是用于基类链技术的父类,实际使用时我们并不关心
dereferenceable类要求子类提供operator*,会自动实现operator->。注意它的operator->函数的定义,不是如其他算术操作符类那样的友元函数,所以必须使用public来继承dereferenceable,否则operator->将会称为类的私有函数,外界无法访问。
由于dereferenceable实现了解引用操作符的语义,因此它可以用于实现自定义的智能指针类,或者是实现代理模式,包装代理某些对象。
比如,下面的代码实现了一个简单的智能指针类my_smart_prt,它public继承了dereferenceable,重载operator*并自动获得了operator->的定义:
template<typename T>
class my_smart_ptr:
public dereferenceable<my_smart_ptr<T>, T* >
{
T *p;
public:
my_smart_ptr(T *x):p(x){
}
~my_smart_ptr(){
delete p;}
T& operator*() const
{
return *p; }
};
void case5()
{
my_smart_ptr<string > p(new string("123"));
assert(p->size() == 3);
}
下标操作符
operators库使用indexable提供了下标操作符[]
的支持,它也属于解引用的范畴,用法和dereferenceable很相似,类摘要如下:
indexable目标参数列表中T和B含义与dereferenceable的T和B含义相同,分别是子类类型和基类链的父类类型。
参数I是下标操作符的值类型,通常应该是整数,但也可以是其他类型,只要它能够与类型T做加法操作。参数R是operator[]的返回值类型,通常应该是一个类型的引用。
indexable要求子类提供一个operator+(T, I)的操作定义,类似于一个指针的算术运算,它应该返回一个迭代器类型,能够使用operator*解引用得到R类型的值。
template<typename T>
class my_smart_array:
public indexable<my_smart_array<T>, int, T& >
{
T *p;
public:
typedef my_smart_array<T> this_type;
typedef T* iter_type;
my_smart_array(T *x):p(x){
}
~my_smart_array(){
delete[] p;}
friend iter_type operator+(const this_type& a, int n)
{
return a.p + n;
}
};
void case6()
{
my_smart_array<double> ma(new double[10]);
ma[0] = 1.0;
*(ma + 1) = 2.0;
cout << ma[1] << endl;
}
bool转型操作符
转型操作符是C++中的一类特殊操作符,而operator bool则更具有特殊性,如果简单的实现bool转型,那么会因为隐式转换在比较操作时会发生意想不到的问题。