Operator Overloading  «Prev  Next»
Lesson 8 Overloading unary operators
Objective Add postfix decrement and increment operator to Clock Class in C++ 23.

Overloading Unary Operators in C++

Unary operators in C++ operate on a single operand, performing operations like incrementing values, negating numbers, or taking addresses. Overloading unary operators enables user-defined types to behave like built-in types, providing natural, intuitive syntax for common operations. This lesson explores unary operator overloading through a practical example: implementing increment and decrement operators for a clock class that manages time in days, hours, minutes, and seconds. Understanding the distinction between prefix and postfix forms, recognizing when to return by value versus reference, and applying modern C++23 features creates operator overloads that are both efficient and semantically correct.

Understanding Unary Operators

C++ provides several unary operators that can be overloaded for user-defined types. These include increment (++), decrement (--), unary minus (-), logical NOT (!), bitwise NOT (~), address-of (&), and dereference (*). Each operator has specific semantics that overloaded versions should respect to maintain code clarity and prevent surprising behavior. The increment and decrement operators exist in both prefix (++x) and postfix (x++) forms, with different semantics: prefix modifies and returns the modified value, while postfix returns the original value before modification.
// Built-in behavior we're emulating
int x = 5;
int y = ++x;  // Prefix: x becomes 6, y is 6
int z = x++;  // Postfix: z is 6, x becomes 7

// User-defined types should behave identically
clock c(100);
clock d = ++c;  // Prefix: c increments, d gets incremented value
clock e = c++;  // Postfix: e gets original value, c increments

The Clock Class Foundation

The clock class stores time as a total number of seconds while maintaining separate fields for days, hours, minutes, and seconds. This dual representation enables efficient calculations using total seconds while providing human-readable access through component fields. The class design demonstrates encapsulation by keeping the internal representation private while exposing meaningful operations through public member functions and overloaded operators.
class clock {
private:
   unsigned long tot_secs;
   unsigned long secs;
   unsigned long mins;
   unsigned long hours;
   unsigned long days;

   // Helper to update component fields from total seconds
   void update_components() {
      secs = tot_secs % 60;
      mins = (tot_secs / 60) % 60;
      hours = (tot_secs / 3600) % 24;
      days = tot_secs / 86400;
   }

public:
   // Constructor converts seconds to time components
   explicit clock(unsigned long i) : tot_secs(i) {
      update_components();
   }

   // Default constructor for zero time
   clock() : clock(0) {}

   // Accessors
   unsigned long get_days() const { return days; }
   unsigned long get_hours() const { return hours; }
   unsigned long get_minutes() const { return mins; }
   unsigned long get_seconds() const { return secs; }
   unsigned long get_total_seconds() const { return tot_secs; }

   // Print formatted time
   void print() const {
      std::cout << days << " days, "
                << hours << ":"
                << std::setfill('0') << std::setw(2) << mins << ":"
                << std::setfill('0') << std::setw(2) << secs;
   }

   // Basic increment by one second
   void tick() {
      ++tot_secs;
      update_components();
   }

   // Basic decrement by one second
   void untick() {
      if (tot_secs > 0) {
         --tot_secs;
         update_components();
      }
   }
};


Prefix Increment Operator (++clock)

The prefix increment operator modifies the object and returns a reference to the modified object. This enables chaining operations like ++++x and ensures the returned value reflects the incremented state. Implementing prefix increment as a member function provides direct access to the object's internal state, making the implementation straightforward and efficient. The operator increments the total seconds, updates component fields, and returns a reference to the current object.
class clock {
public:
   // Prefix increment: ++clock
   clock& operator++() {
      tick();
      return *this;
   }
};

void demonstrate_prefix() {
   clock c(100);
   std::cout << "Before: ";
   c.print();
   std::cout << '\n';

   ++c;  // Increment and use result
   std::cout << "After ++c: ";
   c.print();
   std::cout << '\n';

   // Chaining: each ++ operates on the modified object
   ++++c;
   std::cout << "After ++++c: ";
   c.print();
   std::cout << '\n';
}

Postfix Increment Operator (clock++)

The postfix increment operator creates a copy of the original object, modifies the current object, then returns the copy. This behavior matches built-in types where x++ evaluates to the value before increment. The dummy int parameter distinguishes postfix from prefix—the compiler uses this signature difference to select the correct overload. Because postfix returns a copy rather than a reference, it's less efficient than prefix, making prefix the preferred choice when the return value isn't used.
class clock {
public:
   // Postfix increment: clock++
   // The int parameter is a dummy to distinguish from prefix
   clock operator++(int) {
      clock original = *this;  // Save current state
      tick();                    // Modify current object
      return original;            // Return original state
   }
};

void demonstrate_postfix() {
   clock c(100);
   std::cout << "Before: ";
   c.print();
   std::cout << '\n';

   clock old = c++;  // old gets original, c increments
   std::cout << "After c++, old value: ";
   old.print();
   std::cout << '\n';

   std::cout << "After c++, new c value: ";
   c.print();
   std::cout << '\n';
}


