C++ 中的运算符(operators)设计限制。这段话意在说明:
我们无法控制的东西:C++ 运算符的局限性
作者重复强调:
→ 它们真的不怎么样!
这是在批评 C++ 运算符设计的一些不灵活之处,列举如下:
我们不能控制的内容:
- 运算符名字(name)
- 比如
+
就是+
,你不能定义新的符号运算符(不能有++>
或**
等)。
- 比如
- 优先级(precedence)
- 所有运算符的优先级是固定的,无法改变。
- 例:
*
总比+
优先,你不能定义一个自定义@
运算符并指定它的优先级高于*
。
- 结合性(associativity)
- 例如
a - b - c
是从左到右(左结合),这个行为是固定的。 - 不能定义一个自定义运算符让它变成右结合。
- 例如
- 元数(arity)
- 运算符只能是一元(单目)或二元(双目),不能定义三元、四元的运算符。
- 位置性(fixity)
- 比如前缀(
++x
)、后缀(x++
)或中缀(x + y
)位置是固定的。 - 无法控制自定义运算符是“在哪”出现。
- 比如前缀(
- 求值语义(evaluation semantics)
- 无法控制操作数的求值时机(比如懒求值、惰性计算);
- C++ 保留了语言级别的严格求值顺序,对用户自定义 operator 没法“跳过”。
接下来的问题是:
为什么还要使用运算符?
“The obvious first question: Why should we use operators at all?”
也就是说:既然 C++ 运算符有这么多无法控制的限制,那我们为什么还要使用它们?为什么不全部用函数来表达操作行为?
可能的答案(作者没有写出,但你可以预判如下):
- 语法美观简洁:
a + b
比add(a, b)
更自然; - 操作符重载可以让类表现得像内建类型(数值类、容器类);
- 使用方便,直觉强(如重载
[]
,*
,->
等); - 有些标准库算法(如
std::sort
)默认使用<
、==
运算符; - 可提升表达能力(如定义复数、矩阵等数学结构时)。
总结
这页讲的是 C++ 运算符设计的局限性:
项 | 意义 |
---|---|
name | 名字不能变 |
precedence | 优先级不能改 |
associativity | 结合性不能改 |
arity | 参数数量只能是一元或二元 |
fixity | 不能定义自定义前缀/中缀方式 |
eval semantics | 运算顺序与语义是固定的 |
深入讲解了 C++ 中运算符重载最重要的数学和逻辑语义基础。理解它们对于写出正确、可维护、直观的运算符重载代码至关重要。下面逐条解释:
最重要的:==
与 !=
的对偶性
bool operator==(const T& x, const T& y) noexcept { ... }
bool operator!=(const T& x, const T& y) noexcept { return !(x == y); }
原则:
!=
必须和 ==
表现相反:不能“两个都为假”或“两个都为真”。
破坏这条规则将导致程序逻辑极度混乱。
例如 STL 容器、排序、std::unordered_map
都依赖于等值语义的确定性。
非常重要:运算的结合律 (Associativity)
assert((a + b) + c == a + (b + c));
+
和*
应该是结合的(如果语义上可行)- 违反这一点会导致用户意想不到的结果,尤其在算法、数学类中(如矩阵、向量等)
举例: std::complex
、std::valarray
等都维持结合律。- 违反结合律的运算会破坏通用算法,如
std::accumulate
,std::reduce
等。
排中律:a > b 或 a <= b
assert(a > b || a <= b); // 总是真的?不总是,特别是 float!
这条逻辑上成立,但:
- 对
float
和double
来说不一定,比如:float x = std::numeric_limits<float>::quiet_NaN(); assert(x > 1.0f || x <= 1.0f); // 断言失败
所以:
浮点运算不总满足逻辑三值律(tertium non datur)
最好有:交换律 (Commutativity)
比如:
a + b == b + a
- 对
int
,float
,complex
,这通常成立。 - 但对
std::string
就不成立:std::string a = "Hello", b = "World"; a + b != b + a;
所以不要“指望”所有
+
都是可交换的(尤其是自定义类型)
分配律 (Distributivity)
a * (b + c) == a * b + a * c
- 如果你的类型支持
+
和*
,最好让它们满足分配律(例如矩阵、向量)
可选:封闭性 (Closedness)
a + b 结果是否还是 a 的类型?
比如:
int + int => int
Point + Point => 错(结果应为 Vector)
(合理例外)
如果违反封闭性,必须有语义上的理由。
Affine Space(仿射空间)概念:以 std::chrono
为例
仿射空间是理解时间、空间、位置的数学模型。
// time_point 是“点”,duration 是“向量”
time_point + duration → time_point
time_point - time_point → duration
duration + duration → duration
抽象理解:
概念 | 类比数学结构 |
---|---|
time_point |
空间中的点 |
duration |
向量/位移 |
这种结构常见于: |
- 图形编程(点 vs 向量)
- 物理模拟
- 时间系统
关键在于:不是所有操作都封闭,也不是所有类型都能加减。理解其结构很重要。
C++ chrono 的运算符定义总结:
time_point + duration → time_point
time_point - duration → time_point
time_point - time_point → duration
duration + duration → duration
duration * scalar → duration
duration % duration → duration
这组定义清晰、语义合理,是仿射空间在 C++ 中的经典实现示例。
总结要点
级别 | 原则 | 是否必须? | 示例 |
---|---|---|---|
绝对必要 | == 与 != 相反 |
必须 | 自定义类型比较 |
非常重要 | + , * 的结合律 |
强烈建议 | 向量、矩阵、复杂类型 |
有风险 | 排中律 | 可能失败 | 浮点数中的 NaN 情况 |
建议遵守 | + 的交换律 |
可选 | 有意义时实现 |
数学结构模型 | 仿射空间建模(如 chrono) | 有用 | 时间、空间建模场景 |
如果你需要: |
- 实现自己仿射空间类型(如点 + 向量)
- 安全、直观地重载运算符
- 制定运算符重载规范(团队代码约束)
为什么在 C++ 中应当“符合常规”地进行运算符重载,并引出了 C++17 中的新特性 —— 折叠表达式(fold expressions) 以及其对运算符重载的一些影响。我们逐段理解如下:
为什么要“符合习惯地”重载运算符?
好处:
对 使用者(用户) 而言:
- 直觉性(intuition):符合期望,易于理解。
- 可操控性(manipulation):能用于组合、嵌套等结构。
- 遵守数学性质:如交换律、结合律、封闭性等。
对 实现者 / 设计者 而言:
- 更容易识别:
- 类型操作的最小完备集合(complete basis)
- 在最简与便利性之间取得平衡
- 更容易评估 效率
语言标准库、算法库、泛型编程
- 如:
全部假设操作符具有合理语义。std::accumulate std::reduce std::sort std::set
- 运算符的“可组合性”是 STL 成功的关键之一。
C++17:Fold Expressions 折叠表达式
定义:
将一个 参数包 Args...
用某个二元操作符(如 +
, *
, <<
)做归约操作(fold)。
示例 1:标准输出
template <typename... Args>
auto output(Args&&... args) {
return (std::cout << ... << args); // 展开为:((std::cout << arg1) << arg2) << arg3 ...
}
示例 2:矩阵乘法(顺序重要)
// 从左往右:m * arg1 * arg2 * arg3
template <typename Matrix, typename... Args>
auto multiply_on_right(Matrix&& m, Args&&... args) {
return (m * ... * args); // 左折叠
}
// 从右往左:arg1 * arg2 * arg3 * m
template <typename Matrix, typename... Args>
auto multiply_on_left(Matrix&& m, Args&&... args) {
return (args * ... * m); // 右折叠
}
是否使用左折叠 or 右折叠,取决于操作符是否 可交换(commutative)。
C++17 改变了哪些运算符语义?
P0145 提案引入:
operator&&
,operator||
,operator,
:- 以前不允许自定义这些运算符时对求值顺序做出假设
- C++17 保证它们按顺序求值
- 但仍不建议用户随便重载这些运算符,因为会导致代码语义难以理解
Fold 表达式对 结合律(Associativity) 的要求
- 折叠表达式能简洁地表示多次运算,但要求你的操作是“合理结合的”:
assert((a + b) + c == a + (b + c));
- 如果你的操作不具备结合律,就可能导致结果不一致!
Old-style right fold(传统右折叠)
struct right_multiplies {
template <typename T>
T operator()(T t1, T t2) const {
return operation(t2, t1); // 交换顺序以实现右折叠
}
};
Foo right_fold_old(Foo init, std::initializer_list<Foo> c) {
return std::accumulate(std::crbegin(c), std::crend(c), init, right_multiplies{});
}
适合处理非交换的运算符(如矩阵乘法)
C++17 New-style right fold
template <typename... Args>
Foo right_fold_new(Foo init, Args&&... args) {
return (args * ... * init); // 右折叠
}
更简洁、类型安全、不需要手动实现右乘函数。
总结:你应该怎样处理运算符重载?
准则 | 原因 | 举例 | ||
---|---|---|---|---|
遵循直觉 | 用户易懂、好用 | == , != , + , - |
||
保持数学性质 | 支持算法、泛型 | + 应该是可结合的 |
||
适配 STL 算法 | 如 accumulate , reduce |
运算符不能出错 | ||
使用 C++17 折叠表达式 | 简洁、高效、泛型 | (args + ... + init) |
||
避免滥用 && , ` |
, ,` |
求值顺序、可读性差 | 慎重重载这些 | |
如果你正在: |
- 实现支持表达式求值的 DSL 或矩阵/向量类型
- 设计跨多个值合并的逻辑
- 使用
std::reduce
,fold
,meta-programming
这里给你几个**简单清晰的折叠表达式(fold expressions)**例子,帮你快速理解它们的用法。
1. 输出所有参数(用 <<
折叠)
#include <iostream>
template<typename... Args>
void print_all(Args&&... args) {
(std::cout << ... << args) << '\n'; // 折叠表达式:((cout << arg1) << arg2) << arg3 ...
}
int main() {
print_all("Hello, ", "fold ", "expressions ", 123, "\n");
}
效果:
输出所有参数,类似连续调用 std::cout <<
。
2. 计算所有数字的和(用 +
折叠)
#include <iostream>
template<typename... Numbers>
auto sum_all(Numbers... nums) {
return (nums + ...); // 折叠表达式:((num1 + num2) + num3) + ...
}
int main() {
std::cout << sum_all(1, 2, 3, 4, 5) << '\n'; // 输出 15
}
3. 计算乘积(用 *
折叠)
#include <iostream>
template<typename... Numbers>
auto product_all(Numbers... nums) {
return (nums * ...); // 折叠表达式:((num1 * num2) * num3) * ...
}
int main() {
std::cout << product_all(2, 3, 4) << '\n'; // 输出 24
}
4. 带初始值的折叠(左折叠)
#include <iostream>
template<typename... Numbers>
auto sum_all_with_init(int init, Numbers... nums) {
return (init + ... + nums); // 左折叠,有初始值init
}
int main() {
std::cout << sum_all_with_init(10, 1, 2, 3) << '\n'; // 16
}
5. 逻辑“与”折叠(判断所有条件都为真)
#include <iostream>
template<typename... Bools>
bool all_true(Bools... bools) {
return (bools && ...); // 所有参数都为true才返回true
}
int main() {
std::cout << std::boolalpha;
std::cout << all_true(true, true, false) << '\n'; // false
std::cout << all_true(true, true, true) << '\n'; // true
}
这里给你一个简明的 C++20 三路比较运算符 (operator<=>
) 介绍和示例:
三路比较运算符 operator<=>
(太空船操作符)
- C++20 新增,用于统一比较表达式,返回五种比较类别之一:
| 类型 | 语义说明 |
| ----------------------- | ------------------------ |
|std::strong_equality
| 强相等,值完全不可区分 |
|std::weak_equality
| 弱相等,等价但可区分 |
|std::strong_ordering
| 强排序,有完全的替换性(total order) |
|std::weak_ordering
| 弱排序,有顺序但无替换性 |
|std::partial_ordering
| 部分排序,有可能无法比较大小 | - 支持 相等和大小比较 一次搞定,不用写
==
,<
,>
,<=
,>=
一大堆了。
简单例子:用 operator<=>
自动生成全部比较操作符
#include <compare>
#include <iostream>
#include <string>
struct Person {
std::string name;
int age;
auto operator<=>(const Person&) const = default; // 默认比较:先比name再比age
};
int main() {
Person p1{"Alice", 30};
Person p2{"Bob", 25};
if (p1 < p2) {
std::cout << "p1 < p2\n";
} else if (p1 == p2) {
std::cout << "p1 == p2\n";
} else {
std::cout << "p1 > p2\n";
}
}
这里,operator<=>
自动帮你生成了所有比较运算符(<, <=, >, >=, ==, !=
)。
例子:使用返回的比较类别
#include <compare>
#include <iostream>
struct Number {
int value;
std::strong_ordering operator<=>(const Number& other) const {
return value <=> other.value;
}
};
int main() {
Number a{5}, b{10};
auto cmp = a <=> b;
if (cmp == 0) {
std::cout << "a equals b\n";
} else if (cmp < 0) {
std::cout << "a less than b\n";
} else {
std::cout << "a greater than b\n";
}
}
这个案例展示了如何用 C++20 的三路比较运算符 (operator<=>
) 改写传统的大小写不敏感字符串比较类。
传统 C++17 及之前的写法
你需要手动写所有6个比较操作符:
==, !=, <, >, <=, >=
例子中用ci_compare_equal
和ci_compare_less
函数对象分别实现忽略大小写的相等和小于比较:
struct ci_compare_equal {
bool operator()(char x, char y) const {
return std::toupper(x) == std::toupper(y);
}
};
struct ci_compare_less {
bool operator()(char x, char y) const {
return std::toupper(x) < std::toupper(y);
}
};
inline bool operator==(const CIString& x, const CIString& y) {
return std::equal(x.s.cbegin(), x.s.cend(),
y.s.cbegin(), y.s.cend(), ci_compare_equal{});
}
inline bool operator<(const CIString& x, const CIString& y) {
return std::lexicographical_compare(x.s.cbegin(), x.s.cend(),
y.s.cbegin(), y.s.cend(), ci_compare_less{});
}
// 然后其他操作符通过前两个推导出来
inline bool operator!=(const CIString& x, const CIString& y) { return !(x == y); }
inline bool operator>(const CIString& x, const CIString& y) { return y < x; }
inline bool operator<=(const CIString& x, const CIString& y) { return !(y < x); }
inline bool operator>=(const CIString& x, const CIString& y) { return !(x < y); }
C++20 用三路比较运算符改写
只需要写一个 operator<=>
:
inline std::weak_ordering operator<=>(const CIString& x, const CIString& y) {
return std::lexicographical_compare_3way(
x.s.cbegin(), x.s.cend(), y.s.cbegin(), y.s.cend(),
[] (char x, char y) {
const auto diff = std::toupper(x) - std::toupper(y);
return diff < 0 ? std::weak_ordering::less :
diff > 0 ? std::weak_ordering::greater :
std::weak_ordering::equivalent;
}
);
}
- 利用
std::lexicographical_compare_3way
和自定义的比较函数来实现忽略大小写的三路比较。 - 这样一来,所有比较操作符自动生成,代码简洁且一致。
现实情况
- 这个用法是 C++20 新特性,虽然很强大,但目前库支持和性能问题还在逐步完善中。
- 还需注意泛型代码和与旧代码的兼容性问题。
- 可能暂时不会完全替代旧写法,但未来趋势明显。
一个完整的忽略大小写字符串类 CIString示例,分别展示传统的比较运算符重载写法(C++17及之前)和 C++20 的三路比较运算符写法。
1. 传统写法(C++17及之前)
#include <string>
#include <algorithm>
#include <cctype>
#include <iostream>
struct CIString {
std::string s;
CIString(const std::string& str) : s(str) {}
};
// 忽略大小写的字符相等比较器
struct ci_compare_equal {
bool operator()(char x, char y) const {
return std::toupper(static_cast<unsigned char>(x)) == std::toupper(static_cast<unsigned char>(y));
}
};
// 忽略大小写的字符小于比较器
struct ci_compare_less {
bool operator()(char x, char y) const {
return std::toupper(static_cast<unsigned char>(x)) < std::toupper(static_cast<unsigned char>(y));
}
};
inline bool operator==(const CIString& x, const CIString& y) {
return std::equal(x.s.cbegin(), x.s.cend(), y.s.cbegin(), y.s.cend(), ci_compare_equal{});
}
inline bool operator!=(const CIString& x, const CIString& y) {
return !(x == y);
}
inline bool operator<(const CIString& x, const CIString& y) {
return std::lexicographical_compare(x.s.cbegin(), x.s.cend(), y.s.cbegin(), y.s.cend(), ci_compare_less{});
}
inline bool operator>(const CIString& x, const CIString& y) {
return y < x;
}
inline bool operator<=(const CIString& x, const CIString& y) {
return !(y < x);
}
inline bool operator>=(const CIString& x, const CIString& y) {
return !(x < y);
}
// 测试
int main() {
CIString a("Hello");
CIString b("hELLo");
CIString c("world");
std::cout << std::boolalpha;
std::cout << "(a == b): " << (a == b) << "\n"; // true
std::cout << "(a != c): " << (a != c) << "\n"; // true
std::cout << "(a < c): " << (a < c) << "\n"; // true
std::cout << "(c > a): " << (c > a) << "\n"; // true
}
2. C++20 写法,使用三路比较运算符 (operator<=>
)
#include <string>
#include <compare>
#include <iostream>
#include <cctype>
#include <algorithm>
struct CIString {
std::string s;
CIString(const std::string& str) : s(str) {}
};
// 三路比较运算符实现忽略大小写比较
inline std::weak_ordering operator<=>(const CIString& x, const CIString& y) {
return std::lexicographical_compare_3way(
x.s.cbegin(), x.s.cend(), y.s.cbegin(), y.s.cend(),
[] (char cx, char cy) {
unsigned char ux = static_cast<unsigned char>(cx);
unsigned char uy = static_cast<unsigned char>(cy);
int diff = std::toupper(ux) - std::toupper(uy);
return diff < 0 ? std::weak_ordering::less :
diff > 0 ? std::weak_ordering::greater :
std::weak_ordering::equivalent;
}
);
}
// C++20会自动根据 operator<=> 生成 ==, !=, <, >, <=, >=
int main() {
CIString a("Hello");
CIString b("hELLo");
CIString c("world");
std::cout << std::boolalpha;
std::cout << "(a == b): " << (a == b) << "\n"; // true
std::cout << "(a != c): " << (a != c) << "\n"; // true
std::cout << "(a < c): " << (a < c) << "\n"; // true
std::cout << "(c > a): " << (c > a) << "\n"; // true
}
说明:
- 传统写法需要写齐全的六个比较运算符,逻辑略显繁琐。
- C++20 三路比较写法只写一个函数,自动提供其他比较运算符。
- 都用到了
std::toupper
,为了避免char
负值的问题,转换成unsigned char
后再调用。
DSL(领域专用语言)、UDL(用户定义字面量)、Boost.SML(状态机库),还有提到了Monads(单子)相关的话题,尤其是它们在C++中的使用和理解难点。
下面帮你理清几个重点:
1. DSL(Domain Specific Language)
- 作用:简化复杂对象的构造和操作,使代码更简洁、可读、易操作。
- 特点:
- 代码更简洁(terser)
- 更简单易懂(simpler)
- 更容易被操控和组合(manipulable)
- 示例:
- C++20的
chrono
库通过UDL(用户定义字面量)和操作符重载,实现了日期时间的DSL:using namespace std::chrono; constexpr auto today = 2018y/September/25;
std::filesystem::path
通过重载/
操作符,实现路径拼接的DSL:auto home = path{"/home"} / "user";
- C++20的
2. UDLs(User Defined Literals)
- 方便DSL的实现,比如把
2018y
、25d
等字面量变成对应类型,方便写出可读的领域代码。
3. Boost.SML
- 是一个现代C++状态机库,支持用DSL语法定义状态转换:
return make_transition_table( *"established"_s + event<release> / send_fin = "fin wait 1"_s, ... );
- 这种写法非常符合DSL理念,清晰表达状态机的行为。
4. Monads(单子)
- 讨论的难点:
- 最大的问题是“理解”和“解释”单子(理解起来难,尤其是对非函数式背景的开发者)。
- 另外CT(编译时)技巧用得多,复杂度高。
- 以及“尝试把所有东西都弄成单子”的冲动,这会增加代码复杂度。
- 对比:DSL让代码更易懂,而单子虽然强大,但理解和使用门槛较高。
总结
- DSL 用于简化复杂对象构造,提升可读性和表达能力。
- UDLs 是实现DSL的好帮手。
- Boost.SML示范了如何用DSL表达状态机。
- 单子虽强大,但理解和解释难,是使用中的大问题。
这段内容讲的是Monads(单子)和操作符重载在C++中组合异步计算(比如 future
)时的应用和难点。
主要问题:Monads 和 C++ 中的操作符重载
operator>>=
是右结合的
在Haskell等函数式语言里,>>=
(bind操作符)用来把计算串起来(monadic composition),它是右结合的。这意味着表达式形如:
是理解为:a >>= b >>= c
这给设计C++中类似的operator overload带来挑战,因为C++运算符结合性是固定的(某些操作符是左结合,比如a >>= (b >>= c)
>>=
是右结合)。- 在C++里,用什么操作符重载来实现“monadic composition”?
由于>>=
是右结合,不能直接用它来表达复杂的组合链(尤其链中又有其它组合操作),设计一种清晰可读且语义明确的表达式很难。
Operator Overloading 和 Futures
- 异步计算通常用
future
和promise
来表达,存在串行和并行的组合问题。 - 例如:
my_future<A> f(X); my_future<B> g1(A); my_future<C> g2(A); my_future<D> h(B, C);
- 组合异步调用通常写法:
auto fut = f(); auto split1 = fut.then(g1); auto split2 = fut.then(g2); auto fut2 = when_all(split1, split2).then(h);
- 但这写起来繁琐,不够直观。
利用操作符重载简化组合
- 通过重载操作符,可以把复杂链条变成简洁表达式:
auto fut = f() >= (g1 & g2) >= h;
- 这里
>=
和&
被重载,分别表示“顺序组合”和“并行组合”,用操作符的语义让代码看起来像DSL,更易读。
结论
- Monads的难点之一是组合操作的语法问题,尤其是在C++这种强类型、固定运算符结合性的语言中。
- 操作符重载是实现清晰、简洁、类似DSL的异步组合语法的关键工具。
- 这不仅适合Monads,也适合
future
和异步任务的组合,帮助表达复杂的异步控制流。
这部分内容主要讲的是**C++中操作符重载(operator overloading)**的使用场景和注意事项,尤其是“什么时候应该用操作符重载,什么时候不应该”。
总结如下:
1. 什么时候用操作符重载?
- 你有一个自然的二元函数,这个函数逻辑上是结合两个相同类型(或相关类型)对象的操作,比如加法、乘法、拼接、合并等。
例子:向量加法、字符串拼接等。 - 你的类型符合数学规律,尤其是结合律(associativity)等性质。
这让操作符的语义清晰且可靠。 - 你希望用户能方便地构造表达式,操作符重载让表达式更简洁,更贴近领域语言。
例如,a + b + c
比写成add(add(a,b), c)
更直观。 - 你想简化复杂对象的构造,操作符可以写成类似DSL(领域专用语言)的风格,方便表达复杂逻辑。
- 你希望用户能直观理解你的类型的性质,操作符能传达“这个类型能做什么样的操作”。
2. 什么时候不应该用操作符重载?
- 内容没说完,但通常建议是:
- 不要仅用操作符,忽略了清晰的函数接口。
- 操作符语义不清晰、或容易引起误解时不要用。
- 操作复杂且不符合数学直觉的行为,避免滥用操作符。
- 操作符导致代码难懂时应避免。
3. 其他细节(提及但未展开)
- 自由函数(free function)vs. 成员函数:
- 操作符重载既可以写成成员函数,也可以写成自由函数,通常二元操作符写成自由函数更灵活。
- 写操作符时别忘了加上
constexpr
,const
,noexcept
等修饰符,保证语义正确和性能最优。
总结: 操作符重载是强大工具,适合表达自然且数学意义明确的操作,但要避免滥用。合理设计操作符能让类型更好用、更直观。
你这部分内容继续讲的是操作符重载的注意事项和最佳实践,尤其是“什么时候不该用操作符重载”和“好的操作符重载习惯”
什么时候不要用操作符重载?
- 当你用一个n元(n-ary,即参数不止两个)的函数能更高效时,不要仅仅用操作符重载。
例如,如果要处理三个或以上参数,函数接口比写多个嵌套操作符更清晰、性能也可能更好。 - 当某些操作符还不成熟、不稳定时(比如C++20的
operator<=>
)要谨慎使用。 - 不要破坏
operator==
和operator!=
的互斥性(contrariety),即它们必须逻辑上相反,不能同时为真或同时为假。 - 不要破坏结合律(associativity)。
操作符重载的行为应当满足数学上结合律的期待,否则表达式的结果会令人困惑。 - 不要害怕只重载一个操作符,只要合理(比如
/
操作符)。
并非所有操作符都必须成套出现。 - 不要重载像
operator&&
、operator||
、operator,
这类很特殊的操作符,尽管有提案(P0145),但它们语义复杂,容易导致误用和代码可读性差。 - 不要选用很奇怪的操作符来实现你的数学类型。
操作符应该符合直觉和习惯。
好的实践(DO)
- 使用非数学惯例的约定,例如某些领域可能有自己特殊的操作符约定,符合领域语义即可。
- 考虑区分你的类型以利用仿射空间(affine spaces)等数学结构,方便更好地表达类型关系。
- 对于非交换(non-commutative)操作,可以使用操作符配合C++17的折叠表达式(fold expressions)来简化代码。
- 使用用户自定义字面量(UDLs)作为操作符的辅助,帮助构造复杂表达式或对象。
比如构造单位(单位制DSL)、日期时间等。 - 如果你提供了一个操作符,最好提供它的相关操作符的完整集合,保持接口一致性和完整性。
总结
- 操作符重载是让代码更自然、表达力更强的工具,但用错了就会让代码难懂、出错。
- 遵守数学和逻辑的原则(比如结合律、互斥性),保持代码行为符合预期。
- 合理选择操作符和使用场景,避免滥用和过度复杂化。
- 结合现代C++特性(UDL、折叠表达式等)提升DSL和操作符的表达能力。