In C++, the increment (++
) and decrement (--
) operators can be overloaded for user-defined types to provide custom behavior. These operators can be divided into two versions: prefix (++x, --y) and postfix (x++, y--). Overloading these operators allows developers to define specific behavior for incrementing and decrementing objects of their custom classes.
Overloading Prefix Increment and Decrement
When overloading the prefix version of the increment and decrement operators, we simply define member functions that perform the desired operation and return a reference to the modified object. Let's illustrate this with an example:
class Digit {
private:
int m_digit;
public:
Digit(int digit = 0) : m_digit(digit) {}
// Overloading prefix increment operator (++x)
Digit& operator++() {
if (m_digit == 9)
m_digit = 0;
else
++m_digit;
return *this;
}
// Overloading prefix decrement operator (--x)
Digit& operator--() {
if (m_digit == 0)
m_digit = 9;
else
--m_digit;
return *this;
}
};
#include <iostream>
class Digit
{
private:
int m_digit{};
public:
Digit(int digit=0)
: m_digit{digit}
{
}
Digit& operator++();
Digit& operator--();
friend std::ostream& operator<< (std::ostream& out, const Digit& d);
};
Digit& Digit::operator++()
{
// If our number is already at 9, wrap around to 0
if (m_digit == 9)
m_digit = 0;
// otherwise just increment to next number
else
++m_digit;
return *this;
}
Digit& Digit::operator--()
{
// If our number is already at 0, wrap around to 9
if (m_digit == 0)
m_digit = 9;
// otherwise just decrement to next number
else
--m_digit;
return *this;
}
std::ostream& operator<< (std::ostream& out, const Digit& d)
{
out << d.m_digit;
return out;
}
int main()
{
Digit digit { 8 };
std::cout << digit;
std::cout << ++digit;
std::cout << ++digit;
std::cout << --digit;
std::cout << --digit;
return 0;
}
Overloading Postfix Increment and Decrement
Overloading the postfix version of the increment and decrement operators requires a special consideration due to the need to differentiate them from their prefix counterparts. In C++, postfix increment and decrement operators must have an additional dummy integer parameter. Let's see how this is implemented:
class Digit {
private:
int m_digit;
public:
Digit(int digit = 0) : m_digit(digit) {}
// Overloading postfix increment operator (x++)
Digit operator++(int) {
Digit temp(*this);
if (m_digit == 9)
m_digit = 0;
else
++m_digit;
return temp;
}
// Overloading postfix decrement operator (x--)
Digit operator--(int) {
Digit temp(*this);
if (m_digit == 0)
m_digit = 9;
else
--m_digit;
return temp;
}
};
class Digit
{
private:
int m_digit{};
public:
Digit(int digit=0)
: m_digit{digit}
{
}
Digit& operator++(); // prefix has no parameter
Digit& operator--(); // prefix has no parameter
Digit operator++(int); // postfix has an int parameter
Digit operator--(int); // postfix has an int parameter
friend std::ostream& operator<< (std::ostream& out, const Digit& d);
};
// No parameter means this is prefix operator++
Digit& Digit::operator++()
{
// If our number is already at 9, wrap around to 0
if (m_digit == 9)
m_digit = 0;
// otherwise just increment to next number
else
++m_digit;
return *this;
}
// No parameter means this is prefix operator--
Digit& Digit::operator--()
{
// If our number is already at 0, wrap around to 9
if (m_digit == 0)
m_digit = 9;
// otherwise just decrement to next number
else
--m_digit;
return *this;
}
// int parameter means this is postfix operator++
Digit Digit::operator++(int)
{
// Create a temporary variable with our current digit
Digit temp{*this};
// Use prefix operator to increment this digit
++(*this); // apply operator
// return temporary result
return temp; // return saved state
}
// int parameter means this is postfix operator--
Digit Digit::operator--(int)
{
// Create a temporary variable with our current digit
Digit temp{*this};
// Use prefix operator to decrement this digit
--(*this); // apply operator
// return temporary result
return temp; // return saved state
}
std::ostream& operator<< (std::ostream& out, const Digit& d)
{
out << d.m_digit;
return out;
}
int main()
{
Digit digit { 5 };
std::cout << digit;
std::cout << ++digit; // calls Digit::operator++();
std::cout << digit++; // calls Digit::operator++(int);
std::cout << digit;
std::cout << --digit; // calls Digit::operator--();
std::cout << digit--; // calls Digit::operator--(int);
std::cout << digit;
return 0;
}
Explanation:
There are a few interesting things going on here. First, note that we’ve distinguished the prefix from the postfix operators by providing an integer dummy parameter on the postfix version. Second, because the dummy parameter is not used in the function implementation, we have not even given it a name. This tells the compiler to treat this variable as a placeholder, which means it won’t warn us that we declared a variable but never used it.
Third, note that the prefix and postfix operators do the same job -- they both increment or decrement the object. The difference between the two is in the value they return. The overloaded prefix operators return the object after it has been incremented or decremented. Consequently, overloading these is fairly straightforward. We simply increment or decrement our member variables, and then return *this.
The postfix operators, on the other hand, need to return the state of the object before it is incremented or decremented. This leads to a bit of a conundrum -- if we increment or decrement the object, we won’t be able to return the state of the object before it was incremented or decremented. On the other hand, if we return the state of the object before we increment or decrement it, the increment or decrement will never be called.
The typical way this problem is solved is to use a temporary variable that holds the value of the object before it is incremented or decremented. Then the object itself can be incremented or decremented. And finally, the temporary variable is returned to the caller. In this way, the caller receives a copy of the object before it was incremented or decremented, but the object itself is incremented or decremented. Note that this means the return value of the overloaded operator must be a non-reference, because we can’t return a reference to a local variable that will be destroyed when the function exits. Also note that this means the postfix operators are typically less efficient than the prefix operators because of the added overhead of instantiating a temporary variable and returning by value instead of reference.
Finally, note that we’ve written the post-increment and post-decrement in such a way that it calls the pre-increment and pre-decrement to do most of the work. This cuts down on duplicate code, and makes our class easier to modify in the future.
- For prefix increment and decrement, we directly modify the object and return a reference to it.
- For postfix increment and decrement, we create a temporary copy of the object, modify the original object, and then return the temporary copy.
- The additional dummy parameter in postfix operators distinguishes them from prefix operators, allowing the compiler to differentiate between them during overload resolution.
- The C++ language specification has a special case that provides the answer: the compiler looks to see if the overloaded operator has an int parameter. If the overloaded operator has an int parameter, the operator is a postfix overload. If the overloaded operator has no parameter, the operator is a prefix overload.