第17章 运算符重载

运算符重载基础

运算符重载的概念与原理

运算符重载是C++的核心特性之一,它允许用户为自定义类型定义运算符的行为,使得自定义类型可以像内置类型一样使用运算符。运算符重载本质上是函数重载的一种特殊形式,通过定义特殊的成员函数或非成员函数来实现。

运算符重载的语法规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 成员函数形式的运算符重载
class Complex {
private:
double real;
double imag;

public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

// 成员函数形式的加法运算符重载
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}

// 成员函数形式的赋值运算符重载
Complex& operator=(const Complex& other) {
if (this != &other) {
real = other.real;
imag = other.imag;
}
return *this;
}
};

// 非成员函数形式的运算符重载
Complex operator-(const Complex& lhs, const Complex& rhs) {
return Complex(lhs.getReal() - rhs.getReal(), lhs.getImag() - rhs.getImag());
}

// 流插入运算符重载(必须是非成员函数)
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.getReal() << " + " << c.getImag() << "i";
return os;
}

可重载与不可重载的运算符

可重载的运算符说明可重载形式
+, -, *, /, %算术运算符成员函数或非成员函数
++, --递增/递减运算符成员函数(推荐)
==, !=, <, >, <=, >=比较运算符通常为非成员函数
=, +=, -=, *=, /=, %=赋值运算符必须为成员函数
<<, >>流运算符必须为非成员函数
&&, `,!`
[]下标运算符必须为成员函数
()函数调用运算符必须为成员函数
->, ->*指针运算符必须为成员函数
new, delete内存管理运算符全局函数或类的静态成员函数

不可重载的运算符

  • . (成员访问运算符)
  • .* (成员指针访问运算符)
  • :: (作用域解析运算符)
  • ?: (条件运算符)
  • sizeof (大小运算符)
  • typeid (类型信息运算符)
  • const_cast, dynamic_cast, reinterpret_cast, static_cast (类型转换运算符)

运算符重载的高级实现

一元运算符重载

一元运算符重载需要特别注意前缀和后缀形式的区别,尤其是递增和递减运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Counter {
private:
int value;

public:
Counter(int v = 0) : value(v) {}

// 前缀递增运算符(++c)
Counter& operator++() {
++value;
return *this;
}

// 后缀递增运算符(c++)
// 注意:使用int参数作为占位符来区分
Counter operator++(int) {
Counter temp = *this;
++value;
return temp;
}

// 前缀递减运算符(--c)
Counter& operator--() {
--value;
return *this;
}

// 后缀递减运算符(c--)
Counter operator--(int) {
Counter temp = *this;
--value;
return temp;
}

// 一元负运算符
Counter operator-() const {
return Counter(-value);
}

// 逻辑非运算符
bool operator!() const {
return value == 0;
}
};

复合赋值运算符重载

复合赋值运算符通常应该返回左侧对象的引用,以保持与内置类型的行为一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Vector {
private:
double x, y, z;

public:
Vector(double x = 0.0, double y = 0.0, double z = 0.0)
: x(x), y(y), z(z) {}

// 复合赋值运算符
Vector& operator+=(const Vector& other) {
x += other.x;
y += other.y;
z += other.z;
return *this;
}

Vector& operator-=(const Vector& other) {
x -= other.x;
y -= other.y;
z -= other.z;
return *this;
}

Vector& operator*=(double scalar) {
x *= scalar;
y *= scalar;
z *= scalar;
return *this;
}

Vector& operator/=(double scalar) {
if (scalar != 0) {
x /= scalar;
y /= scalar;
z /= scalar;
}
return *this;
}
};

// 利用复合赋值运算符实现算术运算符
Vector operator+(Vector lhs, const Vector& rhs) {
lhs += rhs;
return lhs;
}

Vector operator-(Vector lhs, const Vector& rhs) {
lhs -= rhs;
return lhs;
}

Vector operator*(Vector v, double scalar) {
v *= scalar;
return v;
}

Vector operator*(double scalar, Vector v) {
v *= scalar;
return v;
}

Vector operator/(Vector v, double scalar) {
v /= scalar;
return v;
}

