Implementing comparison operators via 'tuple' and 'tie', a good idea?
Asked Answered
G

4

115

(Note: tuple and tie can be taken from Boost or C++11.)
When writing small structs with only two elements, I sometimes tend to choose a std::pair, as all important stuff is already done for that datatype, like operator< for strict-weak-ordering.
The downsides though are the pretty much useless variable names. Even if I myself created that typedef, I won't remember 2 days later what first and what second exactly was, especially if they are both of the same type. This gets even worse for more than two members, as nesting pairs pretty much sucks.
The other option for that is a tuple, either from Boost or C++11, but that doesn't really look any nicer and clearer. So I go back to writing the structs myself, including any needed comparision operators.
Since especially the operator< can be quite cumbersome, I thought of circumventing this whole mess by just relying on the operations defined for tuple:

Example of operator<, e.g. for strict-weak-ordering:

bool operator<(MyStruct const& lhs, MyStruct const& rhs){
  return std::tie(lhs.one_member, lhs.another, lhs.yet_more) <
         std::tie(rhs.one_member, rhs.another, rhs.yet_more);
}

(tie makes a tuple of T& references from the passed arguments.)


Edit: The suggestion from @DeadMG to privately inherit from tuple isn't a bad one, but it got quite some drawbacks:

  • If the operators are free-standing (possibly friends), I need to inherit publicly
  • With casting, my functions / operators (operator= specifically) can be easily bypassed
  • With the tie solution, I can leave out certain members if they don't matter for the ordering

Are there any drawbacks in this implementation that I need to consider?

Gameto answered 2/6, 2011 at 18:36 Comment(7)
Looks perfectly reasonable to me...Hormone
That's a very clever idea, even if it doesn't pan out. I'm going to have to investigate this.Senega
This looks pretty reasonable. The only pitfall I can think of now is that tie cannot be applied to bit-field members.Radon
Too bad there isn't some sort of introspection so you could do this automagically for any class - i.e. have default compare operators. The tie trick is cool though.Middlemost
I like this idea! If the tie(...) calls are going to be duplicated in various operators (=, ==, <, etc.) you could write a private inline method make_tuple(...) to encapsulate that and then call it from the various other places, as in return lhs.make_tuple() < rhs.make_tuple(); (though the return type from that method could be fun to declare!)Venality
@aldo: C++14 to the rescue! auto tied() const{ return std::tie(the, members, here); }Gameto
It makes it more readable and easier but one concern is strings. Will this result in two operators being called for string ? The string::compare can be used to only do the comparison once and not iterate through strings twice. Worst case with tuple the strings may be iterated through twice to check equality.Sebrinasebum
B
67

This is certainly going to make it easier to write a correct operator than rolling it yourself. I'd say only consider a different approach if profiling shows the comparison operation to be a time-consuming part of your application. Otherwise the ease of maintaining this should outweigh any possible performance concerns.

Brachy answered 2/6, 2011 at 19:7 Comment(4)
I can't imagine a case where tuple<>'s operator< would be any slower than a handwritten one.Hormone
I got this exact same idea once, and did some experimentation. Was positively surprised to see that the compiler inlined and optimized out everything having to do with tuples and references, emitting assembly almost identical to hand-written code.Biochemistry
@JohannesD: I can support that testimony, did the same onceTe
Does this guarantee strict weak ordering? How?Falsify
R
5

I have come accross this same problem and my solution uses c++11 variadic templates. Here comes the code:

The .h part:

/***
 * Generic lexicographical less than comparator written with variadic templates
 * Usage:
 *   pass a list of arguments with the same type pair-wise, for intance
 *   lexiLessthan(3, 4, true, false, "hello", "world");
 */
bool lexiLessthan();

template<typename T, typename... Args>
bool lexiLessthan(const T &first, const T &second, Args... rest)
{
  if (first != second)
  {
    return first < second;
  }
  else
  {
    return lexiLessthan(rest...);
  }
}

And the .cpp for the base case without arguments:

bool lexiLessthan()
{
  return false;
}

Now your example becomes:

return lexiLessthan(
    lhs.one_member, rhs.one_member, 
    lhs.another, rhs.another, 
    lhs.yet_more, rhs.yet_more
);
Reconnoiter answered 10/5, 2013 at 7:37 Comment(1)
I put in similar solution here but not requiring the != operator. #11312948Sebrinasebum
K
3

In my opinion, you're still not addressing the same issue as the std::tuple solves- namely, you have to know both how many and the name of each member variable, you're duplicating it twice in the function. You could opt for private inheritance.

struct somestruct : private std::tuple<...> {
    T& GetSomeVariable() { ... }
    // etc
};

This approach is a little bit more of a mess to begin with, but you're only maintaining the variables and names in one place, instead of in every place for every operator you wish to overload.

Korykorzybski answered 2/6, 2011 at 19:13 Comment(2)
So I'd be using named accessors to the variables like T& one_member(){ return std::get<0>(*this); } etc? But wouldn't that need me to provide such a method for each "member" I have, including overloads for const and non-const version?Gameto
@Gameto I don't see named accessors as requiring any more work than creating actual variables. Either way you'd have to have a separate name for each variable. I suppose there'd be duplication for const/non-const. However, you can template all this work.Recognition
R
1

If you plan to use more than one operator overload, or more methods from tuple, I'd recommend making tuple a member of the class or derive from tuple. Otherwise, what you're doing is a lot more work. When deciding between the two, an important question to answer is: Do you want your class to be a tuple? If not I would recommend containing a tuple and limiting the interface by using delegation.

You could create accessors to "rename" the members of the tuple.

Recognition answered 2/6, 2011 at 19:51 Comment(2)
I read the OP's question as meaning "is implementing my class' operator< using std::tie reasonable?" I don't understand how this answer relates to that question.Hormone
@Hormone There's some comments that I didn't post here. I've compiled everything so it reads better.Recognition

© 2022 - 2024 — McMap. All rights reserved.