0

I know there are many places to implement your equality operators in c++. However, I wonder if there is some wisdom as to where best to place them? I can foresee 3 possible different cases where different rules might apply. In each of the three cases are any of the options considered better or worse?

Case 1, equality with primitives and standard types and itself:

//option 1, outside the class with friend 
class foo1;

friend bool operator==(const foo1& lhs, const int& rhs);
friend bool operator==(const int& lhs, const foo1& rhs);
friend bool operator==(const foo1& lhs, const foo1& rhs);
//option 2, inside class with out friend for primitives
class foo1
{
    bool operator==(const int& other) const;
    bool operator==(const foo1& other) const;
};

Case 2, two custom classes:

//option 1, outside the class using friend
class foo2;
class bar2;

friend bool operator==(const foo2& lhs, const bar2& rhs);
friend bool operator==(const bar2& lhs, const foo2& rhs);
//option 2, inside the class using friend
class foo2; //forwards declaration
class bar2; //forwards declaration

class foo2
{
    friend bool operator==(const foo2& lhs, const bar2& rhs) const;
    friend bool operator==(const bar2& lhs, const foo2& rhs) const;
};
class bar2
{
    friend bool operator==(const foo2& lhs, const bar2& rhs) const;
    friend bool operator==(const bar2& lhs, const foo2& rhs) const;
};

Case 3, sub-classes:

//option 1, outside all classes using friend
class foo3
{
    class bar3;
}

friend bool operator==(const foo3::bar3& lhs, const foo3::bar3& rhs);
//option 2, inside the scope of the owning class using friend
class foo3
{
    class bar3;

    friend bool operator==(const foo3::bar3& lhs, const foo3::bar3& rhs);
}
//option 3, inside the class with out friend
class foo3
{
    class bar3
    {
        bool operator==(const foo3::bar3& other) const;
    };
}

Derived classes have already been answered here: What's the right way to overload operator== for a class hierarchy?

Operators in namespaces have already been answered here: Namespaces and Operator Overloading in C++

Edit: fixed compiler errors, spelling errors, and constified member functions

Ebony Ayers
  • 350
  • 1
  • 3
  • 9
  • 2
    In every case where you have made them member functions, make them `const`. [example](https://godbolt.org/z/3hEG7rEaW) – Ted Lyngmo Jan 03 '22 at 08:12
  • It seems you `const`ified a few `friend` functions too. That won't work as they are free functions without `this`. – Ted Lyngmo Jan 03 '22 at 08:27
  • I agree with Ted about `const` qualifying member functions. You might also want to consider options in which non-member `operator==()` are not `friend`s of their operands at all, and also that it is possible to have a member coupled with a non-member (e.g. `bool operator==(int) const` as a member of `foo1` and a non-member `bool operator==(int, const foo1 &)` that calls the member version). More generally, you are looking for general rules, but the only general rule is "it depends". – Peter Jan 03 '22 at 08:28
  • @EbonyAyers, can you please clean up the code snippets? They have some redundant code, e.g. the second snippet has `class bar2;` and `class foo2;` both twice. – Enlico Jan 03 '22 at 08:37
  • As usual, it depends. Had there been an obvious "always best" solution, that would have been the only one available. Now you have to make an engineering decision - what are the requirements for my case? – BoP Jan 03 '22 at 09:44
  • You're using the term "sub-classes" but the code shows **nested classes**. The usual interpretation of "subclass" is as a synonym of "derived class" (cf. superclass/base class). – MSalters Jan 03 '22 at 10:08

1 Answers1

0

I think this topic is certainly well described in more then one place. At the moment, unfortunately, I "just" have with me internet and C++ Templates. (In that book, section 21.2.2 addresses the related topic of how you can define == for a class such that != is automatically defined too.)

As regards your question, at the beginning of section 21.2.1 from the linked book, a slight preference for non-member comparison operators is expressed

Because operator== is meant to be symmetrical with respect to its arguments, it is preferable to declare it as a namespace scope function.

(In that very section, there's also a reference, at page 499, to what I refer to in the following as "hidden friends".)

On the other hand, I would also quote this answer, where Barry claims (and supports it with example snippets) that

There is no rule to prefer a member to a non-member function (or vice versa).

Honestly, given the stature of people advocating one or the other solution, I feel I simply don't have the tools yet (knowledge and experience) to express a preference.

As regards making them friend functions, I'd just like to point out that one option is to declare them only inside the class. This makes them "hidden friends", as described in The Power of Hidden Friends in C++, which has the advantages therein explained. Those advantages apply to functions in general, and not just comparison operators. (This "technique" makes use of ADL.)

Here's one consequence of defining operator== as "hidden friend" of Foo. This is Foo,

struct Foo {
    friend bool operator==(Foo const&, Foo const&) {
        return true;
    }
};

where operator== is an "hidden friend", and this is Bar,

struct Bar {
    operator Foo() { return Foo{}; };
};

which is convertible to Foo.

Expectedly, code like Foo{} == Foo{} works, because an operator== is defined for the pair of arguments Foo-Foo.

Maybe less expectedly, code like Bar{} == Bar{} does not work instead, which protects you from accidental implicit conversions.

Had you also delcared bool operator==(Foo const&, Foo const&); outside the class, then the definition of that operator==, which is still inside the class, would become visible to the outside, and Bar{} == Bar{} would be fine.

Enlico
  • 23,259
  • 6
  • 48
  • 102