下标运算符与函数调用运算符

下标运算符和函数调用运算符是两种特殊的运算符,它们必须作为成员函数实现,并且可以被重载为多个版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Matrix {
private:
std::vector<std::vector<double>> data;
size_t rows, cols;

public:
Matrix(size_t r, size_t c) : rows(r), cols(c), data(r, std::vector<double>(c, 0.0)) {}

// 下标运算符重载
std::vector<double>& operator[](size_t row) {
return data[row];
}

const std::vector<double>& operator[](size_t row) const {
return data[row];
}

// 函数调用运算符重载(用于矩阵元素访问)
double& operator()(size_t row, size_t col) {
return data[row][col];
}

const double& operator()(size_t row, size_t col) const {
return data[row][col];
}
};

// 使用示例
Matrix m(3, 3);
m[0][0] = 1.0; // 使用下标运算符
m(1, 1) = 2.0; // 使用函数调用运算符

类型转换运算符

类型转换运算符允许将自定义类型隐式或显式转换为其他类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Rational {
private:
int numerator;
int denominator;

void reduce() {
int gcd = std::gcd(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
if (denominator < 0) {
numerator = -numerator;
denominator = -denominator;
}
}

public:
Rational(int n = 0, int d = 1) : numerator(n), denominator(d) {
reduce();
}

// 转换为double类型的运算符
operator double() const {
return static_cast<double>(numerator) / denominator;
}

// 转换为bool类型的运算符(显式转换,C++11+)
explicit operator bool() const {
return numerator != 0;
}
};

// 使用示例
Rational r(3, 4);
double d = r; // 隐式转换为double
bool b = static_cast<bool>(r); // 显式转换为bool(因为使用了explicit)

运算符重载的编译期优化

编译器对运算符重载的处理

从编译器视角看,运算符重载的实现和使用涉及以下关键优化:

  1. 内联展开

    • 小型运算符重载函数会被编译器内联,消除函数调用开销
    • 尤其是算术运算符和比较运算符等简单操作
  2. 返回值优化(RVO/NRVO)

    • 编译器会优化返回临时对象的运算符,避免不必要的拷贝
    • 例如 operator+ 返回的临时对象
  3. 常量传播

    • 对于常量表达式中的运算符重载,编译器可能在编译期计算结果
    • 尤其是使用 constexpr 修饰的运算符
  4. 运算符重载的决议

    • 编译器会根据参数类型和数量选择最匹配的运算符重载版本
    • 考虑隐式类型转换和cv限定符

constexpr 运算符

C++11引入的 constexpr 关键字可以应用于运算符重载,使得运算符在编译期即可计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Point {
private:
int x, y;

public:
constexpr Point(int x = 0, int y = 0) : x(x), y(y) {}

constexpr int getX() const { return x; }
constexpr int getY() const { return y; }

// constexpr 加法运算符
constexpr Point operator+(const Point& other) const {
return Point(x + other.x, y + other.y);
}

// constexpr 复合赋值运算符
constexpr Point& operator+=(const Point& other) {
x += other.x;
y += other.y;
return *this;
}
};

// 编译期计算
constexpr Point p1(1, 2);
constexpr Point p2(3, 4);
constexpr Point p3 = p1 + p2; // 编译期计算,结果为 (4, 6)
static_assert(p3.getX() == 4 && p3.getY() == 6, "编译期计算错误");

移动语义与运算符重载

移动语义可以显著提高运算符重载的性能,尤其是对于返回值较大的运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class String {
private:
char* data;
size_t length;
size_t capacity;

// 辅助函数
void allocate(size_t cap) {
data = new char[cap];
capacity = cap;
}

public:
// 构造函数
String(const char* str = "") {
length = std::strlen(str);
capacity = length;
allocate(capacity + 1);
std::memcpy(data, str, length + 1);
}

// 析构函数
~String() {
delete[] data;
}

// 移动构造函数
String(String&& other) noexcept
: data(other.data), length(other.length), capacity(other.capacity) {
other.data = nullptr;
other.length = 0;
other.capacity = 0;
}

// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
capacity = other.capacity;
other.data = nullptr;
other.length = 0;
other.capacity = 0;
}
return *this;
}

