CppCon 2018 学习:OPERATOR OVERLOADING HISTORY, PRINCIPLES AND PRACTICE

发布于:2025-07-05 ⋅ 阅读:(18) ⋅ 点赞:(0)

C++ 中的运算符(operators)设计限制。这段话意在说明:

我们无法控制的东西:C++ 运算符的局限性

作者重复强调:

→ 它们真的不怎么样!

这是在批评 C++ 运算符设计的一些不灵活之处,列举如下:

我们不能控制的内容:

  1. 运算符名字(name)
    • 比如 + 就是 +,你不能定义新的符号运算符(不能有 ++>** 等)。
  2. 优先级(precedence)
    • 所有运算符的优先级是固定的,无法改变。
    • 例:* 总比 + 优先,你不能定义一个自定义 @ 运算符并指定它的优先级高于 *
  3. 结合性(associativity)
    • 例如 a - b - c 是从左到右(左结合),这个行为是固定的。
    • 不能定义一个自定义运算符让它变成右结合。
  4. 元数(arity)
    • 运算符只能是一元(单目)或二元(双目),不能定义三元、四元的运算符。
  5. 位置性(fixity)
    • 比如前缀(++x)、后缀(x++)或中缀(x + y)位置是固定的。
    • 无法控制自定义运算符是“在哪”出现。
  6. 求值语义(evaluation semantics)
    • 无法控制操作数的求值时机(比如懒求值、惰性计算);
    • C++ 保留了语言级别的严格求值顺序,对用户自定义 operator 没法“跳过”。

接下来的问题是:

为什么还要使用运算符?

“The obvious first question: Why should we use operators at all?”

也就是说:既然 C++ 运算符有这么多无法控制的限制,那我们为什么还要使用它们?为什么不全部用函数来表达操作行为?

可能的答案(作者没有写出,但你可以预判如下):

  • 语法美观简洁:a + badd(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::complexstd::valarray 等都维持结合律。
  • 违反结合律的运算会破坏通用算法,如 std::accumulate, std::reduce 等。

排中律:a > b 或 a <= b

assert(a > b || a <= b);  // 总是真的?不总是,特别是 float!

这条逻辑上成立,但:

  • floatdouble 来说不一定,比如:
    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_equalci_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";
      

2. UDLs(User Defined Literals)

  • 方便DSL的实现,比如把2018y25d等字面量变成对应类型,方便写出可读的领域代码。

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
    
    是理解为:
    a >>= (b >>= c)
    
    这给设计C++中类似的operator overload带来挑战,因为C++运算符结合性是固定的(某些操作符是左结合,比如>>=是右结合)。
  • 在C++里,用什么操作符重载来实现“monadic composition”?
    由于>>=是右结合,不能直接用它来表达复杂的组合链(尤其链中又有其它组合操作),设计一种清晰可读且语义明确的表达式很难。

Operator Overloading 和 Futures

  • 异步计算通常用futurepromise来表达,存在串行和并行的组合问题。
  • 例如:
    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和操作符的表达能力。