operators
C++提供了强大且自由的操作符重载能力,可以把大多数操作符重新定义为函数,使操作更加简单直观。这方面很好的例子就是标准库中的string和 complex,可以像操作内置类型int、double那样对它们进行算术运算和比较运算,非常方便。
但实现重载操作符却比使用它要麻烦许多,因为很多运算具有对称性,如果定义了operator+,那么很自然需要operator-,如果有小于比较,那么也应该有小于等于、大于、大于等于比较。完全实现这些操作符的重载工作是单调乏味的,而且增加的代码量也增加了出错的可能性,还必须保证这些操作符都实现了正确的语义。
实际上很多操作符可以从其他的操作符自动推导出来,例如a !=b可以是!(a==b),a>=b可以是!(a<b)。因此原则上只需要定义少量的基本操作符,其他的操作符就可以用逻辑组合实现。
在c++标准的std::rel_ops名字空间里提供了四个模板比较操作符 !=、>、<=、>=,只需要为类定义了==和<操作符,那么这四个操作符就可以自动实现。
#include <utility>
class demo_class //一个定义operator<的类
{
public:
demo_class(int n) :x(n) {
}
int x;
friend bool operator<(const demo_class& l, const demo_class& r)
{
return l.x < r.x;
}
};
void case1()
{
demo_class a(10), b(20);
using namespace std::rel_ops; //打开std::rel_ops名字空间
assert(a < b); //自定义的<操作符
assert(b >= a); //>=等操作符被自动实现
}
但std::rel_ops的解决方案过于简单,还很不够。除了比较操作符,还有很多其他的操作符重载标准库没有给出解决方案,而且使用这些操作符需要用using 语句导入std::rel_ops名字空间,不方便,也会带来潜在的冲突风险。
boost.operators库因此应运而生。它采用类似std::rel_ops 的实现手法,允许用户在自己的类里仅定义少量的操作符(如<),就可方便地自动生成其他操作符重载,而且保证正确的语义实现。
operators位于名字空间boost,为了使用operators组件,需要包含头文件<boost/operators.hpp>,即:
#include <boost/operators.hpp>
using namespace boost;
基本运算概念
由于C++可重载的操作符非常多,因此 operators库是由多个类组成的,分别用来实现不同的运算概念,比如 less_than_comparable定义了<系列操作符,left_shiftable定义了<<系列操作符。
operators中的概念很多,包括了C++中的大部分操作符重载,在这里我们先介绍一些最常用的算术操作符:
equality_comparable : 要求提供==, 可自动实现!=, 相等语义;
less_than_comparable : 要求提供<, 可自动实现>、<=、>=:
addable : 要求提供+=, 可自动实现+;
subtractable : 要求提供-=, 可自动实现-;
incrementable : 要求提供前置++, 可自动实现后置++;
decrementable : 要求提供前置--, 可自动实现后置--;
equivalent : 要求提供<, 可自动实现-=, 等价语义。
这些概念在库中以同名类的形式提供,用户需要以继承的方式来使用它们。继承的修饰符并不重要(private、public都可以),因为 operators库里的类都是空类,没有成员变量和成员函数,仅定义了数个友元操作符函数。
例如,less_than_comparable的形式是:
template <class T>
struct less_than_comparable {
friend bool operator> (const T& x, const T& y);
friend bool operator<= (const T& x, const T& y);
friend bool operator>= (const T& x, const T& y);
};
如果要同时实现多个运算概念则可以使用多重继承技术,把自定义类作为多个概念的子类,但多重继承在使用时存在很多问题,稍后将看到operators库使用了特别的技巧来解决这个问题。
算术操作符的用法
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
{
cout << x << "," << y << "," << z << endl;
}
};
我们先来实现less_than_comparable,它要求point类提供<操作符,并由它继承。假定point的小于关系是由三个坐标值的平方和决定的,下面的代码示范了less_than_comparable的用法,只需要为point增加父类,并定义less_than_comparable概念所要求的operator<:
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
{
cout << x << "," << y << "," << z << 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);
}
... //其他成员函数
};
less_than_comparable作为基类的用法可能稍微有点奇怪,它把子类point作为了父类的模板参数:less_than_comparable<point>
,看起来好像是个“循环继承”。实际上,point类作为less_than_comparable的模板类型参数,只是用来实现内部的比较操作符,用做操作符函数的类型,没有任何继承关系。less_than_comparable生成的代码可以理解成这样:
//template<T = point>
struct less_than_comparable
{
friend bool operator>=(const point& x, const point& y)
{
return !(x < y); }
}
明白了less_than_comparable 的继承用法,剩下的就很简单了:point类定义了一个友元operator<操作符,然后其余的>、<=、>=就由less_than_comparable自动生成。几乎不费什么力气,在没有污染名字空间的情况下我们就获得了四个操作符的能力:
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));
}
同样我们可以定义相等关系,使用equality_comparable,规则是point的三个坐标值完全相等,需要自行实现operator==:
class point : boost::less_than_comparable<point>, //使用多重继承
boost::equality_comparable<point> //新增相等关系
{
public:
friend bool operator<(const point& l, const point& r)
{
/*同前*/ }
friend bool operator==(const point& l, const point& r)
{
return r.x == l.x && r.y == l.y && r.z == l.z; }
};
然后我们就自动获得了operator!=定义:
point p0, p1(1,2,3), p2(p1), p3(3,2,1);
assert(p1 == p2);
assert(p1 != p3);
在使用operators库时要注意一点,模板类型参数必须是子类自身,特别是当子类本身也是个模板类的时候,不要错写成子类的模板参数或者子类不带模板参数的名称,否则会造成编译错误。假如我们改写point类为一个模板类:
template<typename T> class point {
...};
那么如下的形式都是错误的:
template<typename T> class point: boost::less_than_comparable<T>
template<typename T> class point: boost::less_than_comparable<point>
正确的写法应该是:
template<typename T> class point: boost::less_than_comparable<point<T>>
因为只有point<T>
才是模板类point的全名。
基类链
多重继承一直是C++中引发争论的话题,喜欢它的人和讨厌它的人几乎同样多。总的来说,多重继承是一种强大的面向对象技术,但使用不当也很容易引发诸多问题,比如难以优化和经典的“钻石型”继承。
operators库使用泛型编程的“基类链”技术解决了多重继承的问题,这种技术通过模板把多继承转换为链式的单继承。
前面当讨论到 less_than_comparable<point>
这种用法时,我们说它不是继承,然而,现在,我们将看到它居然真的可以实现继承的功能,这从一个方面展示了泛型编程的强大威力。
operators库的操作符模板类除了接受子类作为比较类型外,还可以接受另外一个类,作为它的父类,由此可以无限串联链接在一起(但要受编译器的模板编译能力限制),像这样:
demo: 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类再增加加法和减法定义,则继承列表就是:
class point:
less_than_comparable<point, //小于操作
equality_comparable<point, //相等操作
addable<point, //相加操作
subtractable<point //减法操作
> > > >
{
...};
基类链技术会导致代码出现一个有趣的形式:在派生类的基类声明末尾处出现一长串的>(模板声明的结束符),在编写代码时需要小心谨慎以保证尖括号的匹配,使用良好的代码缩进和换行可以减少错误的发生。
复合运算概念
基类链技术解决了多重继承的效率问题,但它也带来了新的问题,为了使用操作符概念需要写出很长的基类链代码。因此 operators库使用基类链把一些简单的运算概念组合成了复杂的概念,即复合运算。这是个很自然的要求,如果有<,则当然会需要==,如果有了+,则可能还需要-、*和/。复合运算不仅进一步简化了代码的编写,给出了更明确的语义,它也可以避免用户代码中基类链过长的问题。
operators库提供的常用复合运算概念如下:
totally_ordered : 全序概念, 组合了equality_comparable和less_than_comparable
additive : 可加减概念, 组合了addable和subtractable
multiplicative : 可乘除概念, 组合了multipliable和dividable
arithmetic : 算术运算概念, 组合了additive和multiplicative
unit_stoppable 可步进概念, 组合了incrementable和decrementable
使用复合运算概念,point类只需要很少的代码就可以很容易地获得完全的算术运算能力:
class point:
totaily_ordered<point, //全序比较运算
additive<point> > //可加减运算
{
public:
friend bool operator<(const point& l, const point& r){
...}
friend bool operator-- (const point& l, const point& r)i ...]
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;
}
};
point的操作符重载验证代码如下:
int main()
{
point p0, p1(1, 2, 3), p2(5, 6, 7), p3(3, 2, 1);
using namespace boost::assign;
vector<point> v = (list_of(p0), p1, p2, p3);
auto pos = std::find(v.begin(), v.end(), point(1, 2, 3));
pos->print();
(p1 + p2).print();
(p3 - p1).print();
assert((p2 - p2) == p0);
}
point类很好地示范了复合运算操作符的用法:一般情况下,类型T继承boost::totally_ordered<T>
,再定义<、==操作符即可获得完全的比较运算功能,能够用于标准容器和算法。
operators库另一个例子是rational类,它实现了有理数,支持全序和算术运算,不过很可惜的是它虽然使用了operators库,但没有用到复合运算概念,从而导致基类声明有16个“>”。
相等与等价
相等与等价是两个极易被混淆的概念。一个简单快速的解释是:相等基于操作符==,即x==y
;而等价基于<,即 !(x<y) && ! (x>y)
,两者在语义上有很大差别。
对于简单类型(如int),相等和等价两者是一致的,例如5==10/2
和! (5<10/2) && !(5>10/2)
。但对于大多数复杂类型和自定义类型,由于==
和<
操作符是两个不同的运算,比较原则可能不同,从而两者具有不同的意义。
之前的point类是一个很好的例子。p1(1,2,3)和p3(3,2,1)两者完全不相等,但等价,因为等价运算使用的是operator<,它比较依据的是成员变量的平方和:1+2*2+3* 3==3*3+2*2+1
。
operators库使用equality_comparable和 equivalent明确地区分了相等与等价这两个概念。equality_comparable基于==
,equivalent则基于<
。但令人困扰的是它们最终都提供了操作符==
,表现相同但含义非常不同。
了解相等与等价的区别非常重要,特别是当自定义类被用做容器的元素的时候。标准库中的关联容器(set、map)和排序算法使用的是等价关系的<操作符,而unordered_set/map和各种find查找算法使用的是相等关系的==操作符。
point类使用不同的规则定义了==和<,可以获得正确的比较和相等语义,下面是应用于标准容器的示范代码:
void case4()
{
point p0, p1(1, 2, 3), p2(5, 6, 7), p3(3, 2, 1);
using namespace boost::assign;
vector<point> v = (list_of(p0), p1, p2, p3);
auto pos = std::find(v.begin(), v.end(), point(1, 2, 3)); //使用相等语义查找元素
for (; pos != v.end(); //查找下一个相等的元素
pos = std::find(pos + 1, v.end(), point(1, 2, 3)))
{
pos->print(); //1,2,3
}
pos = std::find(v.begin(), v.end(), point(2, 1, 3));
assert(pos == v.end());
}
这段代码将只找到p1(1,2,3),并且最后一个assert 断言成立,找不到值为(2,1,3)的point对象。
如果我们改变point 的定义,不使用equality_comparable,而改用equivalent来实现==操作符(等价语义),那么我们不必单独定义==
操作符,它将由equivalent自动用<操作符来实现。上面的测试代码的行为将完全不同,它会输出两个点:p1(1,2,3)和p3(3,2,1),并且断言失败。
在使用关联容器set和map时更需要留意,它们仅要求<操作符,因此是基于等价语义的,即使使用equality_comparable定义了==操作符,把 point对象放入 set 或 map也会产生equivalent的效果。
请读者谨慎地考虑自定义类需要什么样的语义,如果只关心类的等价语义,那么就用equivalent,如果想要精确地比较两个对象的值,就使用equality_comparable。
解引用操作符
operators库使用dereferenceable提供了对解引用操作符*、->的支持,它的用法与之前介绍的算术操作符不太相同。
dereferenceable的类摘要如下:
template <class T, class P, class B =...>
struct dereferenceable : B
{
P operator->() const;
};
dereferenceable有三个模板参数:
1)第一个参数T是要实现 operator->的子类,它的含义与算术操作符类相同
2)第二个参数Р是operator->所返回的类型,也就是指针类型,通常应该是T*
3)最后一个参数B是用于基类链技术的父类,实际使用时我们不需要关心
dereferenceable类要求子类提供operator*,会自动实现operator->。注意它的operator->函数的定义,不是如其他算术操作符类那样的友元函数,因此在使用dereferenceable时必须使用public继承,否则operator->将会成为类的私有成员函数,外界无法访问。
由于 dereferenceable实现了解引用操作符的语义,因此它可以用于实现自定义的智能指针类,或者是实现代理模式,包装代理某些对象。
例如,下面的代码实现了一个简单的智能指针类my_smart_ptr,它 public继承了dereferenceable,重载operator*并自动获得了operator->的定义:
template<typenameT>
class my_smart_ptr:
public dereferenceable<my_smart_ptr<T>, T*> //必须public继承
{
T *p; //内部保存的指针
public:
my_smart_ptr(T*X): p(x) {
} //构造函数
~my_smart_ptr() {
delete p;} //析构函数
T& operator*()const // operator*定义,必须是常函数
{
return *p; }
};
my_smart_ptr的用法就像是scoped_ptr:
my_smart_ptr<string> p (new string ("123"));
assert(p->size() == 3);
下标操作符
operators库使用indexable提供了下标操作符[]的支持,它也属于解引用的范畴,用法与dereferenceable很相似,类摘要如下:
template <class T, class I, class R, class B>
struct indexable : B
{
R operator[](I n) const;
};
indexable模板参数列表中的T和B含义与dereferenceable的T和B含义相同,分别是子类类型和基类链的父类类型。
参数工是下标操作符的值类型,通常应该是整数,但也可以是其他类型,只要它能够与类型T做加法操作。参数R是operator[]
的返回值类型,通常应该是一个类型的引用。
indexable要求子类提供一个operator+(T,I)
的操作定义,类似于一个指针的算术运算,它应该返回一个迭代器类型,能够使用operator*解引用得到R类型的值。
与dereferenceable 的例子类似,我们使用一个my_smart_array 类来模仿实现scoped_array。my_smart_array 从 indexable 公开继承,下标操作符类型是 int,operator[]的返回值类型是T&:
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; //返回一个迭代器,可以使用operator*操作
}
};
由于 my_smart_array 实现了operator+,因此它支持算术运算,又由于它继承自indexable,所以它还自动获得了operator[]的定义:
int main()
{
my_smart_array<double> ma(new double[10]);
ma[0] = 1.0; //operator[]
*(ma + 1) = 2.0; //指针算法运算
cout << ma[1] << endl; //输出2.0
}
bool转型操作符
转型操作符是C++中的一类特殊操作符,而 operator bool则更具有特殊性,如果简单地实现bool转型,那么会因为隐式转换在比较操作时会发生意想不到的问题。
下面的两个类定义了operator bool,因为可以隐式转换为bool,可以写出奇怪的比较代码:
struct demo_a //隐式转换为true
{
bool operator!() const
{
return false;
}
};
struct demo_b //隐式转换为false
{
bool operator!() const
{
return true;
}
};
int main()
{
demo_a a;
demo_b b;
assert(a != b); //隐式转换为bool执行比较
assert(a > b); //true > false
}
为了避免这类尴尬的问题我们可以使用safe bool 惯用法,定义一个到unspecified_bool函数指针类型的转换4,而C++11则在语法层面引入了显式转型操作符的概念(使用explicit修饰)。Boost程序库提供工具 explicit_operator_bool,可以根据编译器对C++11标准的支持程度选择最恰当的解决方案。
explicit_operator_bool不属于operators库,而是core库的一部分,它位于头文件<boost/core/explicit_operator_bool.hpp>
。
explicit_operator_bool要求类实现operator !() const,提供三个操作符定义宏:
#define BOOST_EXPLICIT_OPERATOR_BOOL() //安全bool转型
#define BOOST_EXPLICIT_OPERATOR_BOOL_NOEXCEPT() //异常保证的bool转型
#define BOOST_CONSTEXPR_EXPLICIT_OPERATOR_BOOL() //编译期常量bool转型
我们只需要根据具体情况实现自己的operator!重载,再选择一个宏即可实现安全bool转型,例如:
#include <boost/core/explicit_operator_bool.hpp>
struct demo_a
{
BOOST_EXPLICIT_OPERATOR_BOOL() //定义显示bool转型操作符
bool operator!() const //定义operator!
{
return false;
}
};
struct demo_b
{
BOOST_EXPLICIT_OPERATOR_BOOL_NOEXCEPT() //定义显示bool转型操作符
bool operator!() const //定义operator!
{
return true;
}
};
int main()
{
demo_a a;
demo_b b;
assert(a && !b);
a > b; //编译错误
}
二元操作符
二元操作符(如-、+)的两个参数一般是同类型的,但有时候也可能会是不同类型的。比如 point,可以允许它与一个整型的标量做加减法,其效果是每个坐标值对标量做加减法。
operators库提供了使用两个模板类型参数的概念类,用来支持这种用法,例如less_than_comparable<T, U>
。对于不支持模板偏特化的编译器,operators 为每个操作符提供额外的两种形式,增加后缀“1”和“2”。如果程序可能在不同的编译器上编译,则为了兼容请使用带扩展的模板。例如:
class point:
boost::totally_ordered<point,
boost::addable1<point>, //一个模板参数的概念类
boost::addable2<point, int> > //两个模板参数的概念类
{
public:
...
point& operator+=(const int& r)
{
x += r;
y += r;
z += r;
return *this;
}
...
}
代码示例
#include <utility>
#include <iostream>
#include <assert.h>
using namespace std;
#include <boost/operators.hpp>
using namespace boost;
//
class demo_class
{
public:
demo_class(int n) :x(n) {
}
int x;
friend bool operator<(const demo_class& l, const demo_class& r)
{
return l.x < r.x;
}
};
void case1()
{
demo_class a(10), b(20);
using namespace std::rel_ops;
assert(a < b);
assert(b >= a);
}
//
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
{
cout << x << "," << y << "," << z << 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;
}
};
void case2()
{
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);
}
}
//
#include <boost/assign.hpp>
void case3()
{
point p0, p1(1, 2, 3), p2(5, 6, 7), p3(3, 2, 1);
using namespace boost::assign;
vector<point> v = (list_of(p0), p1, p2, p3);
auto pos = std::find(v.begin(), v.end(), point(1, 2, 3));
pos->print();
(p1 + p2).print();
(p3 - p1).print();
assert((p2 - p2) == p0);
}
//
void case4()
{
point p0, p1(1, 2, 3), p2(5, 6, 7), p3(3, 2, 1);
using namespace boost::assign;
vector<point> v = (list_of(p0), p1, p2, p3);
auto pos = std::find(v.begin(), v.end(), point(1, 2, 3));
for (; pos != v.end();
pos = std::find(pos + 1, v.end(), point(1, 2, 3)))
{
pos->print();
}
pos = std::find(v.begin(), v.end(), point(2, 1, 3));
assert(pos == v.end());
}
//
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);
}
//
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;
}
//
#include <boost/core/explicit_operator_bool.hpp>
struct demo_a
{
BOOST_EXPLICIT_OPERATOR_BOOL()
bool operator!() const
{
return false;
}
//explicit operator bool() const {
// return true;
//}
};
struct demo_b
{
BOOST_EXPLICIT_OPERATOR_BOOL_NOEXCEPT()
bool operator!() const
{
return true;
}
//explicit operator bool() const {
// return false;
//}
};
void case7()
{
demo_a a;
demo_b b;
assert(a && !b);
//assert(a != b);
//assert(a > b);
}
//
int main()
{
case1();
case2();
case3();
case4();
case5();
case6();
case7();
}