// 加法运算符(支持移动语义)
String operator+(const String& other) const {
String result;
result.length = length + other.length;
result.capacity = result.length;
result.allocate(result.capacity + 1);
std::memcpy(result.data, data, length);
std::memcpy(result.data + length, other.data, other.length + 1);
return result; // 返回值优化
}

// 复合赋值运算符
String& operator+=(const String& other) {
size_t newLength = length + other.length;
if (newLength > capacity) {
char* newData = new char[newLength + 1];
std::memcpy(newData, data, length);
delete[] data;
data = newData;
capacity = newLength;
}
std::memcpy(data + length, other.data, other.length + 1);
length = newLength;
return *this;
}
};

// 使用示例
String s1("Hello");
String s2(" World");
String s3 = s1 + s2; // 利用返回值优化,无额外拷贝

运算符重载的性能优化

性能优化策略

  1. 使用引用传递

    • 对于参数,优先使用 const& 传递,避免不必要的拷贝
    • 对于返回值,根据情况选择值返回(利用RVO)或引用返回
  2. 实现复合赋值运算符

    • 复合赋值运算符通常比对应的二元运算符更高效
    • 二元运算符可以基于复合赋值运算符实现,减少代码重复
  3. 利用移动语义

    • 对于返回较大对象的运算符,确保移动语义被正确实现
    • 使用 noexcept 标记不抛出异常的移动操作
  4. 避免不必要的内存分配

    • 对于字符串、容器等类型,预先分配足够的内存
    • 实现小对象优化(SSO)减少内存分配
  5. 使用内联函数

    • 对于简单的运算符重载,使用 inline 关键字或让编译器自动内联
  6. 考虑 constexpr

    • 对于可以在编译期计算的运算符,使用 constexpr 修饰

性能比较:成员函数 vs 非成员函数

运算符类型推荐实现方式理由
赋值运算符成员函数必须为成员函数,且左侧操作数为类实例
下标运算符成员函数必须为成员函数,且需要访问类的内部状态
函数调用运算符成员函数必须为成员函数,且需要访问类的内部状态
流运算符非成员函数需要左侧操作数为流对象,而非类实例
算术运算符非成员函数支持交换律,如 1 + objobj + 1
比较运算符非成员函数支持交换律,且对称性更好

