Skip to content
academia.sh

Lesson 07 / 14

Operator Overloading

Measures why operator+ returns a new temporary object, and why operator+= instead returns its own object by reference.

Published Updated

In this lesson

  • Measures that operator+ returns a new temporary object
  • Distinguishes that operator+= modifies its own object and returns it by reference
  • Applies limits when deciding which operators are meaningful for a class
Contents

This is the last lesson in this section of K04. The previous lesson measured that a reference works with the caller’s own object without producing any copy. This lesson measures that an operator sometimes does exactly the opposite — it produces a new object. The measurement kind is temporary: the result of the expression a + b is neither a nor b; it is a new Money object, constructed inside the operator’s body, that ceases to exist at the end of the expression.

In C, the only way to add such values was a function with an explicit name: a function like Money add_up(Money a, Money b) was called, and its result was assigned to a separate variable — writing a + b was not possible, because + was defined only for the built-in numeric types. In C++, overloading operator+ opens up the + syntax for a user-defined type like Money too; the caller writes a + b instead of add_up(a, b). This increases the readability of the syntax, but it also hides what the body does (whether it constructs a new object or modifies an existing one) — this is exactly what this lesson’s measurement uncovers: which object is produced behind the operator’s syntax.

What Operator Overloading Is

Operator overloading is defining what a built-in operator such as +, ==, or << means for a user-defined class. When operator+ is written as a member function, the expression a + b is read by the compiler as a.operator+(b) — the syntax stays +, but what gets called is a function the class itself writes. Overloading does not add a new syntax; it gives meaning to the existing operator syntax for user-defined types.

Overloading cannot change how many arguments an operator takes, or its precedence — + always stays binary, and it continues to be evaluated after *. The only thing that can change is what the operator does with its arguments. This limit is deliberate: someone calling operator+ expects the “takes two values and produces a result” behavior for Money, just as for int; overloading cannot make a syntactic change that breaks this expectation, it can only define what the “result” and “modification” part means.

// money.hpp — a class holding an amount in cents, for operator overloading
#ifndef MONEY_HPP
#define MONEY_HPP

#include <cstdio>

class Money {
public:
    explicit Money(long cents) : cents_(cents) {}

    Money operator+(const Money& other) const {
        return Money(cents_ + other.cents_);
    }

    Money& operator+=(const Money& other) {
        cents_ += other.cents_;
        return *this;
    }

    bool operator==(const Money& other) const {
        return cents_ == other.cents_;
    }

    long cents() const { return cents_; }

private:
    long cents_;
};

inline void use_money(const Money& m) {
    std::printf("cents=%ld\n", m.cents());
}

#endif

operator+: Returns a New Temporary

// plus-measurement.cpp — is the result of a + b a new temporary
#include <cstdio>

#include "money.hpp"

int main() {
    Money a(100);
    Money b(250);

    Money c = a + b;

    std::printf("is c the same object as a %s\n", &c == &a ? "yes" : "no");
    std::printf("is c the same object as b %s\n", &c == &b ? "yes" : "no");
    use_money(a);
    use_money(b);
    use_money(c);
    return 0;
}
is c the same object as a no
is c the same object as b no
cents=100
cents=250
cents=350

CR15. The expression a + b turns into the call a.operator+(b); the body of operator+, return Money(cents_ + other.cents_);, constructs a new Money and returns it by value — it modifies neither a nor b, and both keep their own cents_ value (100 and 250). The returned object is constructed into c; c’s address is neither the same as a’s nor as b’s. operator+ being marked const (operator+(const Money& other) const) guarantees that neither other nor the object it is called on (this) is modified — by definition, an addition operator must produce only the result, not the operands being added.

operator+=: Modifies Its Own Object and Returns It by Reference

// plus-equals-measurement.cpp — does a += b modify a itself
#include <cstdio>

#include "money.hpp"

int main() {
    Money a(100);
    Money b(250);

    Money& result = (a += b);

    std::printf("is result the same object as a %s\n", &result == &a ? "yes" : "no");
    use_money(a);
    return 0;
}
is result the same object as a yes
cents=350

CR16. The body of operator+=, cents_ += other.cents_;, modifies its own cents_ member, then return *this; returns itself by reference — it constructs no new object. When a += b is called, a itself changes (it becomes 350), and the result of (a += b) is again a itself: &result == &a is true. This is the exact opposite of operator+: operator+ produces a new temporary without modifying either operand, while operator+= modifies a single object (the left one) and returns a reference of kind self. The two operators’ return types carry this difference in their signature — operator+ returns by value (Money), operator+= by reference (Money&); reading the signature alone tells in advance which one produces a new object and which one modifies an existing one.

The Limits of Defining a Meaningful Operator

The fact that an operator can be overloaded for a user-defined class does not mean every class must define every operator. A few conventions keep operator definitions consistent:

  • Binary arithmetic operators like +, -, * are usually written as free functions, not members (Money operator+(const Money& a, const Money& b)), because this provides symmetric conversion for expressions whose left side is not Money either (for example, if an int were to be added to a Money in the future). Money defines operator+ as a member for this lesson’s measurement; the free-function form is only mentioned here.
  • + can be written in terms of +=: instead of cents_ + other.cents_, the body of operator+ could also be written as Money result(cents_); result += other; return result; — basing + on += without repeating +=‘s logic prevents the two operators’ behavior from drifting apart. Money writes the two independently to keep this lesson’s measurement simple.
  • == is an equality, not an ordering; Money::operator== compares the two objects’ cents_ values, and an ordering operator like < is not defined in this lesson.
  • << (stream output) cannot be a member function — in the expression std::cout << m, the left side is std::ostream, not Money; that is why operator<< is written as a free function, and if it needs to access Money’s private member, access is granted with a friend declaration (mentioned only by name in lesson 02). This lesson does not define operator<<; it does the same job with an ordinary function named use_money.

