c++ less operator overload, which way to use?
Asked Answered
C

5

32

For example: in a C++ header file, if I defined a struct Record and I would like to use it for possible sorting so that I want to overload the less operator. Here are three ways I noticed in various code. I roughly noticed that: if I'm going to put Record into a std::set, map, priority_queue, … containers, the version 2 works (probably version 3 as well); if I'm going to save Record into a vector<Record> v and then call make_heap(v.begin(), v.end()) etc.. then only version 1 works.

  struct Record
  {
      char c;
      int num;

      //version 1
      bool operator <(const Record& rhs)
      {
         return this->num>rhs.num;
      }

      //version 2
      friend bool operator <(const Record& lhs, const Record& rhs) //friend claim has to be here
      {
         return lhs->num>rhs->num;
      }
  };

in the same header file for example:

      //version 3
      inline bool operator <(const Record& lhs, const Record& rhs)
      {
         return lhs->num>rhs->num;
      }

Basically, I would like to throw the questions here to see if someone could come up with some summary what's the differences among these three methods and what are the right places for each version?

Colloquy answered 4/11, 2011 at 23:25 Comment(3)
could you please post complete example programs for each case that Does Not WorkGalata
ideone.com/issBj all scenarios work on this compilerGarin
sorry, I didn't finish editing the question first. Now,it should be completeColloquy
S
14

They are essentially the same, other than the first being non-const and allowing you to modify itself.

I prefer the second for 2 reasons:

  1. It doesn't have to be a friend.
  2. lhs does not have to be a Record
Stokeontrent answered 4/11, 2011 at 23:31 Comment(3)
I think the 2nd version, if you claim the operator < inside the class definition, you have to claim it as a friend of the classColloquy
@Colloquy It doesn't have to be a friend. It actually is best if it isn't one, as it leads to better encapsulation.Stokeontrent
@Pubby8 If you want to define a free function from inside a class, you'll have to add friend. This has nothing to do with accessing the private members.Testa
D
12

The best way to define the less operator is:

struct Record{
    (...)
    const bool operator < ( const Record &r ) const{
        return ( num < r.num );
    }
};
Decadent answered 5/11, 2011 at 12:46 Comment(2)
This does not allow conversions on the left hand side, so definitely is not the best way.Testa
The path of ipmlicit conversions is dangerous and thy shall not walkMorula
B
2

Welcome to where we have even more options.

  //version 1
  bool operator <(const Record& rhs)
  {
     return this->num>rhs.num;
  }

this one is wrong, it should read:

  //version 1
  bool operator <(const Record& rhs)const
  {
     return this->num>rhs.num;
  }

as you want the left hand side to be const-qualified as well.

  //version 2
  friend bool operator <(const Record& lhs, const Record& rhs) //friend claim has to be here
  {
     return lhs->num>rhs->num;
  }

this one is symmetric. So suppose you have a struct Bar with an operator Record.

Then

Record rhs;
Bar lhs;
assert( lhs < bar );

the above works with a symmetric case, but not with a member function version.

The friend in class version is an operator that can only be found via Koenig lookup (Argument Dependent Lookup). This makes it very useful for when you want a symmetric operator (or one where the type is on the right, like ostream&<<*this) bound to a specific template class instance.

If it is outside of the class, it has to be template function, and a template function does overloading differently than a non-template function does; non-template functions permit conversion.

template<class T>
struct point {
  T x ,y;
  point operator-(point const& rhs)const{
    return {x-rhs.x,y-rhs.y};
  }
  friend point operator+(point const& lhs, point const& rhs) {
    return {lhs.x+rhs.x, lhs.y+rhs.y};
  }
};
template<class T>
point<T> operator*( point<T> const& lhs, point<T> const& rhs ) {
  return {lhs.x*rhs.x, lhs.y*rhs.y};
}

here - is asymmetric, so if we have a type that converts to a point<int> on the left, - won't be found.

+ is symmetric and a "Koenig operator", so it is a non-template operator.

* is symmetric, but is a template operator. If you have something that converts-to-point, it won't find the * overload, because deduction will fail.

  //version 3
  inline bool operator <(const Record& lhs, const Record& rhs)
  {
     return lhs->num>rhs->num;
  }

this is similar to the template above, but here that problem doesn't occur. The difference here is that you can get the address of this function outside of the class, while the "koenig operator<" you wrote can only be found via ADL. Oh, and this isn't a friend.

adds in

 auto operator<=>(const Record&)=default;

where we use the spaceship operator <=> to define ordering automatically.

This will use the ordering of both c and num to produce the required result.

Much like the rule of 5, you should seek to make =default here work correctly. Having state that < ignores is a bad smell, and so is entangling different parts of your state.

Brier answered 26/4, 2021 at 15:48 Comment(0)
F
1
  1. The non-member equivalent of your member function
bool operator <(const Record& rhs); 

is

bool operator <(Record& lhs, const Record& rhs);  // lhs is non-const

Now STL containers treat the items they store as const (at least as long as the comparison operator is concerned). So they call const-const variant of your operator. If they don't find it (in case you provided variant 1 only) - it is a compile error.

  1. If you provide both const-const member and const-const non-member:
    struct Record
    {
      bool operator <(const Record& rhs) const;
    };
    
    bool operator <(Record& lhs, const Record& rhs);

it is yet another compiler error because such definition leads to an ambiguity:

If two matches are found at the highest level where a match is found, the call is rejected as ambiguous. /Stroustrup, C++, section 12.3.1/

  1. Finally, (as noted in the previous answers) there's no need in the friend modifier since by default all fields of the struct are public.

PS make_heap doesn't expect the compared items to be const because it is a more low-level beast and using it you're kind of co-authoring a new heap-based library so it is your responsibility to track const-ness of items.

PPS set treatment of items as const does not protect you from modifying the keys of the items after they are inserted into the container - it will result in a runtime error (segfault) if you try it.

Fissionable answered 26/4, 2021 at 15:26 Comment(0)
G
-2

Favor in-class unless it cannot be in-class because first argument is the wrong type.

Greening answered 4/11, 2011 at 23:29 Comment(4)
Actually one should always favour out-of-class, so that conversions on both left and right types apply (and not just the right one). If possible to make it a non-friend then even better yet, since it decouples the operator from the class implementing it just in terms of the public interface.Bulbil
You don't want conversions on comparison operators unless you're some kind of number!Greening
@Greening that's not always true, if some container (map, sorted-set, ...) needs it you might implement it for "non-number" classes.Odisodium
Really, convert one type of collection to another implicitly?Greening

© 2022 - 2024 — McMap. All rights reserved.