When overloading binary or relational operators, it is recommended that they be declared hidden friends of the class.
The hidden friend pattern
The hidden friend pattern consists of declaring and defining a function directly as a friend inside the class body. This reduces the function’s
visibility to argument-dependent lookup only. Approximately, such a function is considered only when called on an object of the enclosing class.
struct MyClass {
friend void function(MyClass const& arg) { // This function is a hidden friend of MyClass
...
}
};
Benefits of hidden friends
Using hidden friends provides the following benefits:
- in contrast to the member function, it allows conversion to be applied to both operands
- in contrast to free functions, it is considered only if one of the operands is an object of the given class
This rule raises issues for these overloaded binary operators:
- mathematical operators:
+
, -
, *
, /
, %
, ^
, &
,
|
, <<
, >>
- until C++20, all relational operators:
==
, !=
, <
, >
, <=
,
>=
- since C++20, non-members key relational operators:
==
, <=>
struct MyClass {
MyClass operator+(const MyClass& rhs) // Noncompliant
{ /* ... */ }
};
bool operator==(const MyClass &lhs, const MyClass& rhs) // Noncompliant
{ /* ... */ }
Why are hidden friends preferred over free functions?
The overloaded operators that are declared as hidden friends are found only by argument-dependent lookup (ADL) for a given class. Roughly, this
means that the hidden friend of class C
is considered a candidate only if one of the arguments of the call is an object of type
C
or one derived from it. In contrast, a free function is considered for any class declared in the same namespace.
For illustration, let’s consider the following example which defines operator/
as a free function.
namespace lib {
class Path {
public:
Path(char const*);
Path(std::string_view);
Path& operator/=(Path const&);
};
Path operator/(Path const& lhs, Path const& rhs) {
auto result = lhs;
result /= rhs;
return result;
}
}
The operator/
will be considered as a candidate for any call a / b
when either a
or b
are of a
type declared in the namespace lib
. This not only makes compilation slower but also lists the function as a candidate in case of a
compilation error, making such a message less readable.
Furthermore, when such an operator is visible via normal lookup, it may be invoked when both arguments are of the type that is convertible to
lib::Path
. This may happen for code in the lib
namespace or after using using namespace lib;
. However, such
code will not compile or, even worse, select different overloads when placed in a different namespace.
namespace lib {
void insideLib(std::string_view sv) {
sv / "dir"; // Compiles, converts both arg to lib::Path
}
} // namespace lib
namespace otherNS {
void withoutUsing(std::string_view sv) {
sv / "dir"; // Either does not compile or calls a different operator
}
void withUsing(std::string_view sv) {
using namespace lib;
sv / "dir"; // Compiles, converts both arg to lib::Path
}
} // namespace otherNS
Such conversion for both arguments will not be allowed when the operator is declared as a hidden friend, as none of the operands are of
lib::Path
type:
namespace lib {
class Path {
public:
Path(char const*);
Path(std::string_view);
Path& operator/=(Path const&);
friend Path operator/(Path const& lhs, Path const& rhs) {
auto result = lhs;
result /= rhs;
return result;
}
};
}
Why are hidden friends preferred over member functions?
When the overloaded operator is declared as a class member, it can be invoked only for objects of that class or classes derived from it. These
restrictions also apply when operator syntax (a op b
) is used and disallows implicit conversion for left operands while allowing them for
right operands.
For example, given the object i
of class Integer
that defines operator+
as a member:
class Integer {
public:
Integer(long long int);
Integer& operator+=(Integer const& rhs);
Integer operator+(Integer const& rhs) const { // Noncompliant
Integer res = *this;
res += rhs;
return res;
}
};
The call i + 10
is well-formed and resolves to i.operator+(10)
, which will convert 10
to an
Integer
object using the implicit converting constructor. However, 10 + i
is ill-formed.
If hidden friend functions were used, the expressions i + 10
and 10 + i
, would resolve to operator+(i, 10)
and operator+(10, i)
respectively, allowing conversion to be performed symmetrically on the integer literal.
class Integer {
public:
Integer(long long int);
Integer& operator+=(Integer const& rhs);
friend Integer operator+(Integer const& lhs, Integer const& rhs) { // Compliant
Integer res = lhs;
res += rhs;
return res;
}
};
Why relational operators are treated differently since C++20?
C++20 has introduced a three-way comparison operator <=>
(also known as spaceship) in addition to the mechanism that considers
additional functions when interpreting relational operations:
-
a < b
(also >
, <=
, >=
) is also interpreted as operator<=>(a, b)
< 0
, a.operator<=>(b) < 0
, or 0 < operator<=>(b, a)
, 0 <
b.operator<=>(a)
,
-
a != b
is also intepreted as !operator==(a, b)
, !a.operator==(b)
, or !operator==(b, a)
,
!b.operator==(a)
,
-
a == b
is also intepreted as operator==(a, b)
, a.operator==(b)
, or operator==(b, a)
,
b.operator==(a)
.
The above mechanism makes overloads for !=
, <
, >
, <=
, >=
replacable
with <=>
and ==
(see S6187). As these overloads will usually be removed, we do not suggest replacing them
with hidden friends.
Additionally, such rewrites consider calls of overloads with the order of argument as spelled (a
, b
), and reversed
(b
, a
). This makes the behavior of expression consistent regardless of the order of operands. Given the following
example:
struct MyString {
MyString(char const* cstr);
bool
operator==(MyString const& other) const; // Compliant since C++20: see below
std::strong_ordering
operator<=>(MyString const& other) const; // Compliant: only available since C++20
};
const MySting ms;
The expression ms == "Some string"
and "SomeString" == ms
both compile, and the latter calls operator==
with
the argument reversed. This removes the drawbacks of declaring all combinations of such operators as members, and the issue is not raised for them for
C++20 and later.
Note, that hidden friends are still preferred over free functions:
struct MyString {
MyString(char const* cstr);
};
bool
operator==(MyString const& lhs, MyString const& rhs) // Noncompliant
{ /* ... */ }
std::strong_ordering
operator<=>(MyString const& lhs, MyString const& rhs) // Noncompliant
{ /* ... */ }