C++ Classes and Objects: Overloading the Assignment Operator
Operator Overloading Fundamentals
Custom data types (classes) lack built-in operator behavior like integer or floating-point types. Operator overloading allows us to define standard operator behavior for user-defined classes, making code more readable and concise by letting us use familiar syntax with custom types.
Key Differences: Operator Overloading vs Function Overloading
Many beginners confuse these two features, but they serve distinct purposes:
- Function Overloading: Multiple functions share the same name within the same scope, with different parameter lists (type or count). The compiler selects the correct function based on the arguments passed. Its goal is to let a single function name work with multiple data types.
- Operator Overloading: A specialized form of overloading that redefines the behavior of existing C++ operators for custom classes. Operator overload functions are named
operatorfollowed by the target operator symbol (e.g.,operator+). Unlike regular functions, their sole purpose is to let custom types use standard operator syntax.
Core Characteristics of Operator Overloading
- Implemented via either member functions or global (free) functions
- Return type matches the expected result of the operator operation
- Parameter count depends on the operator: member functions have an implicit
thispointer, so they take one fewer explicit parameter than the arity of the operator - Five operators cannot be overloaded:
*,::,sizeof,?:, and.
Member Function Operator Overload Example
Member function overloads have an implicit this pointer pointing to the left-hand side operand, so they only require explicit parameters for the right-hand side operands:
// Member function operator overload for vector addition
class Vector2 {
public:
int x = 0;
int y = 0;
int operator+(const Vector2& rhs) {
return this->x + rhs.x + this->y + rhs.y;
}
};
int main() {
Vector2 vec1;
Vector2 vec2;
int total = vec1 + vec2;
std::cout << "Combined vector components sum: " << total << std::endl;
return 0;
}
Note: A member function
operator+cannot take two explicit parameters, as the implicitthiscounts as the first operand. For two-operand operators like+, use a global function if you need to handle both operands explicitly.
Global Function Operator Overload Example
Global overloads have no implicit this pointer, so they take all operands as explicit parameters. If you need to acess private class members, declare the overload function as a friend of the class:
class Point {
private:
int posX;
int posY;
public:
Point() : posX(5), posY(15) {}
// Declare as friend to access private members
friend bool operator==(const Point& lhs, const Point& rhs);
};
bool operator==(const Point& lhs, const Point& rhs) {
return (lhs.posX == rhs.posX) && (lhs.posY == rhs.posY);
}
int main() {
Point p1;
Point p2;
std::cout << "Points are equal (1 = true, 0 = false): " << (p1 == p2) << std::endl;
return 0;
}
Assignment Operator Overloading
The assignment operator (=) is used to copy the value of one existing object to another pre-existing object. The default compiler-generated assignment operator performs a shallow (bitwise) copy, which can cause issues for classes with dynamic memory or nested custom type members. Overloading the assignment operator lets you define safe, correct copying behavior for your class.
Key Differences: Asssignment Overload vs Copy Constructor vs General Operator Overload
| Feature | Copy Constructor | Assignment Operator Overload | General Operator Overload |
|---|---|---|---|
| Use Case | Creates a new object as a copy of an existing one | Modifies an existing object to match another's state | Redefines behavior for any standard operator for custom types |
| Timing | Called when a new object is initialized | Caled when an existing object is assigned to | Depends on the operator used |
| Signature | Takes a const reference to the source object | Returns a reference to the current object, takes a const reference to the source | Varies based on the operator |
Basic Assignment Operator Overload Example
class CalendarDate {
private:
int year;
int month;
int day;
public:
// Default constructor
CalendarDate(int y = 2005, int m = 5, int d = 25) : year(y), month(m), day(d) {}
// Copy constructor
CalendarDate(const CalendarDate& other) : year(other.year), month(other.month), day(other.day) {}
// Assignment operator overload
CalendarDate& operator=(const CalendarDate& source) {
// Self-assignment check to avoid accidental data loss
if (this != &source) {
year = source.year;
month = source.month;
day = source.day;
}
return *this;
}
void printDate() {
std::cout << year << "-" << month << "-" << day << std::endl;
}
};
int main() {
CalendarDate date1(2024, 7, 12);
CalendarDate date2(2021, 6, 26);
// Copy constructor: creates date3 as a copy of date2
CalendarDate date3(date2);
// Assignment overload: modifies date4 to match date1
CalendarDate date4;
date4 = date1;
date1.printDate();
date2.printDate();
date3.printDate();
date4.printDate();
return 0;
}
Chained Assignment Support
C++ supports chained assignment (e.g., a = b = c), which we can enable by returning a reference to the current object from the assignment overload:
#include <iostream>
class CalendarDate {
private:
int year;
int month;
int day;
public:
CalendarDate(int y = 2005, int m = 5, int d = 25) : year(y), month(m), day(d) {}
CalendarDate& operator=(const CalendarDate& source) {
if (this != &source) {
year = source.year;
month = source.month;
day = source.day;
}
return *this;
}
void printDate() {
std::cout << year << "-" << month << "-" << day << std::endl;
}
};
int main() {
CalendarDate date1(2023, 7, 10);
CalendarDate date2, date3;
// Chained assignment: evaluates right-to-left
date3 = date2 = date1;
date1.printDate();
date2.printDate();
date3.printDate();
return 0;
}
The chained assignment works because each assignment returns a reference to the left-hand operand, which becomes the right-hand operand for the next assignment operation.
Default Compiler-Generated Assignment Operator
The C++ compiler automatically generates a default assignment operator for any class that does not define its own. This default operator performs a shallow bitwise copy of all member variables:
- Built-in type members: Directly copies the raw bytes of the member variable
- Custom type members: Calls the custom type's own assignment operator overload
Default Assignment for Built-In Members
class SimpleDate {
private:
int year;
int month;
int day;
public:
SimpleDate(int y = 2005, int m =5, int d=25) : year(y), month(m), day(d) {}
void printDate() {
std::cout << year << "-" << month << "-" << day << std::endl;
}
};
int main() {
SimpleDate holiday(2024,7,12);
SimpleDate copyHoliday;
// Uses compiler-generated default assignment overload
copyHoliday = holiday;
copyHoliday.printDate();
return 0;
}
This code will correctly copy all member values without an explicitly defined assignment overload, as the default shallow copy works for built-in types.
Default Assignment for Custom Type Members
When a class contains a custom type member, the compiler-generated assignment overload will call that member's assignment operator:
class IntegerWrapper {
private:
int value;
public:
IntegerWrapper(int v = 10) : value(v) {}
IntegerWrapper& operator=(const IntegerWrapper& other) {
if(this != &other) value = other.value;
return *this;
}
int getValue() const { return value; }
};
class ComplexDate {
private:
int year;
int month;
int day;
IntegerWrapper flag;
public:
ComplexDate(int y=2005, int m=5, int d=25) : year(y), month(m), day(d), flag(0) {}
void printDate() {
std::cout << year << "-" << month << "-" << day << " (flag: " << flag.getValue() << ")" << std::endl;
}
};
int main() {
ComplexDate today(2024,7,12);
ComplexDate tomorrow;
tomorrow = today;
tomorrow.printDate();
return 0;
}
Here the default assignment overload for ComplexDate will automatically call IntegerWrapper::operator= for the flag member.