The question that must be asked when overloading an operator is this: does this operator’s built-in meaning (+ adds, == compares, << prints) still feel natural for the class? For Money, + meaningfully matches addition and == meaningfully matches equality; but, for example, Money operator*(const Money& a, const Money& b) could be left undefined, because multiplying two amounts has no monetary meaning — this lesson does not write such an operator.

Violating this limit compiles, but it makes the code unreadable. C++ enforces no rule about which meaning an operator must be used with — technically, a subtraction could be written into the body of operator+, and the compiler would not object. But such code separates what everyone who writes a + b expects (“it is adding”) from what actually happens (“it is subtracting”) — this is a design flaw that misleads the class’s user, and its error cannot be caught at compile time. The only thing that limits the power of overloading is not the compiler, but the discipline of staying faithful to the expectation an operator raises in the reader.

Where explicit, the Copy Constructor, and Operators Meet

Money’s constructor is defined as explicit Money(long cents) — the explicit rule from lesson 03 applies here too: an implicit conversion like Money m = 100; does not compile, and Money m(100); or Money m{100}; must be written instead. This interacts directly with operator overloading: the line return Money(cents_ + other.cents_); inside operator+’s body writes an explicit construction; if the constructor were not explicit and the body wrote return cents_ + other.cents_;, the long result could be implicitly converted into a Money — this would open the door to exactly the kind of unwanted implicit conversion chain lesson 03 warned about (for example, a Money being compared, indirectly and by mistake, with a long). operator+’s result is always returned by explicitly constructing a Money; this both follows the explicit rule and keeps what the operator produces explicit in the code. The copy constructor from lesson 05 also comes into play here implicitly: when the line return Money(...) returns a new Money value, the compiler, in most cases, constructs this value directly into the place the caller will receive it (an optimization that skips the value copy), but the conceptual model is still “a new object was constructed and returned” — the copy constructor never being called is an optimization detail, and it does not change the fact that operator+ produces a new object.

Value Semantics: This Course’s Context

M11/K01 showed that a struct is copied field by field on assignment; this is value semantics: when a value is assigned or passed, the two sides become independent of each other. Money applies this principle with C++’s own tools — the copy constructor, copy assignment, and now operator+ returning a new value. operator+=, by contrast, deliberately steps outside this principle: it modifies its own object, because +=‘s built-in meaning is already “update the value on the left” (x += 1 also modifies x, it does not construct a new variable). Which of these two behavior patterns is chosen when writing an operator for a class must stay consistent with the behavior of the operator’s built-in int/long counterpart — Money preserves this consistency: + produces a new value, += modifies the existing one, exactly as for int.

Summary of the Copy, Temporary, and Self Distinction

This topic’s seven lessons measured a single question from different angles: which object does an expression work with? Lessons 03 and 04 measured how an object acquires its own resource (constructor, destructor, RAII); lesson 05 measured how a copy separates from its original (copy); lesson 06 measured how a reference works with the caller’s self without any copying; and this lesson measured when an operator produces a new temporary, and when it modifies its own object and returns itself. The four kinds — self, copy, part, temporary — correspond to the single question to ask when reading a C++ expression: which other object is the object this expression produces or operates on the same as, and which is it different from?

The Bounding Measurement

// chain-measurement.cpp — how many temporaries does a + b + c produce
#include <cstdio>

#include "money.hpp"

int main() {
    Money a(10);
    Money b(20);
    Money c(30);

    Money total = a + b + c;

    use_money(total);
    return 0;
}
cents=60

CR17. The expression a + b + c is evaluated left to right: first a + b runs and produces an unnamed Money with the value cents_=30; then this result becomes the left side of the + c call. Because operator+ is a member function, the second call’s this must point to an object: for this reason, the result of a + b is constructed as one temporary and lives until the full expression (the end of a + b + c;, the semicolon) finishes, then is destroyed. The second operator+ call’s result, with the value cents_=60, is not constructed as a separate temporary: under C++17’s guaranteed copy elision, it is constructed directly in total’s own storage. Two operator+ calls therefore produce one temporary; total is the very result of the second call itself, not a copy of it. Because Money‘s destructor does not contain a printf here, the moment the temporary is constructed and destroyed is not observable in this block; the count is read from the language’s rule, not from this measurement. The answer to the question of “how many temporaries” a chained addition produces depends not so much on how many times the operator is called, but on where each result goes: an intermediate result that becomes the object of the next call is a temporary, while a final result that initializes a variable is that variable itself.

Summary

  • The expression this lesson measures is of kind temporary: operator+ returns a new, unnamed Money without modifying the operands; its return type is by value (Money, not a reference).
  • operator+= modifies its own object and returns itself (*this) by reference; its return type is Money&.
  • Binary arithmetic operators are usually written as free functions; << cannot be a member function because its left side is not the class itself.
  • An operator should be defined only when its built-in meaning is natural for the class.
  • In a chained expression (a + b + c), only the intermediate result (a + b) is constructed as a temporary and lives until the full expression finishes; the final result is constructed directly inside total.
  • This topic’s seven lessons asked the same question: does a C++ expression work with a self, copy, part, or temporary object?

Next Step

This topic measured how an object acquires its own resource, is copied, is passed by reference, and produces new objects through operators. The next topic moves away from this single-object model to the relationship between classes — how a class extends another class’s behavior, and how it is determined at run time which function gets called.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close