欢迎光临
我们一直在努力

对于 C++:C++20中三路比较运算符与格式化库的解析

写在前面

C++20 是 C++ 语言发展史上一个里程碑式的版本,它带来的特性之多、影响之深,堪称"小 C++11"。而在 C++20 引入的众多特性中,有两个看起来不那么"炫酷"、但实际日常开发中每天都可能用到的工具——三路比较运算符(<=>,俗称太空船运算符) 和 std::format 格式化库——它们从最基础的层面改变了我们写比较逻辑和格式化输出的方式。

在 C++20 之前,如果你想让一个自定义类型支持所有的比较操作——<、<=、>、>=、==、!=,你需要手动重载六个运算符。这不仅是体力活,更严重的问题是:很难保证所有运算符的逻辑一致性。人不是机器,在写第六个运算符的时候,很可能已经忘了第一个是怎么写的,于是出现 a < b 和 b > a 结果不一致的 bug 简直是家常便饭。至于格式化输出,printf 的类型不安全问题和 iostream 的冗长语法同样让人头疼——printf("%s", 42) 编译时不会报错,但运行时直接崩溃;而 cout << "Hello, " << name << "! You are " << age << " years old." << endl; 这种写法,读起来费劲,写起来更费劲。

那么在本篇博客中,我们就要来学习这些内容~~


第一章 三路比较运算符(<=>)全景解析

1.1 为什么需要三路比较运算符

在 C++20 之前,想让一个自定义类型支持比较操作,需要写大量的重复代码。

假设有一个 Point 结构体,包含两个整数坐标 x 和 y。如果想让 Point 支持所有六种比较操作,需要写六个运算符重载:

struct Point {
int x;
int y;

// 需要重载 6 个比较运算符…
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
bool operator!=(const Point& other) const {
return !(*this == other);
}
bool operator<(const Point& other) const {
if (x != other.x) return x < other.x;
return y < other.y;
}
bool operator<=(const Point& other) const {
return *this < other || *this == other;
}
bool operator>
(const Point& other) const {
return !(*this <= other);
}
bool operator>=(const Point& other) const {
return !(*this < other);
}
};

这段代码有以下几个问题:

  • 重复劳动:六个运算符,逻辑本质上是相同的,但必须逐个手写。对于一个大型项目中的几十个类型,工作量可想而知。
  • 维护困难:如果 Point 以后增加了 z 坐标,需要修改所有六个运算符,漏掉任何一个都会导致不一致。
  • 容易出错:operator> 的实现依赖于 operator<=,operator<= 又依赖于 operator< 和 operator==,这种依赖链条越长,出现逻辑错误的概率就越大。
  • 性能问题:operator!= 调用 operator==,operator< 调用两次比较——这种间接调用在某些情况下会带来不必要的性能开销。

三路比较运算符 <=> 的诞生,就是为了从根本上解决这个问题。它的核心思想是:只需要实现一个比较函数,描述"当前对象比另一个对象小、相等、还是大",编译器就能自动推导出所有六种比较操作的结果。不管你的类型有多少个成员变量,只要定义好 <=>,剩下的交给编译器,请叫编译器为小聪明~~


1.2 三路比较运算符的基本概念

<=> 运算符的官方名称是"三路比较运算符"(Three-way Comparison Operator),但因为它看起来像一艘太空船(spaceship),所以大家更习惯叫它"太空船运算符"。

语法:

auto result = a <=> b;

这个表达式对 a 和 b 进行比较,然后返回一个比较结果对象。这个对象的类型不是 int,而是 std::strong_ordering、std::weak_ordering 或 std::partial_ordering 三者之一。这三种类型分别对应不同的比较语义严格程度,后面会详细解释。

核心用法:<=> 的返回结果可以跟字面量 0 进行比较,从而判断两个值之间的关系:

  • (a <=> b) < 0 → a < b
  • (a <=> b) == 0 → a == b
  • (a <=> b) > 0 → a > b
  • (a <=> b) <= 0 → a <= b
  • (a <=> b) >= 0 → a >= b

为什么返回值不能直接用 int?这个问题很多人在第一次接触 <=> 时都会问。既然 <=> 的结果能跟 0 比较,那为什么不直接返回一个 int 呢?-1 表示小于,0 表示等于,1 表示大于,多简单?

答案在于:不是所有类型的比较都能用"小于、等于、大于"这三个状态来完整描述。浮点数就是一个典型的例子——NaN(Not a Number)和任何值比较都是"不可比较的"(unordered),既不是小于,也不是等于,也不是大于。如果用 int 返回,无法表达"不可比较"这个第四种状态。再比如,不区分大小写的字符串比较,"Hello" 和 "HELLO" 在忽略大小写时是"等价的",但它们不是"完全相同"的——你不能用 "Hello" 去替换 "HELLO" 而不改变程序行为。int 返回值无法区分"完全相等"和"等价但不等同"这两种不同的语义。

三种比较类别就是为了解决这些问题而设计的。它们本质上是对比较结果的一种类型层面的精细化分类,让编译器在编译期就能知道某个类型比较的语义严格程度,从而在生成代码时做出最优决策。

第一个完整的示例:

#include <iostream>
// 注意:基本类型的 <=> 运算符不需要引入 <compare> 头文件
// 但自定义类型使用 <=> 时,建议包含 <compare> 以使用比较类别类型

int main()
{
int a = 0, b = 0;

// 从标准输入读取两个整数
std::cout << "请输入两个整数: ";
std::cin >> a >> b;

// 使用三路比较运算符进行比较
// a <=> b 返回 std::strong_ordering 类型
// 因为 int 是基本类型,比较结果必然是强序
auto result = a <=> b;

// 将比较结果与字面量 0 进行比较
// 这里 result < 0 的 < 运算符是 std::strong_ordering 类型
// 与字面量 0 之间的比较,不是 int 之间的比较
if (result < 0) {
std::cout << "a < b" << '\\n';
}
else if (result == 0) {
// 注意:这里是 result == 0,不是 result == 0
// 0 是 int 字面量,但 result 是 std::strong_ordering 类型
// 之所以能比较,是因为 std::strong_ordering 重载了 operator==
// 这个重载接受一个 unspecified(未指定)类型的参数
// 只允许与整数字面量 0 进行比较
std::cout << "a == b" << '\\n';
}
else { // result > 0 的情况
std::cout << "a > b" << '\\n';
}

return 0;
}

关于 0 比较的细节:<=> 的返回结果只能跟字面量 0 比较,不能跟 1、-1 或其他任何整数值比较。这是因为 std::strong_ordering、std::weak_ordering、std::partial_ordering 这三个类型都重载了 operator==、operator<、operator<=、operator>、operator>=,这些重载的右操作数类型是一个未指定的整数类型(/*unspecified*/),这个类型只接受值为 0 的整数字面量。如果你写 result == 1,编译器会报错。这是设计上的故意为之——<=> 的结果语义是"小于、等于、大于",而不是"返回一个数值"。


1.3 为自定义类型重载 <=> 运算符

1.3.1 基本语法

<=> 运算符可以重载为类的成员函数,一般形式如下:

auto operator<=>(const T& other) const

  • 参数:一个 const T& 类型的引用,指向被比较的另一个对象。用 const 引用是为了避免不必要的拷贝,同时保证不修改被比较的对象。
  • 返回值:auto 让编译器自动推导返回类型。实际返回类型是 std::strong_ordering、std::weak_ordering 或 std::partial_ordering 之一,具体取决于成员变量的比较类别。
  • const 限定符:表示这个比较操作不会修改当前对象。这是必须的,因为比较两个对象不应该改变它们的状态。
1.3.2 默认生成的 <=> 运算符

如果希望编译器自动生成 <=> 运算符,可以使用 = default 语法:

auto operator<=>(const T& other) const = default;

