| 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

