How to implement C++ (in)equality operators for aggregate structs?
Asked Answered
F

5

14

Sometimes I have structs such as this --

struct aggregate1 {
  std::string name;
  std::vector<ValueT> options;
  size_t foobar;
  // ...
};

-- where (in)equality is simply defined as (in)equality of all members: lhs_name == rhs_name && lhs_options == rhs_options && lhs_foobar == rhs_foobar.

What's the "best" way to implement this? (Best as in: (Runtime-)Efficiency, Maintainability, Readability)

  • operator== in terms of operator!=
  • operator!= in terms of operator==
  • Separate implementations for == and !=
  • As member or as free functions?

Note that this question is only about the (in)equality ops, as comparison (<, <=, ...) doesn't make too much sense for such aggregates.

Fijian answered 14/2, 2011 at 11:30 Comment(1)
Advantage of using free function is that it allows conversion of the first argument like bool b = 2 == Foo{2}; Equality and inequality operator can be defined in terms of each other but I would stick to the convention to define the equality operator.Gerdes
O
3

In C++20, implementing equality and inequality operators can be as simple as declaring operator== as default:

struct S {
  int x;
  // ...

  // As member function
  bool operator==(S const &) const = default;
  
  // As non-member function (hidden friend)
  // friend bool operator==(S const &, S const &) = default;
};

If only operator== is provided, a!=b is interpreted as !(a==b) according to overload resolution, so there is no need for providing an explicit overload for operator!=.

I would argue that defaulting operator== as a hidden friend is preferable because it works with reference-wrapped objects:

S s;
auto rs{std::ref(s)};
rs==rs; // OK for hidden friend; ill-formed if declared as member function

In this example, operator== is not defined for std::reference_wrapper<S>, but argument-dependent lookup (ADL) can select the hidden friend with operands implicitly-converted to S const &. Notice, however, that ::operator==(rs,rs) will only work if operator== is defined as a free function because ADL is not triggered for qualified names.

Offhand answered 19/4, 2022 at 2:11 Comment(0)
F
14

I would do this but maybe move operator== definition to cpp file. Leave operator!= to be inline

Remember to compare member variables that are most likely to differ first so the rest are short-circuited and performance is better.

struct aggregate1 {
  bool operator==(const aggregate1& rhs) const
  {
     return (name == rhs.name)
     && (options == rhs.options)
     && (foobar == rhs.foobar);
  }
  bool operator!=(const aggregate1& rhs) const
  {
    return !operator==(rhs);
  }

  std::string name;
  std::vector<ValueT> options;
  size_t foobar;

  // ...
};
Fi answered 14/2, 2011 at 12:53 Comment(0)
M
9

Member or free function is a matter of taste, and writing separate implementations of == and != seems to me boring, error-prone (you may forget a member in just one of the two operators, and it will take time to notice) without adding anything in terms of efficiency (calling the other operator and applying ! has a negligible cost).

The decision is restricted to "is it better to implement operator== in terms of operator!= or the contrary?

In my opinion, in terms of maintainability/readability/efficiency it's the same; I'd only recommend to do it in the same way everywhere for the sake of consistency. The only case where you'd want to prefer to use one or the other as the "base operator" is when you know that, in the types contained in your structure, that operator is faster than its negation, but I don't know when this could happen.

Mendenhall answered 14/2, 2011 at 11:38 Comment(0)
O
3

In C++20, implementing equality and inequality operators can be as simple as declaring operator== as default:

struct S {
  int x;
  // ...

  // As member function
  bool operator==(S const &) const = default;
  
  // As non-member function (hidden friend)
  // friend bool operator==(S const &, S const &) = default;
};

If only operator== is provided, a!=b is interpreted as !(a==b) according to overload resolution, so there is no need for providing an explicit overload for operator!=.

I would argue that defaulting operator== as a hidden friend is preferable because it works with reference-wrapped objects:

S s;
auto rs{std::ref(s)};
rs==rs; // OK for hidden friend; ill-formed if declared as member function

In this example, operator== is not defined for std::reference_wrapper<S>, but argument-dependent lookup (ADL) can select the hidden friend with operands implicitly-converted to S const &. Notice, however, that ::operator==(rs,rs) will only work if operator== is defined as a free function because ADL is not triggered for qualified names.

Offhand answered 19/4, 2022 at 2:11 Comment(0)
H
1

IMHO, implement as friends and implement the operator== (some STL algorithms will rely on this for example) and the operator!= should be implemented as the negation of the equals operator.

Harman answered 14/2, 2011 at 11:41 Comment(0)
F
-1

(-: Self answer :-)

I would like to highlight one aspect of aggregates WRT efficiency:

The order of evaluation of op== and op!= is irrelevant for (average) performance.

Assuming separate implementations for now and given the two extremes (a-eq) all subelements equal and (b-neq) all subelements inequal, we have these cases:

  • (a-eq) + operator== : Needs to compare all sub elements to return true
  • (a-eq) + operator!= : Needs to compare all sub elements to return false
  • (b-neq) + operator== : Returns false after 1st sub element is determined inequal
  • (b-neq) + operator!= : Returns true after 1st sub element is determined inequal

Since performance on average is the same either way it seems -- at least to me -- more natural to implement op!= in terms of op==, as it feels more natural to me to implement the equality op.

Fijian answered 14/2, 2011 at 14:11 Comment(1)
I don't understand how you arrive at your conclusion. operator== and operator!= will always take the same amount of time to calculate and it doesn't matter which way round you implement them. Remember to keep the one that you are writing in terms of inline so that you don't pay any calling penalty.Fi

© 2022 - 2024 — McMap. All rights reserved.