6

I have a C++ class for which I need to define a comparator that should consider the result of several potentially expensive methods. I do not want to cache the result of these methods for all the objects in my set, because the criteria with the highest priority are cheaper, and I expect the very expensive ones at the bottom to trigger only in rare cases.

If I had a cmp() function that returned respectively -1, 0, or 1 when the first argument is lesser, equal, or greater to the second, and with shortcut logical operators that preserve integers, I could easily write

int compare(const Class &rhs) const {
    return cmp(expensive_method_a(), rhs.expensive_method_b()) ||
           cmp(expensive_method_b(), rhs.expensive_method_b()) ||
           ...
}

Unfortunately I need to work with the < operator, so it becomes ugly, costly, and error-prone:

bool operator<(const Class &rhs) const {
    return expensive_method_a() < rhs.expensive_method_a() ||
           (expensive_method_a() == rhs.expensive_method_a() &&
            (expensive_method_b() < rhs.expensive_method_b() ||
             (expensive_method_b() == rhs.expensive_method_b() &&
              (...
           ))))
}

Or alternatively, less costly but still pretty ugly:

bool operator<(const Class &rhs) const {
    auto al = expensive_method_a(), ar = rhs.expensive_method_a();
    if (al != ar) return al < ar;
    auto bl = expensive_method_b(), br = rhs.expensive_method_b();
    if (bl != br) return bl < br;

I've read about std::tie at This other question, but if I understand correctly, the tie would evaluate all my methods before starting the comparaison, and I want these arguments to be lazily evaluated.

I thought about defining a preprocessor macro such as this:

#define CUT_COMPARE(a,b) { auto _x = (a); auto _y = (b); if (_x != _y) return (_x < _y); }

That I would use like:

bool operator<(const Class &rhs) const {
    CUT_COMPARE(expensive_method_a(), rhs.expensive_method_a());
    CUT_COMPARE(expensive_method_b(), rhs.expensive_method_b());
    ...
}

hoping that the braces would enclose my _x and _y in a private scope, but alas, clang++ complains of multiple definitions of _x and _y.

Is there a prettier way around this ?

b0fh
  • 1,678
  • 12
  • 28
  • 2
    Why does clang complain? Haven't tested the code, but it does seem the variables are in separate scopes. – Vittorio Romeo May 07 '15 at 15:10
  • 1
    Just as a note, `CUT_COMPARE` should actually be written `#define CUT_COMPARE(a, b) do { .... } while (0)` for the reasons found here: http://stackoverflow.com/questions/1067226/c-multi-line-macro-do-while0-vs-scope-block – Bill Lynch May 07 '15 at 15:41

4 Answers4

4

You can forward all the member functions you want to call to a helper template that walks through them as necessary:

bool operator<(const Class& rhs) const {
    return lazy_compare(*this, rhs, &Class::expensive_1,
                                    &Class::expensive_2,
                                    &Class::expensive_3);
}   

The lazy_compare variadic function will walk through those pointer-to-member functions one at a time, as necessary. The base case is just true:

template <typename T, typename... MFs>
bool lazy_compare(const T&, const T&, MFs...) {
    return true;
}

And the recursive case is to pop off the first pointer-to-member and see if we can stop at that one:

template <typename T, typename R, typename... MFs>
bool lazy_compare(const T& left, const T& right, R (T::*mf)() const, MFs... rest) {
    R vleft = (left.*mf)(), vright = (right.*mf)();
    if (vleft != vright) {
        return vleft < vright;
    }   
    else {
        return lazy_compare(left, right, rest...);
    }   
}
Barry
  • 286,269
  • 29
  • 621
  • 977
  • @LightnessRacesinOrbit I suck at naming things. I am open to suggestion? Went with `lazy_compare` for now – Barry May 07 '15 at 15:44
  • @Barrry: `RecursiveLessThanComparator` is clear but verbose. Dunno :) – Lightness Races in Orbit May 07 '15 at 15:57
  • That is pretty neat; now I realize I've forgotten to add a detail that I thought wasn't significant: for some fields I need to invert the sign of the comparison (which I achieve, with my macro, by swapping the arguments). I can see a couple of ways to achieve that, but if you think there is one particularly clever way to achieve that, I'd like to hear it. – b0fh May 08 '15 at 05:17
  • @b0fh Make a wrapper, so you'd end up calling `lazy_compare(..., &Class::expensive_1, invert(&Class::expensive_2), ...)`. Then add another overload on `lazy_compare` where the next argument in the pack is an `invert_t`, – Barry May 08 '15 at 11:19
2

Here is a lazy comparer object. It holds some arbitrary callable F, and it invokes it when you call cmp(lhs, rhs) on a pair of lazy_comp_f<?> objects, stores the results, and tells you who wins:

template<class F>
struct lazy_comp_f {
  F f;
  template<class F1, class F2>
  friend int cmp( lazy_comp_f<F1>const& lhs, lazy_comp_f<F2>const& rhs) {
    auto l = lhs.f();
    auto r = rhs.f();
    // using cmp_ns::cmp; here
    return cmp(l,r);
  }

  // ctors
  lazy_comp_f(F&& fin):f(std::forward<F>(fin)) {}
  lazy_comp_f(lazy_comp_f&&)=default;
  lazy_comp_f(lazy_comp_f const&)=default;
  template<class O, class=std::enable_if_t<std::is_convertible<O const&,F>>>
  lazy_comp_f(lazy_comp_f<O> const&o):f(o.f){}
  template<class O, class=std::enable_if_t<std::is_convertible<O,F>>>
  lazy_comp_f(lazy_comp_f<O>&&o):f(std::move(o).f){}
};
template<class T>
using lazy_comp_t = lazy_comp_f<std::function<T()>>;

