C++ · lesson 5 of 24
Beginner
6Intermediate
11- 5 Operators and Operator Overloading in C++
- 8 Classes and Object-Oriented Programming in C++
- 9 Inheritance and Polymorphism in C++
- 11 STL Containers in C++
- 12 STL Algorithms in C++
- 13 Smart Pointers in C++
- 15 Lambdas and Functional Programming in C++
- 18 Error Handling in C++
- 19 File I/O in C++
- 23 C++ Interview Preparation: Top 35 Questions
- 24 Static Members in C++
Advanced
7Operators and Operator Overloading in C++
C++ operators run for real: integer division, short-circuiting, bit flags, overloading for a vector type, and the C++20 spaceship operator's categories.
By SkillByExample · Updated
Built-in Operators
C++ integer division truncates toward zero, and the logical operators short-circuit. Both show up in one run.
#include <iostream>
bool loud(bool v) { std::cout << "[evaluated " << v << "] "; return v; }
int main() {
int a = 17, b = 5;
std::cout << a / b << " " << a % b << "\n"; // integer division truncates
std::cout << -a / b << " " << -a % b << "\n"; // toward zero, remainder keeps sign
std::cout << 17.0 / 5 << "\n"; // one double operand: real division
a += 3; a *= 2; a %= 7; // (17+3)*2 = 40, 40 % 7
std::cout << "a = " << a << "\n";
int n = 5;
std::cout << n++ << " "; // post: yields old value
std::cout << n << " ";
std::cout << ++n << "\n"; // pre: yields new value
std::cout << "false && ...: "; bool r1 = loud(false) && loud(true); std::cout << r1 << "\n";
std::cout << "true || ...: "; bool r2 = loud(true) || loud(false); std::cout << r2 << "\n";
std::cout << "true && ...: "; bool r3 = loud(true) && loud(false); std::cout << r3 << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o arith arith.cpp
$ ./arith
3 2
-3 -2
3.4
a = 5
5 6 7
false && ...: [evaluated 0] 0
true || ...: [evaluated 1] 1
true && ...: [evaluated 1] [evaluated 0] 0
17 / 5 is 3 because both operands are int; making either one a double gives 3.4. For negatives, division truncates toward zero (-3) and the remainder keeps the dividend’s sign (-2), so (a / b) * b + a % b == a always holds. n++ yields the old value, ++n the new one. The loud calls show short-circuiting: after false && and true || the right side never runs, because the result is already decided. That’s what makes guards like p != nullptr && p->ok() safe.
Bitwise Operators
Bitwise operators work on individual bits — the tool for flags and masks.
#include <bitset>
#include <iostream>
int main() {
unsigned int flags = 0b1010;
auto show = [](const char* label, unsigned int v) {
std::cout << label << std::bitset<8>(v) << " = " << v << "\n";
};
show("flags ", flags);
show("flags & 12 ", flags & 0b1100); // AND: keep bits set in both
show("flags | 5 ", flags | 0b0101); // OR: set bits
show("flags ^ 15 ", flags ^ 0b1111); // XOR: toggle bits
show("flags << 1 ", flags << 1); // shift left: x2
show("flags >> 1 ", flags >> 1); // shift right: /2
std::cout << "~flags = " << ~flags << "\n"; // all 32 bits flipped
constexpr unsigned READ = 1u << 0, WRITE = 1u << 1, EXEC = 1u << 2;
unsigned perms = READ | EXEC;
std::cout << std::boolalpha << "can write? " << bool(perms & WRITE)
<< ", can exec? " << bool(perms & EXEC) << "\n";
std::cout << "1 << 2 + 1 = " << (1 << 2 + 1) << "\n"; // + binds tighter than <<
}
$ g++ -std=c++20 -Wall -Wextra -o bits bits.cpp
bits.cpp: In function 'int main()':
bits.cpp:21:45: warning: suggest parentheses around '+' inside '<<' [-Wparentheses]
21 | std::cout << "1 << 2 + 1 = " << (1 << 2 + 1) << "\n"; // + binds tighter than <<
| ~~^~~
$ ./bits
flags 00001010 = 10
flags & 12 00001000 = 8
flags | 5 00001111 = 15
flags ^ 15 00000101 = 5
flags << 1 00010100 = 20
flags >> 1 00000101 = 5
~flags = 4294967285
can write? false, can exec? true
1 << 2 + 1 = 8
std::bitset<8> prints the low eight bits so you can see each operation. ~flags flips all 32 bits of the unsigned int, not just the four you were thinking about, which is why it prints 4294967285. The permission flags idiom — | to combine, & to test — packs several booleans into one integer. The last line is the precedence trap, and GCC flags it: + binds tighter than <<, so 1 << 2 + 1 is 1 << 3 = 8, not (1 << 2) + 1 = 5. Parenthesize shifts.
Operator Overloading
An overloaded operator is a function named operator+, operator== and so on. Compound assignments that modify the object are members; symmetric binary operators are non-members built on them.
#include <cmath>
#include <iostream>
struct Vector2D {
double x = 0, y = 0;
Vector2D& operator+=(const Vector2D& r) { x += r.x; y += r.y; return *this; }
Vector2D& operator*=(double s) { x *= s; y *= s; return *this; }
Vector2D operator-() const { return {-x, -y}; }
double magnitude() const { return std::sqrt(x * x + y * y); }
};
// Binary operators as non-members, built on the compound ones
Vector2D operator+(Vector2D l, const Vector2D& r) { return l += r; }
Vector2D operator*(Vector2D v, double s) { return v *= s; }
Vector2D operator*(double s, Vector2D v) { return v *= s; } // scalar on the left
bool operator==(const Vector2D& l, const Vector2D& r) { return l.x == r.x && l.y == r.y; }
std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
return os << "(" << v.x << ", " << v.y << ")";
}
int main() {
Vector2D a{3, 4}, b{1, 2};
std::cout << a + b << " " << a * 2 << " " << 3 * b << " " << -a << "\n";
std::cout << "|a| = " << a.magnitude() << "\n";
a += b;
std::cout << "a += b -> " << a << ", a == b? " << (a == b)
<< ", a != b? " << (a != b) << "\n"; // != rewritten from == in C++20
}
$ g++ -std=c++20 -Wall -Wextra -o vector2d vector2d.cpp
$ ./vector2d
(4, 6) (6, 8) (3, 6) (-3, -4)
|a| = 5
a += b -> (4, 6), a == b? 0, a != b? 1
operator+ takes its left operand by value, modifies that copy with += and returns it — the arithmetic lives in one place. 3 * b needs the non-member operator*(double, Vector2D); a member function can’t have a double on its left. operator<< returns the stream so calls chain. And a != b works with no operator!= defined: since C++20, the compiler rewrites a != b as !(a == b).
operator() and Multidimensional operator[]
operator() makes an object callable; here it provides bounds-checked element access. C++23 also allows operator[] with several arguments.
#include <iostream>
#include <stdexcept>
#include <vector>
class Matrix {
public:
Matrix(int rows, int cols) : rows_(rows), cols_(cols), data_(rows * cols, 0.0) {}
double& operator()(int r, int c) { // bounds-checked access
if (r < 0 || r >= rows_ || c < 0 || c >= cols_)
throw std::out_of_range("Matrix index out of bounds");
return data_[r * cols_ + c];
}
double operator()(int r, int c) const { // read-only version
return const_cast<Matrix&>(*this)(r, c);
}
double& operator[](int r, int c) { return data_[r * cols_ + c]; } // C++23, unchecked
private:
int rows_, cols_;
std::vector<double> data_;
};
int main() {
Matrix m(2, 2);
m(0, 0) = 1.5;
m[1, 1] = 5.0;
const Matrix& view = m;
std::cout << view(0, 0) << " " << view(1, 1) << "\n";
try {
m(2, 0) = 9;
} catch (const std::out_of_range& e) {
std::cout << "caught: " << e.what() << "\n";
}
}
$ g++ -std=c++23 -Wall -Wextra -o matrix matrix.cpp
$ ./matrix
1.5 5
caught: Matrix index out of bounds
m(0, 0) goes through the checked operator(), and m(2, 0) throws. m[1, 1] uses C++23’s multidimensional subscript — before C++23, m[1, 1] meant the comma operator and called operator[](1). It needs -std=c++23 (GCC 12+, Clang 15+). The const overload of operator() is what lets view(0, 0) compile through a const Matrix&.
The Spaceship Operator <=> (C++20)
Before C++20, full ordering meant six hand-written operators that had to agree. A defaulted <=> generates all of them, comparing members in declaration order.
#include <algorithm>
#include <compare>
#include <iostream>
#include <vector>
struct Version {
int major, minor, patch;
auto operator<=>(const Version&) const = default; // also gives ==
};
std::ostream& operator<<(std::ostream& os, const Version& v) {
return os << v.major << "." << v.minor << "." << v.patch;
}
int main() {
Version v1{1, 2, 3}, v2{1, 10, 0}, v3{1, 2, 3};
std::cout << std::boolalpha;
std::cout << "v1 < v2: " << (v1 < v2) << "\n";
std::cout << "v1 == v3: " << (v1 == v3) << "\n";
std::cout << "v1 >= v2: " << (v1 >= v2) << "\n";
auto cmp = v2 <=> v1;
std::cout << "(v2 <=> v1) > 0: " << (cmp > 0) << "\n";
std::vector<Version> vs = {{2, 0, 0}, {1, 10, 0}, {1, 2, 3}, {1, 9, 9}};
std::ranges::sort(vs); // uses the generated <
for (const auto& v : vs) std::cout << v << " ";
std::cout << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o spaceship spaceship.cpp
$ ./spaceship
v1 < v2: true
v1 == v3: true
v1 >= v2: false
(v2 <=> v1) > 0: true
1.2.3 1.9.9 1.10.0 2.0.0
{1, 2, 3} < {1, 10, 0} is true because minor 2 < 10 — the comparison is numeric per member, so 1.10.0 correctly sorts after 1.9.9, which string comparison would get wrong. v2 <=> v1 returns an ordering value you compare against 0. A defaulted <=> also implicitly defaults ==, which is why v1 == v3 works.
Comparison Categories
The return type of <=> says what “equal” means for the type.
#include <algorithm>
#include <cctype>
#include <cmath>
#include <compare>
#include <iostream>
#include <string>
struct CaseInsensitive {
std::string s;
std::weak_ordering operator<=>(const CaseInsensitive& o) const {
auto lower = [](std::string t) {
std::ranges::transform(t, t.begin(), [](unsigned char c) { return std::tolower(c); });
return t;
};
return lower(s) <=> lower(o.s); // strong_ordering converts to weak
}
};
int main() {
CaseInsensitive a{"ABC"}, b{"abc"};
std::cout << std::boolalpha;
std::cout << "ABC equivalent to abc: " << ((a <=> b) == 0) << "\n";
std::cout << "but the strings equal: " << (a.s == b.s) << "\n";
double nan = std::nan("");
auto r = nan <=> 1.0; // double <=> double is partial_ordering
std::cout << "NaN < 1: " << (r < 0) << ", NaN > 1: " << (r > 0)
<< ", NaN == 1: " << (r == 0) << ", unordered: "
<< (r == std::partial_ordering::unordered) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o categories categories.cpp
$ ./categories
ABC equivalent to abc: true
but the strings equal: false
NaN < 1: false, NaN > 1: false, NaN == 1: false, unordered: true
std::weak_ordering says “ABC” and “abc” are equivalent for ordering even though they aren’t identical strings. std::partial_ordering exists for values like NaN: NaN <=> 1.0 is unordered, so <, > and == are all false. std::strong_ordering (what int <=> int returns) promises equivalent means identical.
The category matters to the compiler. Here’s a custom ordering — salary descending, then id — declared as strong_ordering:
#include <compare>
struct Employee {
int id;
double salary;
std::strong_ordering operator<=>(const Employee& o) const {
if (salary != o.salary)
return o.salary <=> salary; // double <=> double: partial_ordering
return id <=> o.id;
}
};
int main() { return Employee{1, 10} < Employee{2, 20}; }
$ g++ -std=c++20 -Wall -Wextra -o employee_bad employee_bad.cpp 2>&1 | grep error
employee_bad.cpp:8:29: error: could not convert '(((double)o.Employee::salary) <=> ((double)((const Employee*)this)->Employee::salary))' from 'std::partial_ordering' to 'std::strong_ordering'
double <=> double yields partial_ordering because of NaN, and it can’t be converted to the stronger strong_ordering. Declare the weaker category (or use std::strong_order(a, b), which imposes a total order on doubles):
#include <algorithm>
#include <compare>
#include <iostream>
#include <string>
#include <vector>
struct Employee {
int id;
std::string name;
double salary;
std::partial_ordering operator<=>(const Employee& o) const {
if (auto c = o.salary <=> salary; c != 0) return c; // salary descending
return id <=> o.id; // then id ascending
}
bool operator==(const Employee& o) const { return id == o.id; }
};
int main() {
std::vector<Employee> staff = {
{3, "Cy", 90'000}, {1, "Ann", 120'000}, {2, "Bo", 90'000}, {4, "Di", 75'000}};
std::ranges::sort(staff, std::less{});
for (const auto& e : staff)
std::cout << e.id << " " << e.name << " " << e.salary << "\n";
std::cout << std::boolalpha << "same id, different salary equal? "
<< (Employee{1, "Ann", 1} == Employee{1, "Ann", 2}) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o employee employee.cpp
$ ./employee
1 Ann 120000
2 Bo 90000
3 Cy 90000
4 Di 75000
same id, different salary equal? true
Ann sorts first on salary; Bo and Cy tie at 90000 and fall back to id. The last line shows a design hazard carried over from the ordering: operator== compares only id, so two employees with different salaries are “equal” but not equivalent under <=>. Keep == and <=> consistent unless you have a reason not to.
Practice
-
Make
vector2d.cppsupporta - banda -= busing the same pattern as+.Solution
// member Vector2D& operator-=(const Vector2D& r) { x -= r.x; y -= r.y; return *this; } // non-member Vector2D operator-(Vector2D l, const Vector2D& r) { return l -= r; }a - bfora{3, 4},b{1, 2}prints(2, 2). The unaryoperator-()member and binary non-memberoperator-coexist because they take different numbers of operands. -
Replace the hand-written
operator==invector2d.cppwithbool operator==(const Vector2D&) const = default;inside the struct. Does the output change?Solution
No. A defaulted
==comparesxtheny, exactly what the hand-written one did, and!=is still rewritten from it. -
Add
Flagshelpers tobits.cpp:set(perms, WRITE)andclear(perms, EXEC)as one-line functions.Solution
unsigned set(unsigned p, unsigned f) { return p | f; } unsigned clear(unsigned p, unsigned f) { return p & ~f; }clearANDs with the complement of the flag, keeping every bit except that one:clear(READ | EXEC, EXEC)leavesREAD(1). -
Give
Versionastd::string labelmember declared first and keep<=> = default. How does{"b",1,0,0}compare to{"a",2,0,0}?Solution
{"b",1,0,0}is greater: the defaulted<=>compares members in declaration order, solabeldecides beforemajoris looked at. Member order is part of the comparison’s meaning. -
Fix
employee_bad.cppwhile keeping thestd::strong_orderingreturn type.Solution
std::strong_ordering operator<=>(const Employee& o) const { if (auto c = std::strong_order(o.salary, salary); c != 0) return c; return id <=> o.id; }std::strong_orderon doubles uses the IEEE total order, which places NaNs at the ends instead of leaving them unordered, so it can promisestrong_ordering.