编译器默认生成的 operator<=> 会按照成员变量的声明顺序,依次对每个成员调用 <=> 进行比较:

  • 先比较第一个成员变量。如果 <=> 的结果不是 equal(或 equivalent),直接返回这个结果。
  • 如果第一个成员相等,继续比较第二个成员变量。
  • 以此类推,直到所有成员都比较完。
  • 如果所有成员都相等,返回 equal(或 equivalent)。
  • 默认生成的前提条件:类中的所有成员变量都必须支持 <=> 运算符。只要有一个成员不支持 <=>(比如一个没有重载 <=> 的自定义类型),编译器就无法生成 <=>,此时 = default 会导致编译错误。

    示例:使用默认生成的 <=> 运算符:

    #include <iostream>
    #include <compare>

    class Point {
    int _x; // int 类型支持 <=>,返回 std::strong_ordering
    int _y; // int 类型支持 <=>,返回 std::strong_ordering
    public:
    Point(int x = 0, int y = 0)
    :_x(x)
    ,_y(y)
    {}

    // 让编译器自动生成 operator<=>
    // 编译器会按声明顺序比较 _x 和 _y
    // 先比较 _x,如果 _x 相等再比较 _y
    // 因为 _x 和 _y 都是 int,所以返回类型会被推导为 std::strong_ordering
    auto operator<=>(const Point& other) const = default;
    };

    int main()
    {
    Point p1{ 1, 2 }; // 创建第一个点 (1, 2)
    Point p2{ 1, 3 }; // 创建第二个点 (1, 3)

    // 直接使用 <=> 进行比较
    auto ret = p1 <=> p2;
    // 因为 _x 都是 1,相等,所以继续比较 _y
    // _y 分别是 2 和 3,2 < 3,所以返回 std::strong_ordering::less
    if (ret > 0)
    std::cout << "p1 > p2" << '\\n';
    else if (ret < 0)
    std::cout << "p1 < p2" << '\\n';
    else
    std::cout << "p1 == p2" << '\\n';

    // 编译器自动生成的其他比较运算符
    // 当 operator<=> 被定义为 = default 时
    // 编译器会自动生成 <, <=, >, >= 四个运算符
    // 注意:== 和 != 不会自动生成,需要单独定义或使用operator==的 = default
    // 在 C++20 中,如果定义了 operator<=>,但 operator== 没有被定义
    // 编译器不会自动生成 operator==,因为 operator== 可以用更高效的方式实现
    if (p1 < p2)
    std::cout << "p1 < p2 (通过 <=> 生成)" << '\\n';
    else if (p1 > p2)
    std::cout << "p1 > p2 (通过 <=> 生成)" << '\\n';
    else
    std::cout << "p1 == p2 (通过 <=> 生成)" << '\\n';

    return 0;
    }

    输出结果:

    p1 < p2
    p1 < p2 (通过 <=> 生成)

    1.3.3 自定义 <=> 运算符的实现

    不是所有情况都适合用 = default。有时候需要自定义比较逻辑,比如:

    • 只比较某些成员变量,忽略其他成员
    • 比较逻辑不是简单的逐成员比较
    • 需要特殊的比较规则(如不区分大小写)

    手动实现 <=> 运算符的通用模式:

    #include <iostream>
    #include <compare>

    class Point {
    int _x, _y;
    public:
    Point(int x = 0, int y = 0)
    :_x(x)
    ,_y(y)
    {}

    // 手动实现 operator<=>
    // 比较规则:先比较 _x,如果 _x 相等再比较 _y
    auto operator<=>(const Point& other) const {
    // 第一步:比较 _x
    // 如果 _x <=> other._x 的结果不是 0(即不相等),直接返回这个结果
    if (auto cmp = _x <=> other._x; cmp != 0) {
    return cmp; // 返回 std::strong_ordering 类型
    }
    // 第二步:_x 相等,继续比较 _y
    // 直接返回 _y <=> other._y 的结果
    else {
    return _y <=> other._y;
    }
    }

    // 注意:当手动实现 operator<=> 时
    // 编译器不会自动生成 operator==
    // 需要手动实现 operator==,或者使用operator==的 = default 生成
    bool operator==(const Point& other) const {
    return _x == other._x && _y == other._y;
    }
    };

    int main()
    {
    Point p1{ 2, 3 };
    Point p2{ 1, 5 };

    // 先比较 _x:2 <=> 1 = greater,直接返回 greater
    // 所以结果是 p1 > p2
    auto ret = p1 <=> p2;
    if (ret > 0) std::cout << "p1 > p2" << '\\n';
    else if (ret < 0) std::cout << "p1 < p2" << '\\n';
    else std::cout << "p1 == p2" << '\\n';

    return 0;
    }

    手动实现的关键点:

    • 使用 auto 作为返回类型,让编译器自动推导。如果成员变量都是 int 这种强序类型,返回值就是 std::strong_ordering。
    • 使用 if (auto cmp = …; cmp != 0) 这种带初始化语句的 if 表达式,这是 C++17 引入的语法,在这里非常适用——它把比较和判断合并到一行,代码更紧凑。
    • 如果所有成员比较结果都是 0(相等),<=> 会自动返回 std::strong_ordering::equal。
    • 手动实现 <=> 后,编译器不会自动生成 operator==,需要单独实现。这是因为 operator== 可能有更高效的实现方式(比如 string 可以先比较长度)。

    1.4 编译器自动生成比较运算符的机制

    这是 <=> 运算符最强大的特性:当你为一个类定义了 operator<=> 后,编译器会自动生成 !=、<、<=、>、>= 这几个运算符。具体来说:

    • a != b 被重写为 !(a == b)
    • a < b 被重写为 (a <=> b) < 0
    • a <= b 被重写为 (a <=> b) <= 0
    • a > b 被重写为 (a <=> b) > 0
    • a >= b 被重写为 (a <=> b) >= 0

    但有一个重要的例外:operator== 不会自动通过 <=> 生成。原因是 C++ 标准委员会认为,operator== 在很多情况下可以用更高效的方式实现,不应该强制使用 <=> 来生成。std::string 就是一个典型的例子——它在比较两个字符串是否相等时,可以先比较长度,如果长度不同直接返回 false,不需要逐字符比较。而 <=> 必须逐字符比较才能确定结果。所以 std::string 保留了自定义的 operator==,移除了 !=、<、<=、>、>=,让编译器通过 <=> 生成这些运算符。

    运算符查找优先级:如果类中同时存在自定义的 operator== 和编译器生成的 operator==(通过 <=>),自定义版本优先。也就是说,如果显式声明了 operator==,编译器就不会通过 <=> 生成 operator==。对于 !=、<、<=、>、>= 也是同理——如果已经显式定义了,编译器就不会自动生成。


    1.5 std::string 的 <=> 运算符实践

    std::string 是 C++20 标准库中最早一批适配 <=> 的类型之一。在 C++20 中,std::string 新增了 operator<=>,同时移除了 !=、<、<=、>、>= 这几个运算符的重载,只保留了 operator== 的自定义实现。

    #include <iostream>
    #include <string>
    #include <compare>

    int main()
    {
    std::string s1 = "apple";
    std::string s2 = "banana";
    std::string s3 = "apple";

    // 直接使用 <=> 进行比较
    // std::string 的 operator<=> 返回 std::strong_ordering
    // 因为字符串比较是逐字符比较 ASCII 值,是完全可替换的
    auto cmp = s1 <=> s2;
    if (cmp < 0) {
    std::cout << "s1 < s2" << '\\n'; // 输出:s1 < s2
    }

    // 使用通过 <=> 生成的 < 运算符
    if (s1 < s2) {
    std::cout << "s1 < s2 (通过 <=> 生成)" << '\\n'; // 输出
    }

    // 使用通过 <=> 生成的 > 运算符
    if (s2 > s1) {
    std::cout << "s2 > s1 (通过 <=> 生成)" << '\\n'; // 输出
    }

    // 使用自定义的 operator==(不是通过 <=> 生成的)
    // std::string 的 operator== 先比较长度,长度相等再比较字符
    if (s1 == s3) {
    std::cout << "s1 == s3" << '\\n'; // 输出
    }

    // 使用通过 <=> 生成的 != 运算符
    if (s1 != s2) {
    std::cout << "s1 != s2" << '\\n'; // 输出
    }

    return 0;
    }

    输出结果:

    s1 < s2
    s1 < s2 (通过 <=> 生成)
    s2 > s1 (通过 <=> 生成)
    s1 == s3
    s1 != s2


    第二章 比较类别(Comparison Category)深度解析

    2.1 为什么需要三种比较类别

    在 C++20 中,<=> 运算符的返回类型分为三种:std::strong_ordering、std::weak_ordering 和 std::partial_ordering。它们不是随便设计的,而是对应着数学中三种不同的比较关系。

    用一句话概括三者的区别:

    • 强序(strong_ordering):相等的值完全等价,可以互换。比如整数 5 和 5 是完全相同的,用哪个都一样。
    • 弱序(weak_ordering):相等的值只是"等价"而非"完全相同",不能随意互换。比如不区分大小写的字符串比较,"Hello" 和 "HELLO" 是等价的,但你不能拿 "Hello" 去替代 "HELLO" 而不影响程序行为。
    • 偏序(partial_ordering):允许出现"不可比较"的情况。比如浮点数 NaN 和任何值比较都是不可比较的。

    这三种类型的继承关系是:strong_ordering 可以隐式转换为 weak_ordering,weak_ordering 可以隐式转换为 partial_ordering。也就是说,强序是弱序的子集,弱序是偏序的子集。这个转换规则意味着,如果你的类型满足强序条件,但函数签名要求返回 weak_ordering,编译器会自动帮你转换,不需要手动干预。


    2.2 std::strong_ordering(强序)——最严格的比较

    头文件:<compare>

    官方参考:https://en.cppreference.com/w/cpp/utility/compare/strong_ordering

    定义:std::strong_ordering 是三种比较类别中最严格的一种。它要求:如果两个值比较相等,那么它们必须是"不可区分的"(indistinguishable),即用其中一个替换另一个不会改变程序的行为。

    四个静态成员常量:

    常量名含义对应的比较结果
    std::strong_ordering::less 小于 a < b
    std::strong_ordering::equal 等于(完全相等) a == b
    std::strong_ordering::equivalent 等价 与 equal 语义完全相同
    std::strong_ordering::greater 大于 a > b

    注意 equal 和 equivalent 在 std::strong_ordering 中是完全相同的——它们都是"相等"的意思,提供两个名字只是为了和 std::weak_ordering 的接口保持一致。

    适用场景:

    • 整数类型(int、long、short 等)
    • 字符类型(char、wchar_t、char8_t、char16_t、char32_t)
    • 指针类型
    • 枚举类型
    • 所有成员都是强序类型的结构体/类

    示例代码:

    #include <iostream>
    #include <compare>

    int main()
    {
    int a = 5, b = 10;

    // int 的 <=> 运算符返回 std::strong_ordering
    auto res1 = a <=> b;
    // 5 <=> 10 的结果是 std::strong_ordering::less

    // 直接与枚举值比较
    if (res1 == std::strong_ordering::less) {
    std::cout << "a < b" << '\\n'; // 输出
    }

    // 使用比较类别类型定义的另一个 Point 结构体
    struct Point {
    int x;
    int y;
    // 使用 = default 让编译器自动生成
    // 因为 x 和 y 都是 int,返回类型自动推导为 std::strong_ordering
    auto operator<=>(const Point&) const = default;
    };

    Point p1{ 1, 2 }, p2{ 1, 2 };
    auto res2 = p1 <=> p2;
    // 两个点的 x 和 y 都相等,返回 std::strong_ordering::equal

    if (res2 == std::strong_ordering::equal) {
    std::cout << "p1 == p2" << '\\n'; // 输出
    }

    // 强序的核心性质:等价的对象可以完全互换
    // 如果 p1 == p2,那么在任何地方用 p2 替换 p1
    // 程序的执行结果不会发生任何变化
    if (p1 == p2) {
    // 这两行代码的行为完全相同,因为 p1 和 p2 是不可区分的
    // some_function(p1); // 如果用 p2 替换 p1,行为不变
    // some_function(p2); // 与上一行完全相同的行为
    }

    return 0;
    }


    2.3 std::weak_ordering(弱序)——等价但不等同

    头文件:<compare>

    官方参考:https://en.cppreference.com/w/cpp/utility/compare/weak_ordering

    定义:std::weak_ordering 比强序稍弱。它要求:比较结果满足"小于、等价、大于"三种关系,但"等价"的值不一定是不可区分的。也就是说,两个等价的值可能在某些方面不同,不能保证互换后程序行为不变。

    四个静态成员常量:

    常量名含义说明
    std::weak_ordering::less 小于 a < b
    std::weak_ordering::equivalent 等价 等价但不一定相等,不能互换
    std::weak_ordering::greater 大于 a > b

    注意 std::weak_ordering 没有 equal 常量,只有 equivalent。这是设计上的刻意区分——在弱序中,“等价"不等于"相等”,所以用 equivalent 而不是 equal 来命名,从名字上就告诉开发者:这两个值只是"等价"而已,不要认为它们完全相同。

    适用场景:

    • 不区分大小写的字符串比较:"Hello" 和 "HELLO" 在忽略大小写时等价,但它们不是相同的字符串。
    • 忽略某些非关键成员变量的比较:比如比较两个用户时,只比较 ID,忽略用户名的大小写差异。
    • 按某种规范化形式比较:比如忽略空格、标点符号的比较。

    经典实现:不区分大小写的字符串比较:

    #include <string>
    #include <compare>
    #include <cctype> // 提供 std::tolower 函数
    #include <iostream>
    #include <vector>
    #include <algorithm>

    // 不区分大小写的字符串类
    // 这个类演示了 weak_ordering 的典型应用场景
    class CaseInsensitiveString {
    public:
    // 存储原始字符串
    std::string s;

    // 默认构造函数
    CaseInsensitiveString() = default;

    // 从 std::string 构造,使用 explicit 防止隐式转换
    explicit CaseInsensitiveString(const std::string& str) : s(str) {}

    // 从 C 风格字符串构造,使用 explicit 防止隐式转换
    explicit CaseInsensitiveString(const char* str) : s(str) {}

    // 三路比较运算符(不区分大小写)
    // 返回类型是 std::weak_ordering 而不是 std::strong_ordering
    // 因为不区分大小写的比较中,"等价"不等于"完全相同"
    std::weak_ordering operator<=>(const CaseInsensitiveString& other) const {
    return case_insensitive_compare(s, other.s);
    }

    // 相等运算符(不区分大小写)
    // 注意:当手动实现 operator<=> 时,必须手动实现 operator==
    // 因为编译器不会自动生成 operator==
    bool operator==(const CaseInsensitiveString& other) const {
    return case_insensitive_equal(s, other.s);
    }

    // 转换为普通字符串的访问函数
    const std::string& str() const { return s; }

    private:
    // 不区分大小写的三路比较函数
    // 参数:两个要比较的字符串 a 和 b
    // 返回值:std::weak_ordering 类型
    // – less: a 在忽略大小写的情况下小于 b
    // – equivalent: a 和 b 在忽略大小写的情况下相等
    // – greater: a 在忽略大小写的情况下大于 b
    static std::weak_ordering case_insensitive_compare(
    const std::string& a, const std::string& b) {

    // 使用迭代器逐字符比较
    auto it1 = a.begin(); // 指向字符串 a 的第一个字符
    auto it2 = b.begin(); // 指向字符串 b 的第一个字符

    // 同时遍历两个字符串,直到其中一个到达末尾
    while (it1 != a.end() && it2 != b.end()) {
    // 将当前字符转换为小写
    // 使用 static_cast<unsigned char> 防止负值 char 导致 UB
    char c1 = std::tolower(static_cast<unsigned char>(*it1));
    char c2 = std::tolower(static_cast<unsigned char>(*it2));

    // 比较小写形式的字符
    if (c1 < c2) return std::weak_ordering::less;
    if (c1 > c2) return std::weak_ordering::greater;

    // 当前字符相等,继续比较下一个字符
    ++it1;
    ++it2;
    }

    // 两个字符串同时到达末尾,说明它们完全等价
    if (it1 == a.end() && it2 == b.end()) {
    return std::weak_ordering::equivalent;
    }

    // 如果 a 先到达末尾,说明 a 是 b 的前缀,a < b
    if (it1 == a.end()) return std::weak_ordering::less;

    // 如果 b 先到达末尾,说明 b 是 a 的前缀,a > b
    return std::weak_ordering::greater;
    }

    // 不区分大小写的相等判断函数
    // 参数:两个要比较的字符串 a 和 b
    // 返回值:true 表示在忽略大小写的情况下相等,false 表示不等
    static bool case_insensitive_equal(const std::string& a, const std::string& b) {
    // 先比较长度,长度不同肯定不相等(优化)
    if (a.size() != b.size()) return false;

    // 长度相等,逐字符比较
    auto it1 = a.begin();
    auto it2 = b.begin();

    while (it1 != a.end()) {
    // 将当前字符转换为小写后比较
    char c1 = std::tolower(static_cast<unsigned char>(*it1));
    char c2 = std::tolower(static_cast<unsigned char>(*it2));

    // 只要有一个字符不同,就返回 false
    if (c1 != c2) return false;

    ++it1;
    ++it2;
    }
    return true;
    }
    };

    // 使用示例
    int main() {
    CaseInsensitiveString s1("Hello");
    CaseInsensitiveString s2("HELLO"); // 与 s1 不区分大小写时等价
    CaseInsensitiveString s3("World");
    CaseInsensitiveString s4("hello world");

    // 使用 <=> 进行比较
    // 因为 "Hello" 和 "HELLO" 忽略大小写后完全相同
    // 所以返回 std::weak_ordering::equivalent
    auto res = s1 <=> s2;
    if (res == std::weak_ordering::equivalent) {
    std::cout << "s1 和 s2 在忽略大小写时等价" << '\\n';
    }

    // 弱序的核心性质:等价的对象不一定能互换
    // s1 和 s2 虽然是等价的,但它们存储的原始字符串不同
    // s1.str() 返回 "Hello"
    // s2.str() 返回 "HELLO"
    // 如果用 s2 替换 s1,程序输出的字符串大小写会不同
    std::cout << "s1.str() = " << s1.str() << '\\n'; // Hello
    std::cout << "s2.str() = " << s2.str() << '\\n'; // HELLO

    // 测试所有六种比较运算符
    std::cout << std::boolalpha; // 让 bool 值输出为 true/false
    std::cout << "s1 == s2: " << (s1 == s2) << '\\n'; // true(忽略大小写)
    std::cout << "s1 != s3: " << (s1 != s3) << '\\n'; // true
    std::cout << "s1 < s3: " << (s1 < s3) << '\\n'; // true(H < W)
    std::cout << "s3 > s1: " << (s3 > s1) << '\\n'; // true
    std::cout << "s1 <= s2: " << (s1 <= s2) << '\\n'; // true(等价)
    std::cout << "s1 >= s2: " << (s1 >= s2) << '\\n'; // true(等价)

    // 在容器中使用
    // 因为 CaseInsensitiveString 定义了 operator<=>
    // 所以它满足 std::sort 的要求:需要提供严格弱序(strict weak ordering)
    std::vector<CaseInsensitiveString> words = { s3, s1, s4, s2 };
    std::sort(words.begin(), words.end());

    std::cout << "排序后: ";
    for (const auto& word : words) {
    std::cout << word.str() << " | ";
    }
    std::cout << '\\n';

    return 0;
    }

    弱序的关键理解:弱序中的"等价"(equivalent)和强序中的"相等"(equal)是不同层次的概念。在弱序中,两个值等价意味着它们在比较标准下被认为是"一样的",但它们的实际内容可能不同。比如上面的例子中,"Hello" 和 "HELLO" 在不区分大小写的比较标准下是等价的,但它们的原始字符串内容不同。如果你用 s2 替换 s1,输出的大小写会发生变化,这就是"不可互换"的含义。


    2.4 std::partial_ordering(偏序)——允许"不可比较"

    头文件:<compare>

    官方参考:https://en.cppreference.com/w/cpp/utility/compare/partial_ordering

    定义:std::partial_ordering 是三种比较类别中最宽松的一种。它不仅允许"小于"、“等价”、"大于"三种结果,还允许第四种结果——“不可比较”(unordered)。这意味着在某些情况下,两个值之间不存在任何顺序关系。

    五个静态成员常量:

    常量名含义说明
    std::partial_ordering::less 小于 a < b
    std::partial_ordering::equivalent 等价 a ≈ b
    std::partial_ordering::greater 大于 a > b
    std::partial_ordering::unordered 不可比较 a 和 b 之间没有顺序关系

    适用场景:

    • 浮点数:当其中一个值为 NaN 时,任何比较结果都是 unordered。
    • 特殊数学概念:如复数的模长比较,两个复数可能不可比较。
    • 业务逻辑中的特殊状态:比如一个"无效"的对象和任何"有效"对象比较时,结果应该是 unordered。

    浮点数中的 NaN:NaN(Not a Number,非数)是 IEEE 754 浮点数标准中定义的一种特殊值,用于表示未定义或不可表示的数值结果。比如 0.0 / 0.0、sqrt(-1.0) 等操作的结果就是 NaN。NaN 一个非常反直觉的特性是:NaN 不等于任何值,包括它自己。NaN == NaN 的结果是 false。在 C++20 之前,用 < 比较 NaN 和其他浮点数时,结果是 false(因为 NaN 被认为不小于任何值),但这实际上是一种误导——!(a < b) && !(a > b) && !(a == b) 同时成立,这在数学上自相矛盾。<=> 的 unordered 结果就是为了正确处理这种情况而设计的。

    示例代码:

    #include <iostream>
    #include <compare>
    #include <cmath> // 提供 NAN 宏和 isnan 函数

    int main()
    {
    // 浮点数的 <=> 比较
    float a = 1.0f;
    float b = NAN; // 非数(Not a Number)

    // 1.0f <=> NaN 的结果是 std::partial_ordering::unordered
    auto result = a <=> b;
    if (result == std::partial_ordering::unordered) {
    std::cout << "1.0f 和 NaN 不可比较" << '\\n';
    }

    // 自定义 Temperature 类,演示偏序的实际应用
    struct Temperature {
    float value; // 温度值
    bool is_valid; // 是否有效(比如传感器是否正常工作)

    // 自定义 operator<=>
    // 比较规则:
    // 1. 如果其中一个温度无效,返回 unordered(不可比较)
    // 2. 如果两个都有效,比较实际的温度值
    std::partial_ordering operator<=>(const Temperature& other) const {
    // 如果当前温度或另一个温度无效,返回 unordered
    if (!is_valid || !other.is_valid) {
    return std::partial_ordering::unordered;
    }
    // 两个温度都有效,使用浮点数的 <=> 进行比较
    else {
    return value <=> other.value;
    }
    }
    };

    Temperature t1{ 36.5f, true }; // 有效温度 36.5°C
    Temperature t2{ 37.2f, true }; // 有效温度 37.2°C
    Temperature t3{ 0.0f, false }; // 无效温度(传感器故障)

    // 比较两个有效温度
    auto res1 = t1 <=> t2; // 返回 std::partial_ordering::less
    if (res1 < 0) {
    std::cout << "t1 温度低于 t2" << '\\n';
    }

    // 比较有效温度和无效温度
    auto res2 = t1 <=> t3; // 返回 std::partial_ordering::unordered
    if (res2 == std::partial_ordering::unordered) {
    std::cout << "无法比较有效温度与无效温度" << '\\n';
    }

    // 使用编译器生成的比较运算符
    // 注意:在偏序情况下,使用 < 或 > 时,如果结果是 unordered
    // 那么 a < b、a > b、a == b 都返回 false
    // 这是符合直觉的——既然不可比较,那当然既不小于也不大于也不等于
    if (t1 < t2) {
    std::cout << "t1 < t2" << '\\n'; // 输出
    }

    // 下面这个 if 不会输出,因为 unordered 状态下 < 返回 false
    if (t1 < t3) {
    std::cout << "t1 < t3(不会执行)" << '\\n';
    }

    return 0;
    }


    2.5 三种比较类别的对比总结

    特性std::strong_orderingstd::weak_orderingstd::partial_ordering
    严格程度 最严格 中等 最宽松
    可能的结果 less, equal/equivalent, greater less, equivalent, greater less, equivalent, greater, unordered
    等价的值是否可互换 是,完全不可区分 否,可能不同 视情况而定
    是否允许不可比较
    隐式转换目标 可转为 weak 和 partial 可转为 partial 不可转换(最底层)
    典型应用 整数、指针、字符 不区分大小写字符串 浮点数、NaN
    数学对应 全序关系(Total Order) 弱序关系(Weak Order) 偏序关系(Partial Order)

    std::common_comparison_category:在泛型编程中,有时候需要自动推导出多个类型的比较类别的"公共"类型。比如一个类模板的成员变量类型可能是 int(强序)和 double(偏序),那么整个类的 <=> 返回类型应该是 std::partial_ordering(向下兼容)。std::common_comparison_category 这个类型萃取工具就是为了解决这个问题而设计的,它接受多个比较类别类型,返回它们中最"弱"的那个。


    2.6 命名比较函数(Named Comparison Functions)

    头文件:<compare>

    官方参考:https://en.cppreference.com/w/cpp/utility/compare/named_comparison_functions

    C++20 在 <compare> 头文件中提供了一组命名比较函数,用于将 <=> 的返回结果转换为布尔值。这些函数的作用和直接跟 0 比较是一样的,但有些人觉得函数名更直观、可读性更好。

    六个命名比较函数:

    函数名等价表达式含义
    std::is_eq(cmp) cmp == 0 是否相等
    std::is_neq(cmp) cmp != 0 是否不相等
    std::is_lt(cmp) cmp < 0 是否小于
    std::is_lteq(cmp) cmp <= 0 是否小于等于
    std::is_gt(cmp) cmp > 0 是否大于
    std::is_gteq(cmp) cmp >= 0 是否大于等于

    函数签名(以 is_eq 为例):

    constexpr bool is_eq(std::partial_ordering cmp) noexcept;

    • 参数:std::partial_ordering 类型(因为 strong_ordering 和 weak_ordering 可以隐式转换为 partial_ordering,所以这个函数可以接受所有三种比较类别的结果)。
    • 返回值:bool 类型,表示比较结果是否满足对应的关系。
    • constexpr:可以在编译期执行。
    • noexcept:不会抛出异常。

    示例:

    #include <iostream>
    #include <compare>

    int main()
    {
    int a = 5, b = 3;
    auto cmp = a <=> b;

    // 使用命名比较函数判断结果
    // 等价于 cmp > 0
    if (std::is_gt(cmp)) {
    std::cout << "5 > 3" << '\\n';
    }

    // 等价于 cmp == 0
    if (std::is_eq(cmp)) {
    std::cout << "5 == 3(不会执行)" << '\\n';
    }

    // 等价于 cmp != 0
    if (std::is_neq(cmp)) {
    std::cout << "5 != 3" << '\\n';
    }

    return 0;
    }

    使用建议:命名比较函数和直接跟 0 比较这两种方式在功能上是完全等价的,选择哪一种主要看个人偏好。在大多数情况下,直接跟 0 比较更简洁,代码也更短。但在复杂的泛型代码中,命名比较函数可以让代码的意图更清晰。


    2.7 std::compare_three_way 函数对象

    头文件:<compare>

    官方参考:https://en.cppreference.com/w/cpp/utility/compare/compare_three_way

    std::compare_three_way 是 C++20 引入的一个函数对象,它的作用类似于 std::less、std::greater 等比较函数对象,但它是专门为 <=> 三路比较运算符设计的。

    类定义(简化):

    struct compare_three_way {
    template<class T, class U>
    constexpr auto operator()(T&& t, U&& u) const
    -> decltype(std::forward<T>(t) <=> std::forward<U>(u));
    };

    关键点:

    • 模板参数:接受两个任意类型的参数 T 和 U,只要它们支持 <=> 运算符。
    • 返回值:自动推导为 T 和 U 的 <=> 返回类型。
    • constexpr:可以在编译期使用。
    • 完美转发:使用 std::forward 保持参数的左右值属性。

    示例:

    #include <compare>
    #include <iostream>
    #include <algorithm>
    #include <string>
    #include <vector>

    int main() {
    // 创建一个 compare_three_way 函数对象
    std::compare_three_way cmp;

    // 对整数使用
    int a = 5, b = 3;
    // cmp(a, b) 等价于 a <=> b
    // 返回 std::strong_ordering::greater
    auto result1 = cmp(a, b);
    std::cout << "5 <=> 3: " << std::is_gt(result1) << '\\n'; // true

    // 对字符串使用
    std::string s1 = "apple", s2 = "banana";
    // cmp(s1, s2) 等价于 s1 <=> s2
    // 返回 std::strong_ordering::less
    auto result2 = cmp(s1, s2);
    std::cout << "apple <=> banana: " << std::is_lt(result2) << '\\n'; // true

    // 对浮点数使用
    double x = 1.5, y = 2.5;
    // 两个普通浮点数比较,返回 std::partial_ordering::less
    auto result3 = cmp(x, y);
    std::cout << "1.5 <=> 2.5: " << std::is_lt(result3) << '\\n'; // true

    // 使用 compare_three_way 进行排序
    std::vector<std::string> words = { "banana", "apple", "cherry", "date" };

    // 在排序中使用 compare_three_way
    // 注意:std::sort 需要的是 < 比较,所以我们用 cmp(a, b) < 0
    std::sort(words.begin(), words.end(),
    [](const auto& a, const auto& b) {
    return std::compare_three_way{}(a, b) < 0;
    });

    std::cout << "排序后: ";
    for (const auto& word : words) {
    std::cout << word << " ";
    }
    std::cout << '\\n';

    return 0;
    }

    compare_three_way 的优势:它提供了一个统一的接口来调用 <=> 运算符,在泛型编程中特别有用。比如,当编写一个模板函数,需要对不同类型的参数进行三路比较时,使用 std::compare_three_way 比直接写 a <=> b 更灵活,因为它可以处理更复杂的类型转换场景。


    第三章 std::format 格式化库全景解析

    嘿嘿大家,看到这个format,脑袋瓜其实立马就想到了python和spdlog哦,我们可是没少接触这个格式化~~,所以我们接下来学习C++中的这个知识点自然也就是水到渠成。

    3.1 为什么需要 std::format

    在 C++20 之前,C++ 中格式化字符串主要有两种方式:

    C 风格的 printf 系列:

    printf("Hello, %s! You are %d years old. Your salary is %.2f.\\n", name, age, salary);

    优点:简洁、性能好。缺点:类型不安全——printf("%s", 42) 编译时不会报错,但运行时崩溃。格式化字符串和参数类型必须在运行时完全匹配,否则是未定义行为。

    C++ 风格的 iostream 系列:

    std::cout << "Hello, " << name << "! You are " << age << " years old. Your salary is " << std::fixed << std::setprecision(2) << salary << "." << std::endl;

    优点:类型安全。缺点:语法冗长——为了格式化一个简单的句子,写了一大堆 << 操作符和操纵器(manipulator)。性能也较差,因为多次函数调用和难以优化的连续输出。

    std::format 的目标:兼具 printf 的性能和简洁性,以及 iostream 的类型安全性。它使用类似 Python 的格式化语法,{} 作为占位符,格式说明符放在 : 后面,直观且强大。


    3.2 std::format——最核心的格式化函数

    头文件:<format>

    官方参考:https://en.cppreference.com/w/cpp/utility/format/format

    函数签名:

    template<class... Args>
    std::string format(std::format_string<Args...> fmt, Args&&... args);

    参数详解:

    参数类型说明
    fmt std::format_string<Args…> 格式化字符串,包含 {} 占位符。这是一个编译期检查的字符串类型,如果格式字符串和参数类型不匹配,编译时会报错
    args… Args&&… 可变参数包,是要格式化的参数值。使用完美转发保持参数类型

    返回值:

    • 类型:std::string
    • 说明:返回格式化后的字符串。如果格式化失败(比如格式字符串错误),抛出 std::format_error 异常。

    基本用法示例:

    #include <format>
    #include <iostream>
    #include <string>
    #include <vector>
    #include <list>

    int main() {
    std::string name = "Alice";
    int age = 30;
    double salary = 85000.5;

    // 1. 默认位置参数(按顺序替换)
    // {} 的个数和参数个数必须匹配
    // 如果参数不够,编译期会报错
    auto str1 = std::format("Hello, {}! You are {} years old.", name, age);
    std::cout << str1 << '\\n';
    // 输出:Hello, Alice! You are 30 years old.

    // 2. 位置索引参数(可以指定顺序,重复使用)
    // {0} 表示第一个参数,{1} 表示第二个参数
    // 可以多次引用同一个参数
    auto str2 = std::format("Hello, {0}! {0} is {1} years old.", name, age);
    std::cout << str2 << '\\n';
    // 输出:Hello, Alice! Alice is 30 years old.

    // 3. 格式化说明(类似于 printf 的 %f, %.2f 等)
    // {:.2f} 表示保留两位小数的浮点数
    auto str3 = std::format("Salary: {:.2f}", salary);
    std::cout << str3 << '\\n';
    // 输出:Salary: 85000.50

    // 4. C++23 开始支持 STL 容器的格式化
    // 需要 C++23 编译器支持
    std::vector<int> v = { 1, 2, 3, 4 };
    std::list<double> lt = { 10.1, 10.2, 10.3 };
    // 以下代码需要 C++23 支持
    // auto str4 = std::format("vector: {}\\nlist: {}\\n", v, lt);
    // std::cout << str4 << '\\n';

    return 0;
    }

    位置索引的重要规则:不能混合使用显式索引和隐式索引。也就是说,要么全部使用 {}(隐式索引,按顺序匹配),要么全部使用 {0}、{1} 等(显式索引)。以下代码是错误的:

    // 错误:混合使用隐式索引和显式索引
    auto s3 = std::format("{} {1} {} {0}", "x", "y"); // 编译错误!


    3.3 格式化说明符(Format Specifiers)深度解析

    std::format 的格式化说明符是其最强大的部分。占位符的基本结构如下:

    {[arg_id][:format_spec]}

    其中 format_spec 的完整语法是:

    [[fill]align][sign][#][0][width][.precision][type]

    下面逐一解析每个组件。

    3.3.1 填充和对齐(Fill and Align)

    语法:[fill]align

    其中 align 必须是以下三个选项之一:

    对齐选项含义说明
    < 左对齐 内容在字段左侧,右侧填充
    > 右对齐 内容在字段右侧,左侧填充(默认行为)
    ^ 居中对齐 内容在字段中间,两侧均匀填充

    fill 是可选的自定义填充字符,默认是空格。如果指定了填充字符,则必须同时指定对齐方式。

    示例:

    #include <format>
    #include <iostream>

    int main() {
    // 基本对齐
    // 指定宽度为 10,分别使用左对齐、右对齐、居中对齐
    std::cout << std::format("|{:<10}|", "left") << '\\n';
    // 输出:|left |
    std::cout << std::format("|{:>10}|", "right") << '\\n';
    // 输出:| right|
    std::cout << std::format("|{:^10}|", "center") << '\\n';
    // 输出:| center |

    // 自定义填充字符
    // 在冒号后面、对齐符号前面指定填充字符
    std::cout << std::format("|{:*<10}|", "left") << '\\n';
    // 输出:|left******|
    std::cout << std::format("|{:_>10}|", "right") << '\\n';
    // 输出:|_____right|
    std::cout << std::format("|{:-^10}|", "center") << '\\n';
    // 输出:|–center–|

    // 数字对齐
    // 数字默认右对齐
    std::cout << std::format("|{:0>5}|", 42) << '\\n';
    // 输出:|00042|
    std::cout << std::format("|{:*<5}|", 42) << '\\n';
    // 输出:|42***|

    return 0;
    }

    3.3.2 符号控制(Sign)

    语法:[sign]

    符号选项含义示例
    + 总是显示符号 正数显示 +,负数显示 –
    仅显示负号 正数不显示符号,负数显示 -(默认行为)
    (空格) 正数前加空格,负数前加负号 正数前加一个空格,负数前加 –

    示例:

    #include <format>
    #include <iostream>

    int main() {
    int positive = 42;
    int negative = 42;

    // 默认行为:只显示负号
    std::cout << std::format("默认: {} {}\\n", positive, negative);
    // 输出:默认: 42 -42

    // 总是显示符号
    std::cout << std::format("总是显示符号: {:+} {:+}\\n", positive, negative);
    // 输出:总是显示符号: +42 -42

    // 空格模式:正数前加空格,负数前加负号
    std::cout << std::format("空格模式: {: } {: }\\n", positive, negative);
    // 输出:空格模式: 42 -42

    // 浮点数符号控制
    double fp_positive = 3.14;
    double fp_negative = 2.71;
    std::cout << std::format("浮点数: {:+} {:+}\\n", fp_positive, fp_negative);
    // 输出:浮点数: +3.14 -2.71

    return 0;
    }

    3.3.3 替代形式(#)

    语法:#

    # 标志的作用是请求"替代形式"的输出,对于不同的类型有不同的效果:

    类型# 的效果示例
    整数(b、o、x、X) 显示进制前缀 0b、0、0x、0X
    浮点数(f、e、E、g、G) 即使没有小数部分也显示小数点 10. 而不是 10

    示例:

    #include <format>
    #include <iostream>

    int main() {
    int decimal = 42;
    int hex = 255;
    double fp = 10.0;

    // 整数:显示进制前缀
    std::cout << std::format("十六进制: {:#x}\\n", hex);
    // 输出:十六进制: 0xff
    std::cout << std::format("八进制: {:#o}\\n", decimal);
    // 输出:八进制: 052
    std::cout << std::format("二进制: {:#b}\\n", decimal);
    // 输出:二进制: 0b101010

    // 浮点数:强制显示小数点
    std::cout << std::format("浮点数: {:#.0f}\\n", fp);
    // 输出:浮点数: 10.
    std::cout << std::format("科学计数: {:#.0e}\\n", fp);
    // 输出:科学计数: 1.e+01

    return 0;
    }

    3.3.4 零填充(0)

    语法:0

    0 标志相当于设置了填充字符为 0、对齐方式为右对齐。它只对数字类型有效。

    示例:

    #include <format>
    #include <iostream>

    int main() {
    int num = 42;

    // 宽度为 6,用 0 填充
    std::cout << std::format("|{:06}|\\n", num);
    // 输出:|000042|

    // 宽度为 8,用 0 填充
    std::cout << std::format("|{:08}|\\n", num);
    // 输出:|00000042|

    // 浮点数零填充
    std::cout << std::format("|{:08.2f}|\\n", 3.14);
    // 输出:|00003.14|

    return 0;
    }

    3.3.5 宽度(Width)

    语法:[width]

    宽度指定字段的最小宽度。如果内容的实际长度小于宽度,会根据对齐方式在两侧填充。如果内容的实际长度大于宽度,宽度设置会被忽略,内容不会被截断。

    动态宽度:宽度可以用嵌套的 {} 来指定,从参数列表中动态获取。

    示例:

    #include <format>
    #include <iostream>

    int main() {
    std::string text = "hello";
    int number = 123;

    // 固定宽度
    std::cout << std::format("|{:10}|\\n", text);
    // 输出:|hello |
    std::cout << std::format("|{:10}|\\n", number);
    // 输出:| 123|

    // 动态宽度(使用嵌套参数)
    // {:{}} 中,第一个 {} 是内容,第二个 {} 是宽度值
    int width = 15;
    std::cout << std::format("|{:{}}|\\n", text, width);
    // 输出:|hello |
    std::cout << std::format("|{:{}}|\\n", number, width);
    // 输出:| 123|

    // 结合对齐
    std::cout << std::format("|{:>10}|\\n", text);
    // 输出:| hello|
    std::cout << std::format("|{:<10}|\\n", number);
    // 输出:|123 |

    return 0;
    }

    3.3.6 精度(Precision)

    语法:[.precision]

    精度对于不同类型的含义不同:

    类型精度的含义
    浮点数(f、F、e、E) 小数点后的位数
    浮点数(g、G) 有效数字的位数
    字符串(s) 输出的最大字符数(截断)
    整数 精度无效,会被忽略

    示例:

    #include <format>
    #include <iostream>

    int main() {
    double pi = 3.141592653589793;

    // 默认浮点数输出
    std::cout << std::format("默认: {}\\n", pi);
    // 输出:默认: 3.141592653589793

    // 保留两位小数
    std::cout << std::format("两位小数: {:.2f}\\n", pi);
    // 输出:两位小数: 3.14

    // 保留五位小数
    std::cout << std::format("五位小数: {:.5f}\\n", pi);
    // 输出:五位小数: 3.14159

    // 科学计数法精度
    std::cout << std::format("科学计数: {:.2e}\\n", pi);
    // 输出:科学计数: 3.14e+00

    // 字符串精度(最大字符数,从开头截断)
    std::string long_text = "Hello, World!";
    std::cout << std::format("截断: {:.5}\\n", long_text);
    // 输出:截断: Hello

    // 通用格式,保留几位有效数字
    std::cout << std::format("通用: {:.4g}\\n", 123.456);
    // 输出:通用: 123.5
    std::cout << std::format("通用: {:.3g}\\n", 0.00123);
    // 输出:通用: 0.00123

    return 0;
    }

    3.3.7 类型说明符(Type)

    语法:[type]

    类型说明符指定如何解释和格式化数据。不同类别的类型有不同的可用选项。

    整数类型说明符:

    类型含义示例(num=42)
    d 十进制(默认) 42
    b 二进制 101010
    B 二进制(大写前缀) 101010(配合 # 为 0B101010)
    o 八进制 52
    x 十六进制(小写) 2a
    X 十六进制(大写) 2A
    c 输出为字符 *(如果 num=42)

    浮点数类型说明符:

    类型含义示例(value=123.456789)
    f、F 定点表示法 123.456789
    e 科学计数法(小写 e) 1.234568e+02
    E 科学计数法(大写 E) 1.234568E+02
    g 通用格式(自动选择 f 或 e,取较短者,默认) 123.457
    G 通用格式(大写 E 版本) 123.457
    a 十六进制浮点数表示(小写) 0x1.edd3c07fb4b69p+6
    A 十六进制浮点数表示(大写) 0X1.EDD3C07FB4B69P+6

    字符和字符串类型说明符:

    类型含义示例
    默认输出 字符原样输出
    d 输出字符的 ASCII 值 'A' 输出为 65
    x、X 输出字符的十六进制值 'A' 输出为 41

    布尔类型说明符:

    类型含义示例(flag=true)
    输出 true 或 false true
    s 输出 true 或 false(字符串形式) true
    d 输出数字形式 1

    示例:

    #include <format>
    #include <iostream>

    int main() {
    int num = 42;

    // 整数类型
    std::cout << std::format("十进制: {}\\n", num);
    // 输出:十进制: 42
    std::cout << std::format("二进制: {:b}\\n", num);
    // 输出:二进制: 101010
    std::cout << std::format("二进制(大写): {:#B}\\n", num);
    // 输出:二进制(大写): 0B101010
    std::cout << std::format("八进制: {:o}\\n", num);
    // 输出:八进制: 52
    std::cout << std::format("十六进制(小写): {:x}\\n", num);
    // 输出:十六进制(小写): 2a
    std::cout << std::format("十六进制(大写): {:X}\\n", num);
    // 输出:十六进制(大写): 2A

    // 浮点数类型
    double value = 123.456789;
    std::cout << std::format("默认: {}\\n", value);
    // 输出:默认: 123.456789
    std::cout << std::format("定点: {:f}\\n", value);
    // 输出:定点: 123.456789
    std::cout << std::format("科学: {:e}\\n", value);
    // 输出:科学: 1.234568e+02
    std::cout << std::format("通用: {:g}\\n", value);
    // 输出:通用: 123.457
    std::cout << std::format("十六进制浮点: {:a}\\n", value);
    // 输出:十六进制浮点: 0x1.edd3c07fb4b69p+6

    // 大写版本
    std::cout << std::format("科学(大写): {:E}\\n", value);
    // 输出:科学(大写): 1.234568E+02
    std::cout << std::format("十六进制浮点(大写): {:A}\\n", value);
    // 输出:十六进制浮点(大写): 0X1.EDD3C07FB4B69P+6

    // 字符和字符串
    char ch = 'A';
    const char* cstr = "Hello";
    std::string str = "World";
    std::cout << std::format("字符: {}\\n", ch);
    std::cout << std::format("C字符串: {}\\n", cstr);
    std::cout << std::format("字符串: {}\\n", str);
    std::cout << std::format("字符ASCII: {:d}\\n", ch); // 65
    std::cout << std::format("字符十六进制: {:x}\\n", ch); // 41

    // 布尔类型
    bool flag = true;
    std::cout << std::format("默认: {}\\n", flag); // true
    std::cout << std::format("数字: {:d}\\n", flag); // 1
    std::cout << std::format("字符串: {:s}\\n", flag); // true

    return 0;
    }


    3.4 format_to 和 format_to_n——直接写入缓冲区

    3.4.1 std::format_to

    函数签名:

    template<class OutputIt, class... Args>
    OutputIt format_to(OutputIt out, std::format_string<Args...> fmt, Args&&... args);

    参数详解:

    参数类型说明
    out OutputIt 输出迭代器,指向要写入的目标位置。可以是任何满足输出迭代器要求的类型
    fmt std::format_string<Args…> 格式化字符串,同 std::format
    args… Args&&… 可变参数包,要格式化的参数值

    返回值:

    • 类型:OutputIt(即传入的迭代器类型)
    • 说明:返回指向最后一个写入字符之后的迭代器。这个返回值可以用来计算实际写入的字符数,或者继续进行后续写入。

    与 std::format 的区别:std::format 返回一个 std::string,需要分配内存并拷贝字符串。std::format_to 直接将结果写入已有的缓冲区,避免了不必要的内存分配和拷贝,性能更好。在需要频繁格式化输出的高性能场景中,format_to 是更好的选择。

    使用示例:

    #include <format>
    #include <string>
    #include <vector>
    #include <iostream>
    #include <iterator> // std::back_inserter

    int main() {
    // 方法一:使用已分配空间的 string 的迭代器
    std::string str1;
    // 预先分配足够的空间
    // 可以使用 formatted_size 预先计算需要的空间大小
    str1.resize(100);

    // format_to 将格式化结果写入 str1 的开头
    // 返回值 iter 指向最后一个写入字符之后的位置
    auto iter = std::format_to(str1.begin(), "Hello, {}! The answer is {}.", "World", 42);

    // 计算实际写入的字符数,并调整 string 大小
    // iter – str1.begin() 就是实际写入的字符数
    str1.resize(iter str1.begin());
    std::cout << str1 << '\\n';
    // 输出:Hello, World! The answer is 42.

    // 方法二:使用 std::back_inserter(自动扩容)
    // std::back_inserter 创建一个输出迭代器,每次赋值时自动调用 push_back
    // 这样就不需要手动分配空间了
    std::string str2;
    // format_to 会自动将字符一个个追加到 str2 的末尾
    std::format_to(std::back_inserter(str2), "Hello, {}! The answer is {}.", "World", 42);
    std::cout << str2 << '\\n';
    // 输出:Hello, World! The answer is 42.

    // 方法三:直接输出到标准输出流
    // std::ostream_iterator 包装了 std::cout
    std::format_to(std::ostream_iterator<char>(std::cout),
    "Hello, {}! The answer is {}.\\n", "World", 42);
    // 输出:Hello, World! The answer is 42.

    // std::back_inserter 的原理演示
    std::vector<int> vec;
    auto inserter = std::back_inserter(vec);
    *inserter = 10; // 底层调用 vec.push_back(10)
    *inserter = 20; // 底层调用 vec.push_back(20)
    *inserter = 30; // 底层调用 vec.push_back(30)
    for (int x : vec) {
    std::cout << x << " "; // 输出:10 20 30
    }
    std::cout << '\\n';

    return 0;
    }

    3.4.2 std::format_to_n

    函数签名:

    template<class OutputIt, class... Args>
    format_to_n_result<OutputIt> format_to_n(OutputIt out, std::size_t n,
    std::format_string<Args...> fmt,
    Args&&... args);

    参数详解:

    参数类型说明
    out OutputIt 输出迭代器,指向要写入的目标位置
    n std::size_t 最大输出字符数
    fmt std::format_string<Args…> 格式化字符串
    args… Args&&… 可变参数包

    返回值:

    • 类型:std::format_to_n_result<OutputIt>,这是一个结构体,包含两个成员:
      • out:OutputIt 类型,指向实际写入的最后一个字符之后的位置
      • size:std::ptrdiff_t 类型,如果输出没有被截断,则是格式化后的完整字符数;如果被截断了,则是 std::format_to_n_result::truncation_counter(一个特殊值,表示截断)

    用途:在需要限制输出长度的场景中使用,防止缓冲区溢出。比如在写入固定大小的缓冲区时,或者需要确保输出不超过某个限制时。

    使用示例:

    #include <format>
    #include <string>
    #include <iostream>

    int main() {
    std::string str3;
    // 预先分配固定大小的缓冲区
    str3.resize(20);

    // 限制最大输出为 20 个字符
    // 如果格式化结果超过 20 个字符,多余的部分会被截断
    auto result = std::format_to_n(str3.begin(), 20,
    "Hello, {}! The answer is {}.", "World", 42);

    // 输出被截断的字符串(最多 20 个字符)
    std::cout << str3 << '\\n';
    // 输出:Hello, World! The

    // 可以通过 result.size 判断是否被截断
    // 如果 result.size > 20,说明被截断了
    std::cout << "完整长度: " << result.size << '\\n';

    return 0;
    }

    3.4.3 std::formatted_size

    函数签名:

    template<class... Args>
    std::size_t formatted_size(std::format_string<Args...> fmt, Args&&... args);

    参数详解:

    参数类型说明
    fmt std::format_string<Args…> 格式化字符串
    args… Args&&… 可变参数包

    返回值:

    • 类型:std::size_t
    • 说明:返回格式化后的字符串长度(字符数),不包含结尾的空字符('\\0')。这个函数不会实际生成格式化字符串,只是计算长度。

    用途:在需要预先分配缓冲区大小的场景中非常有用。比如先用 formatted_size 计算需要的空间,然后分配空间,再用 format_to 写入。

    使用示例:

    #include <format>
    #include <string>
    #include <iostream>

    int main() {
    // 计算格式化后的字符串长度
    // 注意:这个函数不会实际生成字符串,只是计算长度
    std::size_t size = std::formatted_size("Hello, {}! The answer is {}.", "World", 42);

    std::cout << "需要的字符数: " << size << '\\n';
    // 输出:需要的字符数: 28

    // 使用 formatted_size 预先分配空间
    std::string str;
    str.resize(size);

    // 然后使用 format_to 写入
    std::format_to(str.begin(), "Hello, {}! The answer is {}.", "World", 42);
    std::cout << str << '\\n';
    // 输出:Hello, World! The answer is 42.

    return 0;
    }


    3.5 vformat 和 make_format_args——参数打包与转发

    3.5.1 std::vformat

    函数签名:

    std::string vformat(std::string_view fmt, std::format_args args);

    参数详解:

    参数类型说明
    fmt std::string_view 格式化字符串,注意这里是 string_view 而不是 format_string,所以不会在编译期检查格式字符串的正确性
    args std::format_args 打包后的格式化参数,通过 std::make_format_args 创建

    返回值:

    • 类型:std::string
    • 说明:返回格式化后的字符串,同 std::format。

    与 std::format 的区别:std::format 接受可变参数模板,直接在调用点展开参数。std::vformat 接受已经打包好的 std::format_args,适合在需要将格式化参数作为参数传递的场景中使用。比如在实现日志库时,你可能需要将格式化字符串和参数包一起传递,这时 vformat 就派上了用场。

    3.5.2 std::make_format_args

    函数签名:

    template<class... Args>
    format_args make_format_args(Args&... args);

    参数详解:

    参数类型说明
    args… Args&… 要格式化的参数,注意是左值引用,不能是右值

    返回值:

    • 类型:std::format_args(实际上是 std::basic_format_args<std::format_context> 的别名)
    • 说明:返回一个包含参数引用的打包对象。

    重要注意事项:

  • 参数必须是左值:std::make_format_args 的参数是左值引用,不能传递右值。这是因为 make_format_args 存储的是参数的引用,而不是拷贝。如果允许右值,会导致悬空引用——右值在表达式结束后就被销毁了,但 format_args 还持有指向它的引用。

  • 生命周期管理:format_args 对象的生命周期不能超过它引用的参数的生命周期。也就是说,在 format_args 被使用期间,原始参数必须保持有效。

  • 示例:

    #include <format>
    #include <iostream>
    #include <string_view>

    int main() {
    std::string_view format_string = "Hello, {}! The answer is {}.";

    // 注意:make_format_args 的参数必须是左值
    // 不能写 std::make_format_args("hello world", 42) 这种形式
    // 因为字符串字面量和整数字面量是右值
    const char* str = "hello world"; // 左值
    int x = 42; // 左值

    // 将参数打包
    auto args = std::make_format_args(str, x);

    // 使用打包的参数进行格式化
    std::string result = std::vformat(format_string, args);
    std::cout << result << '\\n';
    // 输出:Hello, hello world! The answer is 42.

    return 0;
    }

    3.5.3 使用 vformat 实现日志库

    vformat 的典型应用场景是实现日志库,因为日志库通常需要接受格式化字符串和可变参数,并将它们传递给底层的格式化函数:

    #include <format>
    #include <iostream>
    #include <string_view>

    // 使用 vformat 实现一个简单的日志库
    class Logger {
    public:
    // 日志级别枚举
    enum class Level { Debug, Info, Warning, Error };

    // 核心日志函数
    // 接受格式化字符串和可变参数包
    template<typename... Args>
    void log(Level level, std::string_view fmt, Args&&... args) {
    // 将参数包打包为 format_args,然后传递给 vformat
    // 注意:这里使用 args… 而不是 std::forward<Args>(args)…
    // 因为 make_format_args 需要左值
    auto formatted_msg = std::vformat(fmt, std::make_format_args(args...));
    output_message(level, formatted_msg);
    }

    // 便捷函数:各个日志级别
    template<typename... Args>
    void debug(std::string_view fmt, Args&&... args) {
    log(Level::Debug, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void info(std::string_view fmt, Args&&... args) {
    log(Level::Info, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void warning(std::string_view fmt, Args&&... args) {
    log(Level::Warning, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void error(std::string_view fmt, Args&&... args) {
    log(Level::Error, fmt, std::forward<Args>(args)...);
    }

    private:
    void output_message(Level level, const std::string& msg) {
    // 将日志级别转换为字符串
    const char* level_str = "";
    switch (level) {
    case Level::Debug: level_str = "DEBUG"; break;
    case Level::Info: level_str = "INFO"; break;
    case Level::Warning: level_str = "WARNING"; break;
    case Level::Error: level_str = "ERROR"; break;
    }

    // 使用 std::format 输出格式化后的日志
    // 包含时间戳(这里简化为固定格式)
    std::cout << std::format("[{}] {}\\n", level_str, msg);
    }
    };

    int main() {
    Logger logger;

    // 调试日志
    logger.debug("This is a debug message");
    // 输出:[DEBUG] This is a debug message

    // 错误日志,带参数
    logger.error("User '{}' failed to access resource {}. Retry count: {}",
    "Bob", "/api/data", 3);
    // 输出:[ERROR] User 'Bob' failed to access resource /api/data. Retry count: 3

    return 0;
    }

    3.5.4 使用 vformat 实现延迟格式化

    有时候,我们希望在稍后的时间点再执行格式化,而不是立即格式化。比如在性能关键路径上,日志消息可能不会被输出(日志级别过滤),我们希望避免不必要的格式化开销。这种情况下,可以使用 vformat 结合 std::function 实现延迟格式化:

    #include <format>
    #include <vector>
    #include <functional>
    #include <iostream>

    // 延迟格式化类:将格式化操作推迟到执行时
    class DeferredFormatter {
    private:
    // 存储一系列可调用对象,每个对象执行格式化操作
    // 使用 std::function<std::string()> 来存储 lambda 表达式
    std::vector<std::function<std::string()>> formatters_;

    public:
    // 添加一个延迟格式化条目
    // 参数:格式化字符串和参数包
    // 注意:这里按值捕获参数,确保 lambda 拥有参数的所有权
    template<typename... Args>
    void add_formatter(std::string_view fmt, Args&&... args) {
    // emplace_back 直接在 vector 中构造 lambda
    // 按值捕获 fmt 和 args…,这样 lambda 拥有自己的数据副本
    // 在调用时执行 std::vformat
    formatters_.emplace_back([=]() -> std::string {
    return std::vformat(fmt, std::make_format_args(args...));
    });
    }

    // 执行所有延迟的格式化操作
    void execute_all() {
    for (const auto& formatter : formatters_) {
    std::cout << formatter() << '\\n';
    }
    }
    };

    int main() {
    DeferredFormatter deferred;

    // 添加格式化条目,但此时并不执行格式化
    deferred.add_formatter("Initial: {}, {}", 100, "张三");
    deferred.add_formatter("Updated: {}, {}", 200, "李四");

    // 在后续的某个时间点,统一执行所有格式化
    deferred.execute_all();
    // 输出:
    // Initial: 100, 张三
    // Updated: 200, 李四

    return 0;
    }


    3.6 std::formatter——为自定义类型提供格式化支持

    3.6.1 基本概念

    std::formatter 是一个模板类,需要特化来支持自定义类型的格式化。特化 std::formatter 需要实现两个核心函数:

    • parse():解析格式说明符(: 后面的部分)。这个函数在编译期执行。
    • format():执行实际的格式化操作。这个函数在运行时执行。

    基本结构:

    template<>
    struct std::formatter<Type> {
    // 解析格式说明符
    // 参数:ctx 是格式解析上下文
    // 返回值:指向格式说明符结束位置的迭代器
    constexpr auto parse(std::format_parse_context& ctx) {
    // … 解析逻辑
    return ctx.begin(); // 返回解析结束位置
    }

    // 格式化函数
    // 参数:value 是要格式化的值,ctx 是格式化上下文
    // 返回值:指向输出结束位置的迭代器
    auto format(const Type& value, std::format_context& ctx) const {
    // … 格式化逻辑
    return std::format_to(ctx.out(), "…");
    }
    };

    3.6.2 为 Point 类型实现格式化

    #include <iostream>
    #include <format>

    struct Point {
    double x, y;
    };

    // 特化 std::formatter 对于 Point 类型
    // 基础版本:不接受任何格式选项,直接输出 (x, y)
    template<>
    struct std::formatter<Point> {
    // parse 函数:解析格式说明符
    // 这个基础版本不接受任何格式选项
    // 所以直接返回 ctx.begin(),表示不解析任何内容
    constexpr auto parse(std::format_parse_context& ctx) {
    // 返回指向当前解析位置的迭代器
    // 如果格式说明符为空,ctx.begin() 指向 } 或字符串末尾
    return ctx.begin();
    }

    // format 函数:执行实际的格式化
    // 参数 p 是要格式化的 Point 对象
    // 参数 ctx 是格式化上下文,包含输出迭代器等信息
    auto format(const Point& p, std::format_context& ctx) const {
    // 使用 std::format_to 将格式化结果输出到 ctx.out()
    // ctx.out() 返回一个输出迭代器,指向要写入的位置
    return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
    }
    };

    // 使用示例
    int main() {
    Point p{3.14, 2.71};

    // 直接使用 std::format 格式化 Point
    auto str = std::format("Point: {}", p);
    std::cout << str << '\\n';
    // 输出:Point: (3.14, 2.71)

    return 0;
    }

    3.6.3 带自定义格式说明符的 formatter——Person 类

    更复杂的场景是支持自定义格式选项。比如,让 Person 类支持 s(简短)、l(详细)、j(JSON)三种格式:

    #include <format>
    #include <iostream>
    #include <string>

    struct Person {
    std::string name;
    int age;
    std::string city;
    };

    // 特化 std::formatter 对于 Person 类型
    // 支持自定义格式选项:'s' = 简短, 'l' = 详细, 'j' = JSON
    template<>
    struct std::formatter<Person> {
    // 格式选项:默认使用简短格式
    char presentation_type = 's';

    // parse 函数:解析格式说明符
    // 在编译期执行,用于解析 : 后面的格式选项
    constexpr auto parse(std::format_parse_context& ctx) {
    // 获取当前解析位置的迭代器
    auto it = ctx.begin();

    // 如果到达结尾或者遇到 },说明没有格式说明符
    // 使用默认的 's'(简短格式)
    if (it == ctx.end() || *it == '}') {
    return it;
    }

    // 解析单个字符的格式选项
    switch (*it) {
    case 's': // 简短格式:只显示名字
    presentation_type = 's';
    ++it;
    break;
    case 'l': // 详细格式:名字 (年龄) – 城市
    presentation_type = 'l';
    ++it;
    break;
    case 'j': // JSON 格式
    presentation_type = 'j';
    ++it;
    break;
    default:
    // 如果遇到不认识的格式说明符,抛出异常
    throw std::format_error("无效的格式说明符");
    }

    // 检查格式说明符是否已结束
    // 解析完一个字符后,下一个字符应该是 }
    if (it != ctx.end() && *it != '}') {
    throw std::format_error("格式说明符应以 } 结束");
    }

    return it;
    }

    // format 函数:执行实际的格式化
    // 根据 presentation_type 选择不同的格式
    auto format(const Person& p, std::format_context& ctx) const {
    switch (presentation_type) {
    case 's': // 简短格式:只显示名字
    return std::format_to(ctx.out(), "{}", p.name);

    case 'l': // 详细格式:名字 (年龄) – 城市
    return std::format_to(ctx.out(), "{} ({}) – {}",
    p.name, p.age, p.city);

    case 'j': // JSON 格式
    // 注意:使用 R"(…)" 原始字符串字面量
    // 内部的引号不需要转义
    // 但需要将 { 和 } 写为 {{ 和 }} 来转义
    return std::format_to(ctx.out(),
    R"({{"name":"{}","age":{},"city":"{}"}})",
    p.name, p.age, p.city);

    default:
    // 理论上不会执行,因为 parse 已经验证过
    return std::format_to(ctx.out(), "{}", p.name);
    }
    }
    };

    // 使用示例
    int main() {
    Person p{"Alice", 30, "Beijing"};

    // 默认格式(简短)
    std::cout << std::format("默认: {}\\n", p);
    // 输出:默认: Alice

    // 简短格式
    std::cout << std::format("简短: {:s}\\n", p);
    // 输出:简短: Alice

    // 详细格式
    std::cout << std::format("详细: {:l}\\n", p);
    // 输出:详细: Alice (30) – Beijing

    // JSON 格式
    std::cout << std::format("JSON: {:j}\\n", p);
    // 输出:JSON: {"name":"Alice","age":30,"city":"Beijing"}

    return 0;
    }


    3.7 C++23 的 std::print 和 std::println

    虽然 C++23 已经发布,但 std::print 和 std::println 是建立在 std::format 基础上的,作为 std::format 生态的补充,这里简单介绍一下。

    头文件:<print>

    函数签名:

    template<class... Args>
    void print(std::format_string<Args...> fmt, Args&&... args);

    template<class... Args>
    void println(std::format_string<Args...> fmt, Args&&... args);

    说明:std::print 和 std::println 本质上就是 std::format + std::cout 的快捷方式。std::println 会在末尾自动添加换行符。它们结合了 std::format 的类型安全性和 printf 的简洁性。

    使用示例:

    #include <print> // C++23 新头文件

    int main() {
    std::string name = "Bob";
    int age = 25;

    // 使用 std::print 输出(不自动换行)
    std::print("Hello, {}!", name); // 输出:Hello, Bob!

    // 使用 std::println 输出(自动换行)
    std::println("You are {} years old.", age); // 输出:You are 25 years old. (然后换行)

    // 相当于:
    std::cout << std::format("You are {} years old.\\n", age);

    return 0;
    }


    第四章 最佳实践与常见陷阱

    4.1 三路比较运算符的最佳实践

    1. 优先使用 = default:如果类的比较逻辑是简单的逐成员比较,总是优先使用 = default。这不仅减少了代码量,还保证了比较逻辑的一致性。

    2. 手动实现 <=> 时要注意返回类型:如果类中包含浮点数成员,= default 的 <=> 返回类型会自动推导为 std::partial_ordering。如果这不是你想要的(比如你确定 NaN 不会出现),可以手动实现并返回 std::strong_ordering。

    3. 不要忘记 operator==:当手动实现 operator<=> 时,编译器不会自动生成 operator==。需要显式定义 operator==,或者使用 = default 生成。

    4. 注意 operator<=> 的性能:<=> 的默认实现是逐成员比较,对于大型对象(如包含大量成员的类),性能可能不如手写优化版本。在性能关键路径上,可以考虑手动优化。

    5. 使用 std::compare_three_way 进行泛型编程:在模板代码中,std::compare_three_way 提供了一个统一的接口,可以处理各种类型的 <=> 比较。

    4.2 std::format 的最佳实践

    1. 优先使用 std::format 而不是 printf 或 iostream:std::format 兼具类型安全、性能好、语法简洁的优点,是 C++20 中格式化字符串的首选方式。

    2. 在性能敏感场景中使用 format_to:format_to 避免了 std::string 的创建和拷贝,直接写入缓冲区,性能更好。

    3. 使用 formatted_size 预先分配缓冲区:当需要精确控制内存分配时,先用 formatted_size 计算大小,再分配缓冲区,最后用 format_to 写入。

    4. 为自定义类型实现 std::formatter:如果自定义类型需要频繁进行格式化输出,实现 std::formatter 特化是值得的,这可以让你的类型无缝集成到 std::format 生态中。

    5. 注意 std::make_format_args 的参数生命周期:确保 format_args 对象的生命周期不超过它引用的参数的生命周期。

    4.3 常见陷阱

    陷阱 1:<=> 返回类型与 int 混淆

    auto result = a <=> b;
    // 错误:result == 1 是编译错误
    // if (result == 1) { … }
    // 正确:比较的是关系,而不是数值
    if (result > 0) { ... } // 或 result == std::strong_ordering::greater

    陷阱 2:混合使用隐式和显式索引

    // 错误:混合使用隐式索引 {} 和显式索引 {1}
    // auto s = std::format("{} {1} {}", "a", "b"); // 编译错误!
    // 正确:全部使用显式索引
    auto s = std::format("{0} {1} {0}", "a", "b"); // 输出:"a b a"

    陷阱 3:std::make_format_args 传递右值

    // 错误:make_format_args 不接受右值
    // auto args = std::make_format_args(42, "hello"); // 编译错误!
    // 正确:使用左值
    int x = 42;
    const char* str = "hello";
    auto args = std::make_format_args(x, str);

    陷阱 4:浮点数 NaN 的比较

    double nan = std::numeric_limits<double>::quiet_NaN();
    double val = 1.0;

    // 在 C++17 及之前:nan < val 返回 false,nan > val 返回 false,nan == val 返回 false
    // 所有三个都比较完了都是 false,容易误导
    // 在 C++20 中:(nan <=> val) == std::partial_ordering::unordered
    // 明确表示"不可比较",更符合数学直觉

    陷阱 5:std::formatter 的 parse 函数中忘记校验

    // 在 parse 函数中,一定要校验格式说明符的合法性
    // 如果遇到不认识的格式说明符,应该抛出 std::format_error
    // 而不是默默地忽略它


    总结

    C++20 的三路比较运算符 <=> 和 std::format 格式化库,是 C++ 标准库中两个看似简单、实则影响深远的特性。<=> 从根本上改变了 C++ 中比较操作的实现方式,用一个运算符替代了原来需要六个运算符才能完成的工作,同时通过三种比较类型(strong_ordering、weak_ordering、partial_ordering)提供了精确的比较语义控制。std::format 则提供了一个现代、类型安全、高性能的文本格式化方案,统一了 printf 的简洁性和 iostream 的类型安全性。

    从 <=> 的三种比较类别到 std::format 的完整格式说明符体系,从 = default 的自动生成到 std::formatter 的自定义类型扩展,这两个特性体现了 C++20 设计的核心理念:让编译器做更多的工作,让开发者写更少的代码,同时提供更强的类型安全和更清晰的语义表达。

    在实际开发中,建议将这两个特性作为默认选择——用 <=> 替代传统的比较运算符重载,用 std::format 替代 printf 和 iostream 的格式化输出场景。这不仅能减少代码量、提高可维护性,还能避免很多常见的编程错误。

    赞(0)
    未经允许不得转载:171主机测评 » 对于 C++:C++20中三路比较运算符与格式化库的解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址