Here is a template factory function helper that does deduction of the F type:

template<class F>
lazy_comp_f<std::decay_t<F>>
lazy_comp(F&& f){ return {std::forward<F>(f)}; }

Here is a lazy tie. It takes a series of functions that are used to produce expensive items:

template<class...Fs, class R=std::tuple< lazy_comp_f<std::decay_t<Fs>>... >>
R lazy_tie( Fs&& fs ) {
  return R( lazy_comp(std::forward<Fs>(fs)...) );
}

Here is our basic cmp. It uses < and produces a reasonably efficient cmp operation. Local ADL lookup can find a better overload for cases where we can do it better:

template<class T, class U>
int cmp( T const& lhs, U const& rhs ) {
  if (lhs < rhs) return -1;
  if (rhs < lhs) return 1;
  return 0;
}

Now an effort to allow cmp of tuples. Two helpers:

namespace details {
  template<class...Ts, class...Us>
  int cmp(
    std::index_sequence<>,
    std::tuple<Ts...> const& lhs,
    std::tuple<Us...> const& rhs
  ) {
    return 0;
  }
  template<size_t I, size_t...Is,class...Ts, class...Us>
  int cmp(
    std::index_sequence<I, Is...>,
    std::tuple<Ts...> const& lhs,
    std::tuple<Us...> const& rhs
  ) {
    // maybe using comp_ns::cmp here?
    int c = cmp( std::get<I>(lhs), std::get<I>(rhs) );
    if (c!=0) return c;
    return cmp(std::index_sequence<Is...>{}, lhs, rhs);
  }
}

and we call the helper, with defence against unmatched number of lhs/rhs args:

template<class...Ts, class...Us>
std::enable_if_t<sizeof...(Ts)==sizeof...(Us), int>
cmp(
  std::tuple<Ts...> const& lhs,
  std::tuple<Us...> const& rhs
) {
  return details::cmp( std::make_index_sequence<sizeof...(Ts)>{}, lhs, rhs );
}

now the problem is to just provide callables!

Inside class do the following:

auto lazy_comparer() const
// std::tuple< lazy_comp_t<A>, lazy_comp_t<B>, lazy_comp_t<C> > in C++11
// where `A`, `B` and `C` are the return types of expensive_method_a etc
{
  return lazy_tie(
    [=]{ return expensive_method_a(); },
    [=]{ return expensive_method_b(); },
    [=]{ return expensive_method_c(); }
    // etc
  );
}
friend int cmp( Class const& lhs, Class const& rhs ) {
  // using namespace cmp_ns::cmp here
  return cmp( lhs.lazy_comparer(), rhs.lazy_comparer() ) < 0;
}
friend bool operator<( Class const& lhs, Class const& rhs ) {
  return cmp(lhs,rhs)<0;
}

and we are done?

Note that this solution works recursively. Anyone who overrides cmp gets an optimal version, anyone who doesn't gets a < based one. If some substructure has its own lazy based cmp, it gets called.

In C++14 this is done with low type erasure overhead. In C++11, some pointless allocations (for type erasure) are done -- they can be made faster with a delegate-like approach (light weight std::functions) or other microoptimizations.

Some C++14 features used. They are easy to implement in C++11 (other than the auto return type, where I provide a workaround).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • This is really impressive and enlightening, and I wish I coud accept two answers. I'm going for the other one because, in the end, it was a lot more straightforward to understand, but many thanks for taking the time to write yours, which I still need to study a bit more to fully understand its behaviour. – b0fh May 08 '15 at 05:36
0

I would stick to a nice compare method, written that way :

int compare(const Class &rhs) const {
    int cr;
    cr = cmp(expensive_method_a(), rhs.expensive_method_a());
    if (cr != 0) return cr;
    cr = cmp(expensive_method_b(), rhs.expensive_method_b());
    if (cr != 0) return cr;
    ...
}

That way it returns with correct sign as soon as one method gives different result and goes down to the end only in case of equality.

And you can use it directly in all comparators :

bool operator<(const Class &rhs) const {
    return compare(rhs) < 0;
}
bool operator<=(const Class &rhs) const {
    return compare(rhs) <= 0;
}
bool operator>(const Class &rhs) const {
    return compare(rhs) > 0;
}
bool operator>=(const Class &rhs) const {
    return compare(rhs) >= 0;
}
bool operator==(const Class &rhs) const {
    return compare(rhs) == 0;
}
bool operator!=(const Class &rhs) const {
    return compare(rhs) != 0;
}
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • All you did was move the body of `operator<` into the body of a new function `compare`. – Lightness Races in Orbit May 07 '15 at 15:24
  • @LightnessRacesinOrbit : yes but as it can be used for all comparators I think it (at least partly) answers the question because the heavy part is in one single place and the repetitive part is trivial. See my edit – Serge Ballesta May 07 '15 at 16:06
-4

You can just implement it like this:

bool operator<(const Class &rhs) const {
    return expensive_method_a() < rhs.expensive_method_a() ||
           expensive_method_b() < rhs.expensive_method_b() ||
           ..
           expensive_method_N() < rhs.expensive_method_N() ||
}

it will return as soon as one of the methods evaluates to true, without evaluating the other ones.

dau_sama
  • 4,247
  • 2
  • 23
  • 30
  • We should only test `expensive_method_b` if the first comparison results in the same value. If `expensive_method_a() > rhs.expensive_method_a()` then we immediately return false. – Bill Lynch May 07 '15 at 15:34
  • 3
    This doesn't have the same semantics as the desired behavior. If `this->a() > rhs.a()` but `this->b() < rhs.b()`, you'd return `true` instead of `false`. – Barry May 07 '15 at 15:51