Search code examples
c++operator-overloadingcodesynthesis

Canonical relation operators (==,<,...)


Consider a struct (as in: stupid aggergation of several members) with members that all implement a certain relation R (e.g. <):

struct X {
  A a;
  B b;
};

For most operators there exists a canonical definition for X R X. For instance:

bool operator<(X const& x1, X const& x2) {
  if ((x1.a < x2.a) || (x2.a < x1.a)) // I intentionally did not use != here
    return x1.a < x2.a;
  if ((x1.b < x2.b) || (x2.b < x1.b))
    return x1.b < x2.b;
  return false;
}

This is pretty boring to do for all operators, especially if you have quite some members and not only one such struct.

As you can see, operator< over X only relies on operator< of its member types (A,B) besids the use of bool || bool.

Is there a way to specify such operators generically (via templates or builtins?). Boost is not an option (but it would be interesting if it can do this, nevertheless).

It would be even greater if you could specify the evaluation order of the members (for speed).

Edit This question considers C++03, as otherwise you could use std::tuple, I guess.


Solution

  • As apparently there is no non-boost solution, I brewed up some template magic, which I am posting as an answer in case somebody has the same problem;

    Version 1: explicit arguments

    namespace multioperator {
    
      enum LazyBool {
        LB_false = false,
        LB_true = true,
        LB_undefined
      };
    
      template <typename Cmp, typename B> class Operator {
        public:
          typedef typename Cmp::first_argument_type A;
        private:
          A const& a1;
          A const& a2;
          B const& b;
        public:
          Operator(A const& a1, A const& a2, B const& b)
            : a1(a1), a2(a2), b(b) {
          }
          operator bool() const {
            switch (static_cast<LazyBool>(Cmp(a1,a2))) {
              case LB_false:
                return false;
              case LB_true:
                return true;
              case LB_undefined:
              default: // g++ does not understand that we have all branches :(
                return static_cast<bool>(b);
            }
          }
      };
    
      template <typename Fn> class BinaryFunctorMonad {
        public:
          typedef typename Fn::first_argument_type first_argument_type;
          typedef typename Fn::second_argument_type second_argument_type;
          typedef typename Fn::result_type result_type;
        private:
          first_argument_type const& a;
          second_argument_type const& b;
        public:
          BinaryFunctorMonad(first_argument_type const& a, second_argument_type const& b)
            : a(a), b(b) {
          }
          operator result_type() {
            return Fn()(a,b);
          }
      };
    
      enum CmpSymmetry {
        CS_Symmetric = false,
        CS_Asymmetric = true
      };
    
      template <typename Cmp, CmpSymmetry asymmetric> class LazyCmp {
        public:
          typedef typename Cmp::first_argument_type first_argument_type;
          typedef typename Cmp::first_argument_type second_argument_type;
          typedef LazyBool result_type;
          LazyBool operator()(first_argument_type const& a1, second_argument_type const& a2) const {
            if (Cmp(a1,a2))
              return LB_true;
            if (asymmetric && Cmp(a2,a1))
              return LB_false;
            return LB_undefined;
          }
      };
    
      template <typename A, typename B> struct MultiLess {
        typedef
          Operator<
            BinaryFunctorMonad<
              LazyCmp<
                BinaryFunctorMonad<std::less<A> >,
                CS_Asymmetric>
            >, B>
          Type;
      };
    
      template <typename A, typename B> struct MultiEqual {
        typedef
          Operator<
            BinaryFunctorMonad<
              LazyCmp<
                BinaryFunctorMonad<std::equal_to<A> >,
                CS_Symmetric>
            >, B>
          Type;
      };
    
    }
    
    template <typename A, typename B> typename multioperator::MultiLess<A,B>::Type multiLess(A const& a1, A const& a2, B const& b) {
      return typename multioperator::MultiLess<A,B>::Type(a1,a2,b);
    }
    
    template <typename A, typename B> typename multioperator::MultiEqual<A,B>::Type multiEqual(A const& a1, A const& a2, B const& b) {
      return typename multioperator::MultiEqual<A,B>::Type(a1,a2,b);
    }
    // example: multiLess(a1,a2,multiLess(b1,b2,multiLess(c1,c2,false)))
    

    Disclaimer: I know BinaryFunctorMonad is a slight misnomer, I just couldn't come up with something better.

    Version 2: inheritance

    template <typename A, typename Chain> class MultiComparable {
      private:
        A const& a;
        Chain chain;
      public:
        typedef MultiComparable MultiComparableT;
        MultiComparable(A const& a, Chain chain) : a(a), chain(chain) {}
        bool operator<(MultiComparable const& as) {
          if (a != as.a)
            return a < as.a;
          return chain < as.chain;
        }
        bool operator==(MultiComparable const& as) {
          if (a != as.a)
            return false;
          return chain == as.chain;
        }
    };
    
    template <typename A, typename Chain> MultiComparable<A,Chain> multiComparable(A const& a, Chain chain) {
      return MultiComparable<A,Chain>(a,chain);
    }
    
    //example:
    struct X : MultiComparable<int,MultiComparable<float,bool> > {
      int i;
      float f;
      X() : MultiComparableT(i,multiComparable(f,false)) {}
    }