Prefix Decrement Operator (--clock)

The prefix decrement operator mirrors prefix increment but subtracts rather than adds. It modifies the object, returns a reference to the modified object, and enables chaining. For the clock class, decrement reduces total seconds by one, updates component fields, and returns a reference. Including bounds checking prevents underflow, ensuring the clock never represents negative time.
class clock {
public:
   // Prefix decrement: --clock
   clock& operator--() {
      untick();
      return *this;
   }
};

void demonstrate_prefix_decrement() {
   clock c(120);
   std::cout << "Before: ";
   c.print();
   std::cout << '\n';

   --c;
   std::cout << "After --c: ";
   c.print();
   std::cout << '\n';

   // Decrement won't go below zero
   clock zero(1);
   --zero;
   std::cout << "After decrement from 1 second: ";
   zero.print();
   std::cout << '\n';

   --zero;  // Remains at zero
   std::cout << "After another decrement: ";
   zero.print();
   std::cout << '\n';
}

Postfix Decrement Operator (clock--)

The postfix decrement operator completes the set of increment/decrement overloads. Like postfix increment, it uses a dummy int parameter for signature disambiguation, creates a copy of the original state, modifies the current object, and returns the copy. This consistent pattern across all four operators (prefix/postfix increment/decrement) maintains predictable behavior that matches built-in types.
class clock {
public:
   // Postfix decrement: clock--
   clock operator--(int) {
      clock original = *this;  // Save current state
      untick();                  // Modify current object
      return original;            // Return original state
   }
};

void demonstrate_postfix_decrement() {
   clock c(120);
   std::cout << "Before: ";
   c.print();
   std::cout << '\n';

   clock old = c--;  // old gets original, c decrements
   std::cout << "After c--, old value: ";
   old.print();
   std::cout << '\n';

   std::cout << "After c--, new c value: ";
   c.print();
   std::cout << '\n';
}


Complete Clock Class Implementation

The complete clock class demonstrates all four increment and decrement operators working together. This implementation provides a natural interface for time manipulation, enabling code like ++time to advance by one second or time-- to go back one second. The consistency with built-in type behavior makes the class intuitive for users familiar with C++ fundamentals.
#include <iostream>
#include <iomanip>

class clock {
private:
   unsigned long tot_secs;
   unsigned long secs, mins, hours, days;

   void update_components() {
      secs = tot_secs % 60;
      mins = (tot_secs / 60) % 60;
      hours = (tot_secs / 3600) % 24;
      days = tot_secs / 86400;
   }

public:
   explicit clock(unsigned long i = 0) : tot_secs(i) {
      update_components();
   }

   void print() const {
      std::cout << days << "d "
                << std::setfill('0') << std::setw(2) << hours << ":"
                << std::setfill('0') << std::setw(2) << mins << ":"
                << std::setfill('0') << std::setw(2) << secs;
   }

   void tick() {
      ++tot_secs;
      update_components();
   }

   void untick() {
      if (tot_secs > 0) {
         --tot_secs;
         update_components();
      }
   }

   // Prefix increment
   clock& operator++() {
      tick();
      return *this;
   }

   // Postfix increment
   clock operator++(int) {
      clock original = *this;
      tick();
      return original;
   }

   // Prefix decrement
   clock& operator--() {
      untick();
      return *this;
   }

   // Postfix decrement
   clock operator--(int) {
      clock original = *this;
      untick();
      return original;
   }

   unsigned long get_total_seconds() const { return tot_secs; }
};

int main() {
   clock timer(86400);  // 1 day = 86400 seconds

   std::cout << "Initial time: ";
   timer.print();
   std::cout << '\n';

   ++timer;
   std::cout << "After ++timer: ";
   timer.print();
   std::cout << '\n';

   clock saved = timer++;
   std::cout << "After timer++, saved: ";
   saved.print();
   std::cout << '\n';

   std::cout << "After timer++, timer: ";
   timer.print();
   std::cout << '\n';

   --timer;
   std::cout << "After --timer: ";
   timer.print();
   std::cout << '\n';

   return 0;
}


Other Unary Operators

Beyond increment and decrement, C++ supports overloading several other unary operators. The unary minus operator (-) typically returns a negated copy. The logical NOT operator (!) returns a boolean indicating falseness. The bitwise NOT operator (~) returns a value with all bits flipped. Each operator should maintain semantics consistent with its built-in behavior while adapting to the specific needs of the user-defined type.
class clock {
public:
   // Unary minus: returns clock with negated time (if meaningful)
   clock operator-() const {
      // For demonstration: treat as offset from zero
      // In practice, negative time may not be meaningful
      return clock(0);  // Simplified example
   }

   // Logical NOT: returns true if time is zero
   bool operator!() const {
      return tot_secs == 0;
   }

   // Comparison operators for completeness
   bool operator==(const clock& other) const {
      return tot_secs == other.tot_secs;
   }

   bool operator<(const clock& other) const {
      return tot_secs < other.tot_secs;
   }
};

