Overloading Arithmetic Operators for Complex Numbers
Comparison operators such as == and != are only meaningful for a user-defined class once you define operator== and operator!= for it.
Predicting Equality for Two Complex Numbers
Suppose you have built a class for complex numbers, and you write a == b with two complex values. The language does not know on its own what it means for two complex numbers to be equal. A complex number has two parts, a real part and an imaginary part, and the answer depends on what you decided equality should mean for your type. This article follows a code-first approach: you will see the comparison run, predict what it returns, and then trace the definitions that produced the result.
What do you think happens?
Take a with real part 5 and imaginary part 2, and b with real part 5 and imaginary part -2. Does a == b evaluate to true or false?
Reveal answer
false
The real parts match, but the imaginary parts (2 and -2) do not. The equality definition returns true only when every part matches, so the overall result is false. The trace in the next sections shows exactly where this comes from.
Why a Type Needs Its Own Comparison Definitions
Comparison operators such as == and != are only meaningful for a user-defined class once you define operator== and operator!= for it.
The two operators are written as ordinary functions named operator== and operator!=. The language looks for these names when it sees the == or != symbol between two values of your class. Each one takes two complex values as its parameters and returns a bool, which is either true or false. Without a definition like this, the comparison symbol has no meaning for your type, so the program needs you to supply one.
The same idea applies to arithmetic. The language does not automatically know what it means to add two of your values or subtract one from another. Operator overloading is the mechanism that lets you attach your own behavior to an operator symbol when its operands are your type. An overloaded arithmetic operator is a function whose name is the operator symbol, such as operator+, and whose body defines what the operator means for the user-defined type.
- operator== takes two complex values and returns bool
- operator!= takes two complex values and returns bool
- The bodies are the two definitions described in the source
Tracing How operator== Compares Each Part
The equality operator does not compare whole complex numbers in a single step. It asks two separate questions. First, it checks whether the real part of a equals the real part of b, using a.real() and b.real(). Second, it checks whether the imaginary part of a equals the imaginary part of b, using a.imag() and b.imag(). The two results are joined with the && operator, which means both checks must succeed for the overall result to be true.
Because the comparison uses ==, each part is compared exactly. Two complex numbers whose parts differ, even slightly, are not equal under this definition. The operator returns true only in the case where every part matches.
Equality of two numbers sharing a real part
Let a = 5 + 2i and b = 5 - 2i. Both have real part 5, but their imaginary parts are opposite. Trace the result of a == b using the source definition.
- 1Check the real parts
a.real()is 5 andb.real()is 5, soa.real()==b.real()evaluates to true. - 2Check the imaginary parts
a.imag()is 2 andb.imag()is -2, soa.imag()==b.imag()evaluates to false. - 3Join the checks with &&
true && false evaluates to false, because both checks must succeed.
- 4Return the result
operator== returns false to the caller.
a == b is false, even though the real parts match.
Reusing Equality to Define Inequality
The inequality operator does not repeat the component checks. Its definition is return !(a==b), which first evaluates a==b and then negates the result. The source describes this as implementing inequality in terms of equality, so operator!= depends on operator== and stays consistent with it.
Trace the flow for the pair from the previous section. operator== evaluates to false, so the expression !(false) becomes true, and a != b returns true. The two operators give opposite answers for every pair, because one is literally the negation of the other.
This design has a practical consequence. If the rule for what counts as equal changes, you only need to change operator==. The inequality operator follows automatically, because it is defined in terms of the equality result.
Mistakes in Comparison Operator Overloads
When a comparison returns a result you did not expect, the cause is usually in one of the two definitions. Most divergences come from a single check being wrong, such as using || instead of &&, or checking only one part.
Common mistake Using || instead of && in operator==
What it looks likeWith a = 5 + 2i and b = 5 - 2i, the expression a.real() == b.real() || a.imag() == b.imag() evaluates to true || false, which is true.
Why it fails|| succeeds when either part matches, so a single matching part makes two different numbers compare as equal.
Do this insteadUse && so that both the real and imaginary checks must succeed.
Common mistake Checking only one part
What it looks likeIf operator== compares only a.real() and b.real(), then 5 + 2i and 5 - 2i are reported as equal.
Why it failsThe definition ignores the imaginary part, so differences in that part are invisible to the comparison.
Do this insteadCompare both the real and imaginary parts and join the checks with &&.
Common mistake Writing operator!= with its own separate component checks
Why it failsThe two operators can drift apart. If the equality rule changes, the duplicated inequality checks must be changed separately.
Do this insteadDefine operator!= as !(a == b) so it reuses the equality logic.
To locate a divergence, start from the operator that produced the unexpected value. If it is operator!=, look at operator== first, because the inequality result is only as correct as the equality logic it negates. Then check each part of the equality definition on its own, since a single wrong check is the most common cause.
Pass-by-Value and the Caller's Copy
Arguments passed by value are copied, so the function can modify its parameter freely without changing the caller's value. This is what makes the arithmetic definitions safe to write. Inside operator+, the parameter a is a copy of the caller's first value, so updating a does not touch the caller's variable.
The diagram shows the key asymmetry. The copy named a changes, while the caller's x stays as it was. The copy is the thing being updated, and the caller's x keeps its original value of 2 + 3i. The returned value is what carries the result out of the function, which is why the definition ends with a return statement rather than relying on the parameter to communicate the answer.
Tracing operator+ Step by Step
Start with the addition definition. Its name is operator+, and it accepts two parameters, both of type complex. The body does not compute a fresh sum in a separate variable. Instead, it applies the compound assignment a += b to the first parameter and returns the result. The body relies on a += b being meaningful for the type, which is why the compound assignment matters here.
- Two complex parameters, both passed by value
- The compound assignment updates the first parameter, which is a copy
- The updated copy is returned to the caller
What do you think happens?
Suppose x is 2 + 3i and y is 1 + 4i, and the caller writes z = x + y. What value does z receive, and what value does x hold once the expression has finished?
Reveal answer
z receives 3 + 7i, and x still holds 2 + 3i.
Inside operator+, a is a copy of x (2 + 3i) and b is a copy of y (1 + 4i). a += b makes the copy a equal to 3 + 7i, and that copy is returned. The caller's x was never modified, because the parameter a is a separate value.
Binary and Unary Forms Side by Side
A binary operator takes two operands, so its definition has two parameters. The source gives the same pattern for subtraction, multiplication, and division. Each body performs the matching compound assignment on the first parameter and returns it. Subtraction uses a -= b, multiplication uses a *= b, and division uses a /= b. Only the operator symbol and the compound assignment change; the structure stays the same.
A unary operator takes exactly one operand. Unary minus has a different shape from the binary definitions. Its parameter list has a single complex value, and its body does not use a compound assignment. Instead, it builds and returns a new complex value whose real part is the negation of the original real part and whose imaginary part is the negation of the original imaginary part. If a has real part $a_r$ and imaginary part $a_i$, the result is $(-a_r, -a_i)$.
- One parameter, so the operator takes one operand
- No compound assignment; a new value is constructed
- The result is returned and the operand is not modified in place
| Feature | Binary definition (operator+) | Unary definition (operator-) |
|---|---|---|
| Number of parameters | Two | One |
| Body | Compound assignment on the first parameter | Builds a new value from negated parts |
| What is returned | The updated copy of the first operand | A new complex value |
| Operand modified in place? | Only the copy, never the caller's value | No; a fresh value is constructed |
Counting parameters and describing what each form returns
The unary case shows the contrast clearly. The operand is not modified in place as a compound assignment would do. The function constructs a fresh value and hands it back, whereas in the binary definitions the copy of the first operand is the thing being updated.
Practice Locating a Bug
Suppose a teammate wrote operator== as a.real() == b.real() || a.imag() == b.imag() and reports that 3 + 4i and 3 + 9i are equal. Which definition contains the bug, and what single change corrects the result? Then predict the result of a != b for the same pair after your fix.
Hints
- Ask which operator is responsible for the equality decision, since operator!= only negates it.
- Think about what || returns when only one of its two checks succeeds.
- Once operator== is fixed, operator!= updates with no change to its own body.
Key Takeaways
- A user-defined type needs its own operator== and operator!= because the language cannot infer what equality means for a complex number.
- operator== returns true only when the real parts and the imaginary parts are both equal, joined with &&, and operator!= is defined as !(a==b) so it stays consistent with equality.
- Most comparison divergences come from one wrong check, such as using || instead of && or comparing only one part, so inspect the equality definition first.
- Arithmetic operators take pass-by-value operands, so a binary definition like operator+ updates a copy of the first operand and returns it, leaving the caller's values unchanged.
- A unary operator such as operator- takes one parameter and returns a new value, while a binary operator takes two parameters.