Canonical relation operators (==,<,…)

2019-08-13 19:53发布

问题:

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.

回答1:

Yes boost can do it using tuple.

Thus you can do it yourself via templates. But the extra work to do that seems like a waste of time. Just do it the simple way with a function (though I don't like your logic).

#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_comparison.hpp>


struct X
{
    int     a;
    float   b;
};

The standard way:

#if (V == 1)

// The normal way of doing it.
bool operator<(X const& lhs, X const& rhs)
{
    if (lhs.a < rhs.a)                          {return true;}
    if ((lhs.a == rhs.a) && (lhs.b < rhs.b))    {return true;}

    // Of course for small structures (as above) it is easy to compress the above
    // lines into a single statement quickly.
    //
    // For larger structures they tend to break it out
    // until they get it correct then spend ten minutes
    // collapsing it into a single expression.
    return false;
}

The real normal way of doing it after compression

#elif (V == 6)

// The normal way of doing it.
bool operator<(X const& lhs, X const& rhs)
{
    return (
                (lhs.a < rhs.a)
             || ((lhs.a == rhs.a) && (lhs.b < rhs.b))
           );
}

The way I used to like because it was clear.

#elif (V == 2)

// The way I like doing it because I think it is slightly more readable.
// Though I normally use the one above now.
bool operator<(X const& lhs, X const& rhs)
{
    if (lhs.a < rhs.a)      {return true;}
    if (lhs.a > rhs.a)      {return false;}
    // If we get here the A are equal
    if (lhs.b < rhs.b)      {return true;}
    if (lhs.b > rhs.b)      {return false;}
    return false;
}

Long winded tuple version

#elif (V == 3)

// A version that will use tupples to do it.
bool operator<(X const& lhs, X const& rhs)
{
    typedef boost::tuple<int, float>   Comp;
    Comp    l(lhs.a, lhs.b);
    Comp    r(rhs.a, rhs.b);
    return l < r;
}

Short compact tuple version

#elif (V == 4)

// A version that will use tupples but slightly more compact.
bool operator<(X const& lhs, X const& rhs)
{
    return boost::make_tuple(lhs.a, lhs.b) < boost::make_tuple(rhs.a, rhs.b);
}
#endif


回答2:

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)) {}
}