boost:operators

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_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提供了对解引用操作符*->的支持,其类摘要如下:

在这里插入图片描述
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转型,那么会因为隐式转换在比较操作时会发生意想不到的问题。

猜你喜欢

转载自blog.csdn.net/zhizhengguan/article/details/119247328