Ch17.1: Operators Are Functions

中文(简体)

1. The Substitution, Stated Precisely

An operator expression is a function call with a different spelling. The compiler rewrites it, then does ordinary overload resolution:

You write The compiler calls
a + boperator+(a, b)
a += ba.operator+=(b)
-aoperator-(a)
a[i]a.operator[](i)
a(b, c)a.operator()(b, c)
a == boperator==(a, b), and in C++20 also a.operator==(b) with the operands reversed if that is the only candidate
delete pp->~T() then operator delete(p) (Ch18.1)

Two consequences follow immediately. First, an operator is looked up exactly like a function: if it is a member, the left operand must be of that class; if it is not a member, argument-dependent lookup finds it in the namespace of its arguments. Second, an operator can be called explicitly by name, which is how you prove to yourself that nothing magical happens.

2. A Complete Example: the Canonical Shape

The shape every arithmetic value type should follow: a compound assignment as the primitive that mutates and returns a reference, and a binary operator defined on top of it that takes its operands by value and returns a new value.


#include <fast_io.h>
#include <cstdint>

namespace money_ns
{

struct money
{
	::std::int_least64_t cents{};

	constexpr money &operator+=(money other) noexcept
	{
		cents += other.cents;
		return *this;
	}
};

[[nodiscard]] inline constexpr money operator+(money a, money b) noexcept
{
	return a += b;
}

}

int main()
{
	try
	{
		using money_ns::money;
		money const a{1250};
		money const b{375};
		::fast_io::io::println("a + b        = ", (a + b).cents);
		::fast_io::io::println("operator+(a,b)= ", operator+(a, b).cents);
		money c{a};
		c += b;
		::fast_io::io::println("c += b       = ", c.cents);
	}
	catch throws (::std::error e)
	{
		::fast_io::io::print("error: ");
		::fast_io::io::println(e);
	}
}

a + b        = 1625
operator+(a,b)= 1625
c += b       = 1625

Everything worth noticing is in those twenty-five lines:

3. What You Cannot Do

The feature is narrower than people expect. You cannot overload an operator unless at least one operand is a class or enumeration type:


int operator+(int a, int b) { return a * b; }

error: ‘int operator+(int, int)’ must have an argument of class or enumerated type

You cannot invent an operator that does not exist, so no **, no <>, no &&&:


s operator**(s a, s b) { return a; }

error: expected initializer before ‘*’ token

You cannot change an operator's arity, its associativity, or its precedence: a + b * c groups as a + (b * c) for your type too, and a.b.c cannot be redefined as multiply. And ten operator spellings are not overloadable at all — the list is short, closed, and worth knowing by heart, because it is exactly the set of expressions a reader is entitled to understand without knowing your type:

Not overloadable What it answers Why the language keeps it out of reach
:: which entity a name denotes name lookup happens before any object exists, so there is nothing to dispatch on
. which member of an object member access is the one operation the compiler must fully know: layout, offsetof, and the meaning of every other member expression depend on it. (operator-> is the overloadable one-step cousin.)
.* which member through a pointer-to-member same reason; operator->* is overloadable and lives in the library
?: which of two expressions is evaluated it is one of the three forms of lazy evaluation in the language; a function call would evaluate both arms
sizeof, sizeof..., alignof how big the type is, and how it is aligned these are answers about layout, not about values; they must not run code (Ch2.7)
typeid what the dynamic type is a property of the object, not of your interface; a type must not be able to report itself as another type
static_cast, dynamic_cast, const_cast, reinterpret_cast what the value means as another type casts are the escape hatches from the type system; a type that could define reinterpret_cast could make it lie

Two footnotes. # and ## are not C++ operators at all — they belong to the preprocessor, which runs before there are types to overload (Ch3.11). And a C-style cast (T)x has no operator name: it is a request to try const_cast, static_cast, static_cast plus const_cast, and reinterpret_cast in that order, so it cannot be overloaded either — it can only be redirected through a conversion operator.

Everything else is overloadable, including the four that should never be: the address-of &, the short-circuiting && and ||, and the sequencing ,. That asymmetry is the whole subject of Ch17.8: the language protects the operators it considers structural, and trusts you with the ones it considers disposable — and one of the disposable ones, &, is the single most load-bearing operation in systems code.

The list above is what makes operator overloading safe to reason about at all: an expression such as a + b * c means the same shape for every type in the program, so a reader's parser never has to be told about your class. Everything this chapter warns about is inside that shape: the punctuation is fixed, but its meaning is not, and that is where the trouble lives.

4. Member or Non-Member?

Operator Must be a member Must be a non-member Choose freely, prefer
=, [], (), -> yes
<<, >> (as stream I/O) member, unless you are doing iostreams — and you are not (Ch17.8)
+= -= *= /= and friends member: they modify the object
+ - * / == <=> non-member: symmetry, and implicit conversions on both sides
operator T() yes do not write it at all (Ch17.8)

The symmetry argument is the only one that matters in practice: 2 * v calls operator*(int, vec2), which can only exist as a non-member, while v * 2 would work either way. If v * 2 compiles and 2 * v does not, you have leaked an implementation choice into the type's interface.

5. Rules of Thumb