案例:高效字符串类的运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class String {
private:
enum { SSO_CAPACITY = 15 };
union {
char smallBuffer[SSO_CAPACITY + 1];
char* largeBuffer;
} buffer;
size_t length;
bool isSmall;

void copyFrom(const char* str, size_t len) {
if (len <= SSO_CAPACITY) {
isSmall = true;
std::memcpy(buffer.smallBuffer, str, len + 1);
} else {
isSmall = false;
buffer.largeBuffer = new char[len + 1];
std::memcpy(buffer.largeBuffer, str, len + 1);
}
length = len;
}

public:
// 构造函数
String(const char* str = "") {
copyFrom(str, std::strlen(str));
}

// 析构函数
~String() {
if (!isSmall) {
delete[] buffer.largeBuffer;
}
}

// 复制构造函数
String(const String& other) {
copyFrom(other.c_str(), other.length);
}

// 移动构造函数
String(String&& other) noexcept {
if (other.isSmall) {
isSmall = true;
std::memcpy(buffer.smallBuffer, other.buffer.smallBuffer, other.length + 1);
} else {
isSmall = false;
buffer.largeBuffer = other.buffer.largeBuffer;
other.buffer.largeBuffer = nullptr;
}
length = other.length;
other.length = 0;
}

// 赋值运算符(拷贝交换 idiom)
String& operator=(String other) noexcept {
swap(*this, other);
return *this;
}

// 交换函数
friend void swap(String& lhs, String& rhs) noexcept {
using std::swap;
swap(lhs.length, rhs.length);
swap(lhs.isSmall, rhs.isSmall);
if (lhs.isSmall && rhs.isSmall) {
std::swap_ranges(lhs.buffer.smallBuffer, lhs.buffer.smallBuffer + std::max(lhs.length, rhs.length) + 1,
rhs.buffer.smallBuffer);
} else if (!lhs.isSmall && !rhs.isSmall) {
swap(lhs.buffer.largeBuffer, rhs.buffer.largeBuffer);
} else if (lhs.isSmall && !rhs.isSmall) {
char* temp = rhs.buffer.largeBuffer;
std::memcpy(lhs.buffer.smallBuffer, rhs.buffer.smallBuffer, rhs.length + 1);
rhs.buffer.largeBuffer = lhs.buffer.largeBuffer;
lhs.buffer.largeBuffer = temp;
lhs.isSmall = false;
rhs.isSmall = true;
} else {
swap(rhs, lhs);
}
}

// 加法运算符
String operator+(const String& other) const {
String result;
size_t newLength = length + other.length;
if (newLength <= SSO_CAPACITY) {
result.isSmall = true;
std::memcpy(result.buffer.smallBuffer, c_str(), length);
std::memcpy(result.buffer.smallBuffer + length, other.c_str(), other.length + 1);
} else {
result.isSmall = false;
result.buffer.largeBuffer = new char[newLength + 1];
std::memcpy(result.buffer.largeBuffer, c_str(), length);
std::memcpy(result.buffer.largeBuffer + length, other.c_str(), other.length + 1);
}
result.length = newLength;
return result;
}

// 复合赋值运算符
String& operator+=(const String& other) {
size_t newLength = length + other.length;
if (newLength <= SSO_CAPACITY) {
// 仍使用小缓冲区
std::memcpy(isSmall ? buffer.smallBuffer + length : buffer.largeBuffer + length,
other.c_str(), other.length + 1);
} else if (isSmall) {
// 从小缓冲区切换到大缓冲区
char* newBuffer = new char[newLength + 1];
std::memcpy(newBuffer, buffer.smallBuffer, length);
std::memcpy(newBuffer + length, other.c_str(), other.length + 1);
buffer.largeBuffer = newBuffer;
isSmall = false;
} else {
// 已使用大缓冲区,需要扩容
char* newBuffer = new char[newLength + 1];
std::memcpy(newBuffer, buffer.largeBuffer, length);
std::memcpy(newBuffer + length, other.c_str(), other.length + 1);
delete[] buffer.largeBuffer;
buffer.largeBuffer = newBuffer;
}
length = newLength;
return *this;
}

// 下标运算符
char& operator[](size_t index) {
return isSmall ? buffer.smallBuffer[index] : buffer.largeBuffer[index];
}

const char& operator[](size_t index) const {
return isSmall ? buffer.smallBuffer[index] : buffer.largeBuffer[index];
}

// 获取C风格字符串
const char* c_str() const {
return isSmall ? buffer.smallBuffer : buffer.largeBuffer;
}

// 获取长度
size_t size() const {
return length;
}
};

// 非成员运算符重载
bool operator==(const String& lhs, const String& rhs) {
if (lhs.size() != rhs.size()) return false;
return std::strcmp(lhs.c_str(), rhs.c_str()) == 0;
}

bool operator!=(const String& lhs, const String& rhs) {
return !(lhs == rhs);
}

bool operator<(const String& lhs, const String& rhs) {
return std::strcmp(lhs.c_str(), rhs.c_str()) < 0;
}

std::ostream& operator<<(std::ostream& os, const String& str) {
return os << str.c_str();
}

运算符重载的最佳实践

设计原则

  1. 保持语义一致

    • 运算符的行为应该与内置类型的对应运算符一致
    • 例如,+ 应该表示加法,而不是其他操作
  2. 遵循最小惊讶原则

    • 运算符的行为应该符合用户的预期
    • 避免重载运算符以执行与名称不符的操作
  3. 考虑对称性

    • 对于二元运算符,考虑是否需要支持交换律
    • 例如,1 + objobj + 1 应该都能工作
  4. 注意异常安全性

    • 确保运算符重载在异常情况下不会泄露资源
    • 使用RAII和拷贝交换 idiom 提高异常安全性
  5. 避免过度重载

    • 只重载有明确语义的运算符
    • 避免为了语法糖而重载运算符