void demonstrate_other_unary() {
   clock zero(0);
   clock nonzero(100);

   std::cout << "zero is " << (!zero ? "empty" : "not empty") << '\n';
   std::cout << "nonzero is " << (!nonzero ? "empty" : "not empty") << '\n';
}

Modern C++17/20/23 Enhancements

Modern C++ provides features that enhance operator overloading. C++20's spaceship operator (<=>) enables defining all comparison operators with a single overload. constexpr allows operator overloads to participate in compile-time computation. noexcept specifications document exception safety guarantees. C++23's explicit object parameters provide alternative syntax for operator overloads, though traditional syntax remains more common for operators.
class clock {
public:
   // C++20: Spaceship operator generates all comparisons
   auto operator<=>(const clock& other) const = default;

   // noexcept specifications for exception safety
   clock& operator++() noexcept {
      tick();
      return *this;
   }

   clock operator++(int) noexcept {
      clock original = *this;
      tick();
      return original;
   }
};

// constexpr for compile-time computation
class simple_counter {
private:
   int value;

public:
   constexpr explicit simple_counter(int v = 0) : value(v) {}

   constexpr simple_counter& operator++() noexcept {
      ++value;
      return *this;
   }

   constexpr int get() const noexcept { return value; }
};

// Compile-time computation with overloaded operators
constexpr int test_compile_time() {
   simple_counter c(5);
   ++c;
   ++c;
   return c.get();
}

static_assert(test_compile_time() == 7);

Best Practices for Unary Operator Overloading

Effective unary operator overloading follows established conventions. Prefix operators return references enabling chaining and avoiding unnecessary copies. Postfix operators return values reflecting pre-modification state. Both forms should maintain semantics matching built-in types—users expect ++x and x++ to behave consistently across all types. Prefer prefix over postfix when the return value isn't needed, as prefix avoids the copy overhead. Mark operators noexcept when they cannot throw exceptions. Use constexpr when operators can execute at compile time. Document any deviations from standard operator semantics clearly.


Common Pitfalls and Solutions

Several common mistakes plague unary operator implementations. Returning values instead of references from prefix operators prevents chaining and creates unnecessary copies. Forgetting the dummy int parameter in postfix operators causes compilation errors or incorrect overload resolution. Modifying and returning the modified object from postfix operators violates expected semantics. Implementing only prefix or only postfix forms creates incomplete interfaces. Testing both forms thoroughly, especially in chains and expressions, catches these errors before they reach production code.
// WRONG: Prefix returning by value
clock operator++() {  // Should return clock&
   tick();
   return *this;  // Creates copy, breaks chaining
}

// WRONG: Postfix without dummy parameter
clock operator++() {  // Conflicts with prefix
   // Won't compile - ambiguous
}

// WRONG: Postfix returning modified value
clock operator++(int) {
   tick();
   return *this;  // Should return original value
}

// CORRECT implementations shown earlier

Testing Unary Operators

Thorough testing verifies operator overloads behave correctly in all contexts. Test prefix and postfix forms separately and in combination. Verify return values and side effects match expectations. Test chaining behavior like ++++x and x----. Ensure operators work correctly in expressions and as statements. Test boundary conditions like decrementing zero or incrementing maximum values. Modern C++20's spaceship operator and concepts simplify testing by reducing the number of operators requiring individual tests.
void test_clock_operators() {
   // Test prefix increment
   clock c1(100);
   clock& ref = ++c1;
   assert(c1.get_total_seconds() == 101);
   assert(&ref == &c1);  // Returns reference to same object

   // Test postfix increment
   clock c2(100);
   clock old = c2++;
   assert(old.get_total_seconds() == 100);  // Returns original
   assert(c2.get_total_seconds() == 101);   // c2 incremented

   // Test prefix decrement
   clock c3(100);
   --c3;
   assert(c3.get_total_seconds() == 99);

   // Test postfix decrement
   clock c4(100);
   clock old2 = c4--;
   assert(old2.get_total_seconds() == 100);
   assert(c4.get_total_seconds() == 99);

   // Test chaining
   clock c5(100);
   ++++c5;
   assert(c5.get_total_seconds() == 102);

   // Test boundary condition
   clock zero(0);
   --zero;
   assert(zero.get_total_seconds() == 0);  // Doesn't go negative

   std::cout << "All tests passed!\n";
}


Conclusion

Unary operator overloading extends C++'s type system by enabling user-defined types to participate in natural, expressive syntax. The clock class demonstrates how increment and decrement operators provide intuitive time manipulation while maintaining semantics consistent with built-in types. Understanding the distinction between prefix and postfix forms, implementing both correctly with appropriate return types, and applying modern C++ features like constexpr and noexcept creates operator overloads that are efficient, safe, and predictable. These principles apply across all unary operators, from arithmetic negation to logical complement, forming the foundation for designing types that feel natural to C++ programmers while leveraging the language's full expressive power.

Overloaded Unary Operators - Exercise

Click the Exercise link below to try your hand at adding overloaded postfix decrement and increment operators to the clock class.
Overloaded Unary Operators - Exercise

SEMrush Software