views:

707

answers:

4

You can pass a function pointer, function object (or boost lambda) to std::sort to define a strict weak ordering of the elements of the container you want sorted.

However, sometimes (enough that I've hit this several times), you want to be able to chain "primitive" comparisons.

A trivial example would be if you were sorting a collection of objects that represent contact data. Sometimes you will want to sort by

last name, first name, area code
. Other times
first name, last name
- yet other times
age, first name, area code
... etc

Now, you can certainly write an additional function object for each case, but that violates the DRY principle - especially if each comparison is less trivial.

It seems like you should be able to write a hierarchy of comparison functions - the low level ones do the single, primitive, comparisons (e.g. first name < first name), then higher level ones call the lower level ones in succession (probably chaining with && to make use of short circuit evaluation) to generate the composite functions.

The trouble with this approach is that std::sort takes a binary predicate - the predicate can only return a bool. So if you're composing them you can't tell if a "false" indicates equality or greater than. You can make your lower level predicates return an int, with three states - but then you would have to wrap those in higher level predicates before they could be used with std::sort on their own.

In all, these are not insurmountable problems. It just seems harder than it should be - and certainly invites a helper library implementation.

Therefore, does anyone know of any pre-existing library (esp. if it's a std or boost library) that can help here - of have any other thoughts on the matter?

[Update]

As mentioned in some of the comments - I've gone ahead and written my own implementation of a class to manage this. It's fairly minimal, and probably has some issues with it in general. but on that basis, for anyone interested, the class is here:

http://pastebin.com/f52a85e4f

And some helper functions (to avoid the need to specify template args) is here:

http://pastebin.com/fa03d66e

+2  A: 

One conventional way to handle this is to sort in multiple passes and use a stable sort. Notice that std::sort is generally not stable. However, there’s std::stable_sort.

That said, I would write a wrapper around functors that return a tristate (representing less, equals, greater).

Konrad Rudolph
You mean subsequent passes would just sort the equal ranges?That could also work, but seems to be doing extra work (minimal, but may be significant), and still involves some boilerplate.I'd also prefer to avoid relying on stable_sort, although not for any specific reason other than options :-)
Phil Nash
+2  A: 

std::sort is not guaranteed to be stable because stable sorts are usually slower than non-stable ones ... so using a stable sort multiple times looks like a recipe for performance trouble...

And yes it's really a shame that sort ask for a predicate: I see no other way than create a functor accepting a vector of tristate functions ...

siukurnin
Yeah, in fact that's exactly what I have done now. I'm using this class:http://pastebin.com/f52a85e4f... and these helper functions for syntactic sugar:http://pastebin.com/fa03d66e- obviously I can increase the limit of 3 functions - but you get the idea.
Phil Nash
+6  A: 

You could build a little chaining system like so:

struct Type {
  string first, last;
  int age;
};

struct CmpFirst {
  bool operator () (const Type& lhs, const Type& rhs) { return lhs.first < rhs.first; }
};

struct CmpLast {
  bool operator () (const Type& lhs, const Type& rhs) { return lhs.last < rhs.last; }
};

struct CmpAge {
  bool operator () (const Type& lhs, const Type& rhs) { return lhs.age < rhs.age; }
};

template <typename First, typename Second>
struct Chain {
  Chain(const First& f_, const Second& s_): f(f_), s(s_) {}

  bool operator () (const Type& lhs, const Type& rhs) {
    if(f(lhs, rhs))
      return true;
    if(f(rhs, lhs))
      return false;

    return s(lhs, rhs);
  }

  template <typename Next>
  Chain <Chain, Next> chain(const Next& next) const {
     return Chain <Chain, Next> (*this, next);
  }

  First f;
  Second s;
};

struct False { bool operator() (const Type& lhs, const Type& rhs) { return false; } };

template <typename Op>
Chain <False, Op> make_chain(const Op& op) { return Chain <False, Op> (False(), op); }

Then to use it:

vector <Type> v;  // fill this baby up

sort(v.begin(), v.end(), make_chain(CmpLast()).chain(CmpFirst()).chain(CmpAge()));

The last line is a little verbose, but I think it's clear what's intended.

Jesse Beder
The problem with this implementation is that your low level comparison functions return bools. You only want to chain onto the next comparison if the current test compares *equal*.
Phil Nash
See my own stab at this that I posted a link to in my reply to siukurnin, above. I can now do:std::sort( c.begin(), c.end(), MakeCompareChain( f1, f2, f3 ) );
Phil Nash
No, it does work - we just need to do two comparisons (a == b is the same as not(a < b) and not(b < a)).
Jesse Beder
Ah, you are correct - sorry. I really should read the answers more carefully before commenting :-).Voting you up now.
Phil Nash
Take a look at this (ancient) CUJ article: "Using a Typelist to Create a Flexible Compound Sorting Object) http://www.ddj.com/cpp/184401667
xtofl
The advantage of this solution (and the CUJ solution) is that the comparator is known at compile time. The Phil Nash solution will result in slower code, though it has the merit of run-time extensibility.
xtofl
+1  A: 

The chaining solution is verbose. You could also use boost::bind in conjunction with std::logical_and to build your sorting predicate. See the linked article for more information: How the boost bind library can improve your C++ programs

MP24
Hi MP24. Did you see my own example:I'm using this class: http://pastebin.com/f52a85e4f ... and these helper functions for syntactic sugar: http://pastebin.com/fa03d66e.I can now do: std::sort( c.begin(), c.end(), MakeCompareChain( f1, f2, f3 ) );
Phil Nash
n fact, looking again, I'm not using boost::bind in there after all. Potentially I use it in client code - but having a look at what I have done here I haven't needed it so far. I do tend to use boost:bind everywhere in general though ;-)
Phil Nash