boost:operators 布满荆棘的人生 2022-08-29 10:57 157阅读 0赞 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`库里面都是空类,没有成员函数和成员变量,仅定义了数个友元操作符函数。比如: ![在这里插入图片描述][46160cfa7b394626ae576caa80ad021a.png] 如果要同时实现多个运算概念则可以使用多重继承技术,把自定义类作为多个概念的子类,但多重继承在使用时存在很多问题,需要用到一些特别的技巧 ## 算术操作符 ## 我们用一个三维空间的点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类在增加加法和减法定义,则继承列表就是: ![在这里插入图片描述][9f430cd052a24906807f7a7f6a7929c2.png] 基类链技术会导致代码出现一个有趣的形式:在派生类的基类声明末尾处出现一长串的`>`。编写代码时需要小心谨慎以保证尖括号的匹配,使用良好的代码缩进和换行可以减少错误的发生。 ## 复合运算的概念 ## 基类链技术解决了多重继承的效率问题,但它也带来了新的问题,为了使用操作符概念需要写出很长的基类链代码。因此operators库使用基类链把一些简单的运算概念组合成了复杂的概念,即复合运算。复合运算不仅进一步简化了代码的编写,给出了更明确的语义,它也可以避免用户代码中基类链过长的问题。 operators库提供的常用复合运算概念如下: * `totally_ordered`:全序概念,组合了equality\_comparable和less\_than\_comparable * `additive`:可加减概念,组合了addable和substractable * `multiplicative`:可乘除概念,组合了multipliable和dividable * `arithmetic`:算术运算概念,组合了additive和multiplicative * `unit_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提供了对解引用操作符`*`、`->`的支持,其类摘要如下: ![在这里插入图片描述][5a6633a43bf441709c0dd14d29e22c3b.png] 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很相似,类摘要如下: ![在这里插入图片描述][70dab19a152c4831ae3f4f10588b9af5.png] 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转型,那么会因为隐式转换在比较操作时会发生意想不到的问题。 ### 文章目录 ### * * 基本运算概念 * 算术操作符 * 基类链 * 复合运算的概念 * 相等与等价 * 解引用操作符 * 下标操作符 * bool转型操作符 [46160cfa7b394626ae576caa80ad021a.png]: /images/20220829/8330e0e9a6dd47699e0a9bcbef983c53.png [9f430cd052a24906807f7a7f6a7929c2.png]: /images/20220829/2c00d0c1e88d4c87800b4fa3d1146367.png [5a6633a43bf441709c0dd14d29e22c3b.png]: /images/20220829/692644b484324477ac079410539df482.png [70dab19a152c4831ae3f4f10588b9af5.png]: /images/20220829/f944054f347c41b9a30a55b54f8700ba.png
还没有评论,来说两句吧...