常见陷阱与解决方案

  1. 忘记返回值

    • 例如,赋值运算符应该返回 *this 的引用
    • 解决方案:确保所有运算符都有正确的返回类型
  2. 忘记处理自赋值

    • 例如,赋值运算符中没有检查 this == &other
    • 解决方案:在赋值运算符中添加自赋值检查
  3. 返回局部对象的引用

    • 例如,operator+ 返回局部变量的引用
    • 解决方案:返回值而不是引用,或确保返回的引用指向有效的对象
  4. 重载逻辑运算符时忘记短路求值

    • 例如,operator&&operator|| 没有实现短路求值
    • 解决方案:意识到逻辑运算符的重载会失去短路求值特性,或使用模板技巧模拟
  5. 过度使用隐式类型转换

    • 例如,定义了多个隐式类型转换运算符
    • 解决方案:对于可能引起歧义的转换,使用 explicit 关键字

现代C++中的运算符重载

  1. C++11 特性

    • explicit 类型转换运算符
    • 移动语义和 noexcept
    • 委托构造函数减少代码重复
  2. C++14 特性

    • 泛型 lambda 表达式简化运算符重载
    • 变量模板用于编译期计算
  3. C++17 特性

    • 结构化绑定简化返回多个值的运算符
    • constexpr if 用于编译期条件分支
  4. C++20 特性

    • 概念(Concepts)用于约束运算符重载的参数类型
    • 协程(Coroutines)可能影响某些运算符的实现

运算符重载的实际应用

应用场景

  1. 数值类型

    • 复数、分数、向量、矩阵等
    • 需要支持算术运算和比较运算
  2. 字符串类

    • 需要支持连接、比较等操作
    • 通常实现 +, +=, ==, !=, < 等运算符
  3. 容器类

    • 自定义容器需要支持下标访问、迭代等
    • 实现 [], ==, != 等运算符
  4. 智能指针

    • 需要支持指针相关操作
    • 实现 ->, * 等运算符
  5. 函数对象和lambda表达式

    • 需要支持函数调用操作
    • 实现 operator() 运算符
  6. 迭代器

    • 需要支持指针-like操作
    • 实现 ++, --, *, -> 等运算符
  7. 自定义字面量

    • C++11+ 支持自定义字面量运算符
    • 例如,实现 ""_km 字面量表示千米

案例:矩阵类的运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// 矩阵类的完整实现
class Matrix {
private:
std::vector<std::vector<double>> data;
size_t rows, cols;

public:
Matrix(size_t r, size_t c) : rows(r), cols(c), data(r, std::vector<double>(c, 0.0)) {}

// 构造函数:从初始化列表创建
Matrix(std::initializer_list<std::initializer_list<double>> init) {
rows = init.size();
if (rows > 0) {
cols = init.begin()->size();
data.resize(rows, std::vector<double>(cols, 0.0));
size_t i = 0;
for (auto& row : init) {
size_t j = 0;
for (auto val : row) {
if (j < cols) {
data[i][j] = val;
}
++j;
}
++i;
}
}
}

// 下标运算符
std::vector<double>& operator[](size_t row) {
return data[row];
}

const std::vector<double>& operator[](size_t row) const {
return data[row];
}

// 函数调用运算符
double& operator()(size_t row, size_t col) {
return data[row][col];
}

const double& operator()(size_t row, size_t col) const {
return data[row][col];
}

// 复合赋值运算符
Matrix& operator+=(const Matrix& other) {
if (rows != other.rows || cols != other.cols) {
throw std::invalid_argument("Matrix dimensions mismatch");
}
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
data[i][j] += other.data[i][j];
}
}
return *this;
}

Matrix& operator-=(const Matrix& other) {
if (rows != other.rows || cols != other.cols) {
throw std::invalid_argument("Matrix dimensions mismatch");
}
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
data[i][j] -= other.data[i][j];
}
}
return *this;
}

