Overloading the increment and decrement operators

The increment (++) and decrement (--) operators come in two forms: prefix (e.g., ++x) and postfix (e.g., x++). Both forms are unary operators that modify their operands, so they're best overloaded as member functions.

Overloading prefix increment and decrement

Prefix operators are straightforward to overload. Let's create a PageNumber class for navigating a document:

#include <iostream>

class PageNumber
{
private:
    int m_page{};
    int m_totalPages{};

public:
    PageNumber(int page, int total)
        : m_page{page}, m_totalPages{total} {}

    PageNumber& operator++();
    PageNumber& operator--();

    friend std::ostream& operator<<(std::ostream& out, const PageNumber& pn);
};

PageNumber& PageNumber::operator++()
{
    if (m_page < m_totalPages)
        ++m_page;

    return *this;
}

PageNumber& PageNumber::operator--()
{
    if (m_page > 1)
        --m_page;

    return *this;
}

std::ostream& operator<<(std::ostream& out, const PageNumber& pn)
{
    out << "Page " << pn.m_page << " of " << pn.m_totalPages;
    return out;
}

int main()
{
    PageNumber current{1, 5};

    std::cout << current << '\n';
    std::cout << ++current << '\n';
    std::cout << ++current << '\n';
    std::cout << --current << '\n';

    return 0;
}

This outputs:

Page 1 of 5
Page 2 of 5
Page 3 of 5
Page 2 of 5

The PageNumber stays within bounds, demonstrating custom behavior for increment and decrement. We return *this so that operators can be chained together.

Overloading postfix increment and decrement

How does the compiler distinguish between prefix and postfix operators when both have the same name? C++ uses a clever solution: postfix operators take a dummy int parameter.

#include <iostream>

class PageNumber
{
private:
    int m_page{};
    int m_totalPages{};

public:
    PageNumber(int page, int total)
        : m_page{page}, m_totalPages{total} {}

    PageNumber& operator++();    // prefix
    PageNumber& operator--();    // prefix
    PageNumber operator++(int);  // postfix
    PageNumber operator--(int);  // postfix

    friend std::ostream& operator<<(std::ostream& out, const PageNumber& pn);
};

PageNumber& PageNumber::operator++()
{
    if (m_page < m_totalPages)
        ++m_page;

    return *this;
}

PageNumber& PageNumber::operator--()
{
    if (m_page > 1)
        --m_page;

    return *this;
}

PageNumber PageNumber::operator++(int)
{
    PageNumber temp{*this};
    ++(*this);
    return temp;
}

PageNumber PageNumber::operator--(int)
{
    PageNumber temp{*this};
    --(*this);
    return temp;
}

std::ostream& operator<<(std::ostream& out, const PageNumber& pn)
{
    out << "Page " << pn.m_page << " of " << pn.m_totalPages;
    return out;
}

int main()
{
    PageNumber current{3, 10};

    std::cout << current << '\n';
    std::cout << ++current << '\n';  // prefix: increment first, then return
    std::cout << current++ << '\n';  // postfix: return first, then increment
    std::cout << current << '\n';
    std::cout << --current << '\n';  // prefix: decrement first, then return
    std::cout << current-- << '\n';  // postfix: return first, then decrement
    std::cout << current << '\n';

    return 0;
}

This outputs:

Page 3 of 10
Page 4 of 10
Page 4 of 10
Page 5 of 10
Page 4 of 10
Page 4 of 10
Page 3 of 10

Let's break down what's happening in the postfix operators:

  1. We create a temporary copy of the current object
  2. We use the prefix operator to modify the current object
  3. We return the temporary copy (the pre-modification state)

The postfix operator returns by value (not by reference) because we can't return a reference to a local variable that will be destroyed.

Why prefix is more efficient

Notice that the postfix operators create a temporary copy of the object. This extra copy makes postfix operators less efficient than prefix operators. For built-in types like int, the compiler optimizes away this difference, but for class types, the overhead can be significant.

Best Practice
Prefer using prefix increment/decrement operators over postfix versions when the return value isn't needed.

A practical example

Here's a Slider class for a UI control:

#include <iostream>

class Slider
{
private:
    int m_value{0};
    int m_min{0};
    int m_max{100};
    int m_step{1};

public:
    Slider(int value = 0, int min = 0, int max = 100, int step = 1)
        : m_value{value}, m_min{min}, m_max{max}, m_step{step} {}

    Slider& operator++()
    {
        m_value += m_step;
        if (m_value > m_max)
            m_value = m_max;
        return *this;
    }

    Slider& operator--()
    {
        m_value -= m_step;
        if (m_value < m_min)
            m_value = m_min;
        return *this;
    }

    Slider operator++(int)
    {
        Slider temp{*this};
        ++(*this);
        return temp;
    }

    Slider operator--(int)
    {
        Slider temp{*this};
        --(*this);
        return temp;
    }

    int getValue() const { return m_value; }

    friend std::ostream& operator<<(std::ostream& out, const Slider& s)
    {
        out << "Slider: " << s.m_value << " [" << s.m_min << "-" << s.m_max << "]";
        return out;
    }
};

int main()
{
    Slider volume{50, 0, 100, 10};

    std::cout << volume << '\n';
    std::cout << ++volume << '\n';  // Increase by step
    std::cout << ++volume << '\n';  // Increase by step again

    Slider before{volume++};  // Save before increasing
    std::cout << "Before: " << before << ", After: " << volume << '\n';

    return 0;
}

This outputs:

Slider: 50 [0-100]
Slider: 60 [0-100]
Slider: 70 [0-100]
Before: Slider: 70 [0-100], After: Slider: 80 [0-100]

Summary

Prefix increment/decrement (++x, --x): Modify the object and return a reference to the modified object. Implemented as member functions returning *this by reference.

Postfix increment/decrement (x++, x--): Return the object's original value, then modify it. Take a dummy int parameter to distinguish from prefix. Create temporary copy, use prefix operator to modify, return the copy by value.

Distinguishing prefix from postfix: C++ uses a dummy int parameter for postfix operators - the parameter itself is never used, it's just a signal to the compiler.

Efficiency difference: Postfix operators are less efficient than prefix because they create a temporary copy. For built-in types, compilers optimize this away, but for class types, the overhead can be significant.

Implementation pattern: Postfix operators should call the prefix operators to avoid code duplication. Create temporary copy, call prefix operator on *this, return the temporary.

Best practice: Prefer prefix operators over postfix when the return value isn't needed, as prefix operators avoid the unnecessary copy.

Overloading increment and decrement operators allows custom classes to work naturally with loops and counter-like operations, but the postfix versions come with performance costs that should be considered.