Matrix& operator*=(const Matrix& other) {
if (cols != other.rows) {
throw std::invalid_argument("Matrix dimensions mismatch for multiplication");
}
Matrix result(rows, other.cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < other.cols; ++j) {
for (size_t k = 0; k < cols; ++k) {
result(i, j) += data[i][k] * other.data[k][j];
}
}
}
*this = std::move(result);
return *this;
}

Matrix& operator*=(double scalar) {
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
data[i][j] *= scalar;
}
}
return *this;
}

// 获取行数和列数
size_t getRows() const { return rows; }
size_t getCols() const { return cols; }
};

// 非成员运算符重载
Matrix operator+(Matrix lhs, const Matrix& rhs) {
lhs += rhs;
return lhs;
}

Matrix operator-(Matrix lhs, const Matrix& rhs) {
lhs -= rhs;
return lhs;
}

Matrix operator*(Matrix lhs, const Matrix& rhs) {
lhs *= rhs;
return lhs;
}

Matrix operator*(Matrix m, double scalar) {
m *= scalar;
return m;
}

Matrix operator*(double scalar, Matrix m) {
m *= scalar;
return m;
}

std::ostream& operator<<(std::ostream& os, const Matrix& m) {
for (size_t i = 0; i < m.getRows(); ++i) {
os << "[";
for (size_t j = 0; j < m.getCols(); ++j) {
os << m(i, j);
if (j < m.getCols() - 1) os << ", ";
}
os << "]" << std::endl;
}
return os;
}

案例:智能指针的运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 简单智能指针实现
template <typename T>
class SmartPtr {
private:
T* ptr;
size_t* refCount;

void release() {
if (refCount) {
if (--(*refCount) == 0) {
delete ptr;
delete refCount;
}
}
}

public:
explicit SmartPtr(T* p = nullptr) : ptr(p), refCount(p ? new size_t(1) : nullptr) {}

SmartPtr(const SmartPtr& other) : ptr(other.ptr), refCount(other.refCount) {
if (refCount) {
++(*refCount);
}
}

SmartPtr(SmartPtr&& other) noexcept : ptr(other.ptr), refCount(other.refCount) {
other.ptr = nullptr;
other.refCount = nullptr;
}

~SmartPtr() {
release();
}

SmartPtr& operator=(const SmartPtr& other) {
if (this != &other) {
release();
ptr = other.ptr;
refCount = other.refCount;
if (refCount) {
++(*refCount);
}
}
return *this;
}

SmartPtr& operator=(SmartPtr&& other) noexcept {
if (this != &other) {
release();
ptr = other.ptr;
refCount = other.refCount;
other.ptr = nullptr;
other.refCount = nullptr;
}
return *this;
}

// 指针运算符
T& operator*() const {
return *ptr;
}

T* operator->() const {
return ptr;
}

// 布尔转换运算符
explicit operator bool() const {
return ptr != nullptr;
}

// 比较运算符
bool operator==(const SmartPtr& other) const {
return ptr == other.ptr;
}

bool operator!=(const SmartPtr& other) const {
return !(*this == other);
}
};

高级运算符重载技术

模板类的运算符重载

模板类的运算符重载需要特别注意模板参数的推导和特化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
template <typename T>
class Vector {
private:
T* data;
size_t size;
size_t capacity;

public:
Vector() : data(nullptr), size(0), capacity(0) {}

explicit Vector(size_t n, const T& value = T()) : size(n), capacity(n) {
data = new T[capacity];
for (size_t i = 0; i < size; ++i) {
data[i] = value;
}
}

~Vector() {
delete[] data;
}

// 下标运算符
T& operator[](size_t index) {
return data[index];
}

const T& operator[](size_t index) const {
return data[index];
}

// 复合赋值运算符
Vector& operator+=(const Vector& other) {
if (size != other.size) {
throw std::invalid_argument("Vector sizes mismatch");
}
for (size_t i = 0; i < size; ++i) {
data[i] += other.data[i];
}
return *this;
}

// 获取大小
size_t getSize() const { return size; }
};

// 模板函数形式的运算符重载
template <typename T>
Vector<T> operator+(Vector<T> lhs, const Vector<T>& rhs) {
lhs += rhs;
return lhs;
}

template <typename T>
std::ostream& operator<<(std::ostream& os, const Vector<T>& v) {
os << "[";
for (size_t i = 0; i < v.getSize(); ++i) {
os << v[i];
if (i < v.getSize() - 1) os << ", ";
}
os << "]";
return os;
}

完美转发与运算符参数

在运算符重载中,完美转发可以提高性能,特别是对于需要转发参数的运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class String {
// ... 现有实现 ...

public:
// ... 现有方法 ...

// 完美转发的加法运算符
template <typename... Args>
String operator+(Args&&... args) const {
String result = *this;
result.append(std::forward<Args>(args)...);
return result;
}

private:
// 可变参数 append 方法
void append() {}

template <typename T, typename... Args>
void append(T&& arg, Args&&... args) {
append(std::forward<T>(arg));
append(std::forward<Args>(args)...);
}

void append(const String& other) {
*this += other;
}

void append(String&& other) {
// 优化:直接移动 other 的内容
if (isSmall && other.isSmall) {
size_t newLength = length + other.length;
if (newLength <= SSO_CAPACITY) {
std::memcpy(buffer.smallBuffer + length, other.buffer.smallBuffer, other.length + 1);
length = newLength;
other.length = 0;
} else {
// 切换到大缓冲区
*this += std::move(other);
}
} else {
*this += std::move(other);
}
}

void append(const char* str) {
String temp(str);
*this += temp;
}

void append(char c) {
String temp(1, c);
*this += temp;
}
};

运算符重载的性能基准测试

为了评估不同运算符重载实现的性能,可以使用基准测试框架进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <benchmark/benchmark.h>

// 基准测试:字符串连接运算符
static void BM_StringConcat(benchmark::State& state) {
String s1("Hello, ");
String s2("world!");
for (auto _ : state) {
String s3 = s1 + s2;
benchmark::DoNotOptimize(s3);
}
}
BENCHMARK(BM_StringConcat);

// 基准测试:矩阵乘法运算符
static void BM_MatrixMultiply(benchmark::State& state) {
size_t size = state.range(0);
Matrix m1(size, size);
Matrix m2(size, size);

// 初始化矩阵
for (size_t i = 0; i < size; ++i) {
for (size_t j = 0; j < size; ++j) {
m1(i, j) = i + j;
m2(i, j) = i - j;
}
}

for (auto _ : state) {
Matrix m3 = m1 * m2;
benchmark::DoNotOptimize(m3);
}
}
BENCHMARK(BM_MatrixMultiply)->DenseRange(10, 50, 10);

BENCHMARK_MAIN();

总结

运算符重载是C++中一种强大的特性,它允许用户为自定义类型定义运算符的行为,使得自定义类型可以像内置类型一样使用运算符。通过深入理解运算符重载的机制、实现技巧与性能优化策略,开发者可以编写更加高效、可靠、安全的C++代码,充分发挥C++的性能优势。

关键要点

  1. 语义一致性:运算符的行为应该与内置类型的对应运算符一致,遵循最小惊讶原则。

  2. 性能优化

    • 使用引用传递减少拷贝开销
    • 利用返回值优化(RVO/NRVO)
    • 实现移动语义提高性能
    • 考虑小对象优化(SSO)减少内存分配
    • 使用 constexpr 支持编译期计算
  3. 异常安全性

    • 确保运算符重载在异常情况下不会泄露资源
    • 使用RAII和拷贝交换 idiom 提高异常安全性
  4. 设计选择

    • 对于需要支持交换律的运算符,优先使用非成员函数实现
    • 对于修改对象状态的运算符,使用成员函数实现
    • 对于流运算符等特殊情况,使用非成员函数实现
  5. 现代C++特性

    • 利用C++11+的移动语义和 noexcept
    • 使用C++14的泛型lambda表达式简化实现
    • 利用C++17的结构化绑定和 constexpr if
    • 考虑C++20的概念(Concepts)约束参数类型

通过合理使用运算符重载,开发者可以创建更加直观、易用且高效的自定义类型,提升代码的可读性和可维护性,同时保持C++的高性能特性。