Cpp中新时代格式化输出方法


概述

别的语言早就有的东西,积重难返的C++总是晚了很多年才有,在这之前搞了很多格式化方案,直到C++20才大刀阔斧改掉。

Formatting

最基础的格式化函数 format 开始,其定义是这样:

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

第一个参数是格式化字符串,描述文本格式,后续参数就是需要被格式化的其他参数。C++20 在 C++11 的基础上,为 chrono 库提供了完善的 format 支持,我们再也不需要使用旧的 C 风格时间格式化函数了。

// 使用 std::chrono 来打印日志的时间
using TimePoint = std::chrono::time_point;

这里简单说明一下 format 的格式化字符串格式。格式化字符串由以下三类元素组成。

  • 普通字符(除了 { 和 } 以外),这些字符会被直接拷贝到输出中,不会做任何更改。
  • 转义序列,包括 ,在输出中分别会被替换成{和}。
  • 替换字段,由 { … } 构成,这些替换字段会替换成 format 后续参数中对应的参数,并根据格式控制描述生成输出。

对于替换字段的两种形式:

除了最简单的 format 参数,C++20 还提供了三个有用的工具函数,作为扩展功能: 1. format_to 2.format_to_n 3. formatted_size

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

int main() {
    // format_to
    // 将生成的文本输出到一个输出迭代器中,
    // 其他与format一致,这样可以兼容标准STL算法函数的风格,
    // 也便于将文本输出到其他的流中或者自建的字符串类中。
    std::string resultLine1;
    std::format_to(std::back_inserter(resultLine1), "{} + {} = {}", 1, 2, 1 + 2);
    std::cout << resultLine1 << std::endl;

    // format_to_n
    // 将生成的文本输出到一个输出迭代器中,同时指定输出的最大字符数量。
    // 其他与format一致,相当于format_to的扩展版本,
    // 在输出目标有字符限制的时候非常有效。
    std::string resultLine2(5, ' ');
    std::format_to_n(resultLine2.begin(), 5, "{} + {} = {}", 1, 2, 1 + 2);
    std::cout << resultLine2 << std::endl;

    // formatted_sizes
    // 获取生成文本的长度,参数与format完全一致。
    auto resultSize = std::formatted_size("{} + {} = {}", 1, 2, 1 + 2);
    std::cout << resultSize << std::endl;

    std::string resultLine3(resultSize, ' ');
    std::format_to(resultLine3.begin(), "{} + {} = {}", 1, 2, 1 + 2);
    std::cout << resultLine3 << std::endl;
}

格式化参数包

format 函数,可以直接以函数参数形式进行传递。此外,C++20 还提供了 format_args 相关接口,可以把“待格式化的参数”合并成一个集合,通过 vformat 函数进行文本格式化。

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

int main() {
    std::string resultLine1 = std::vformat("{} * {} = {}", std::make_format_args(
        3, 4, 3 * 4
    ));
    std::cout << resultLine1 << std::endl;

    std::format_args args = std::make_format_args(
        3, 4, 3 * 4
    );

    std::string resultLine2;
    std::vformat_to(std::back_inserter(resultLine2), "{} * {} = {}", args);
    std::cout << resultLine2 << std::endl;
}

针对上述代码中用到的类型和函数:

  • 第一,format_args 类型,表示一个待格式化的参数集合,可以包装任意类型的待格式化参数。这里需要注意的是 format_args 中包装的参数是引用语义,也就是并不会拷贝或者扩展包装参数的生命周期,所以开发者需要确保被包装参数的生命周期。所以一般来说,format_args 也就用于格式化函数的参数,不建议用于其他用途。
  • 第二,make_format_args 函数,用于通过一系列参数构建一个 format_args 对象。类似地,需要注意返回的 format_args 的引用语义。
  • 第三,vformat 函数。包含两个参数,分别是格式化字符串(具体规范与 format 函数完全一致)和 format_args 对象。该函数会根据格式化字符串定义去 format_args 对象中获取相关参数并进行格式化输出,其他与 format 函数没有差异。
  • 第四,vformat_to 函数。该函数与 format_to 类似,都是通过一个输出迭代器进行输出的。差异在于,该函数接收的“待格式化参数”,需要通过 format_args 对象进行包装。因此,vformat 可以在某些场景下替代 format。至于具体使用哪个,你可以根据自己的喜好进行选择。

formatting

Formatting 库的核心是 formatter 类,对于所有希望使用 format 进行格式化的参数类型来说,都需要按照约定实现 formatter 类的特化版本。

formatter 类主要完成的工作就是:格式化字符串的解析、数据的实际格式化输出。C++20 为基础类型与 string 类型定义了标准的 formatter。此外,我们还可以通过特化的 formatter 来实现其他类型、自定义类型的格式化输出。

标准格式化规范

C++ Formatting 的标准格式化规范,是以 Python 的格式化规范为基础的。基本语法:

填充与对齐   符号   #   0   宽度   精度L   类型

这里的每个参数都是可选参数。

填充与对齐

第一个为填充与对齐,用于设置填充字符与对齐规则。

该参数包含两部分,第一部分为填充字符,如果没有设定,默认使用空格作为填充。第二部分为填充数量与对齐方式,填充数量就是指定输出的填充字符数量,对齐方式指的是待格式化参数输出时相对于填充字符的位置。

目前 C++ 支持三种对齐方式:

符号前缀显示

“符号” “#” 和“0”,用于设定数值类型的前缀显示方式。

“符号”可以设置数字前缀的正负号显示规则。需要注意的是,“符号”也会影响 inf 和 nan 的显示方式。

“#” 会对整数和浮点数有不同显示行为。

如果被格式化参数为整数,并且将整数输出设定为二进制、八进制或十六进制时会在数字前添加进制前缀,也就是 0b、0 和 0x。 如果被格式化参数为浮点数,那么即使浮点数没有小数位数,也会强制在数字后面追加一个小数点。

“0” 用于为数值输出填充 0,并支持设置填充位数。比如 04 就会填充 4 个 0。

宽度与精度

宽度用于设置字段输出的最小宽度,可以使用一个十进制数,也可以通过 {} 引用一个参数。

精度是一个以 . 符号开头的非负十进制数,也可以通过{}引用一个参数。对于浮点数,该字段可以设置小数点的显示位数。对于字符串,可以限制字符串的字符输出数量。

宽度与精度都支持通过 {} 引用参数,此时如果参数不是一个非负整数,在执行 format 时就会抛出异常。

L 与类型

L 用于指定参数以特定语言环境(locale)方式输出参数,类型选项用于设置参数的显示方式。

自定义 formatter

Formatting 库中的 formatter 类型对各种类型的格式化输出毕竟是有限的——它不可能覆盖所有的场景,特别是我们的自定义类型。我们先看一个最简单的自定义 formatter 案例。

#include <format>
#include <iostream>
#include <vector>
#include <cstdint>

template<class CharT>
struct std::formatter<std::vector<int32_t>, CharT> : std::formatter<int32_t, CharT> {
    template<class FormatContext>
    auto format(std::vector<int32_t> t, FormatContext& fc) const {
        auto it = std::formatter<int32_t, CharT>::format(t.size(), fc);

        for (int32_t v : t) {
            *it = ' ';
            it++;

            it = std::formatter<int32_t, CharT>::format(v, fc);
        }

        return it;
    }
};

int main() {
    std::vector<int32_t> v = { 1, 2, 3, 4 };

    // 首先,调用format输出vector的长度,
    // 然后遍历vector,每次输出一个空格后再调用format输出数字。
    std::cout << std::format("{:#x}", v);
}

在这段代码中,实现了格式化显示 vector 类型的对象的功能。我们重点关注的是第 7 行实现的 formatter 特化——std::formatter, CharT>。

其中,CharT 表示字符类型,它可以根据用户的实际情况替换成 char 或者 wchar_t 等。

通过代码你会发现,我们重载了 format 成员函数,该函数用于控制格式化显示。该函数包含两个参数:

  • t: std::vector: 被传入的待格式化参数
  • fc: FormatContext&: 描述格式化的上下文

一个实践

可以尝试用format库和source_location配合实现一个日志库,当然能用就行。


评论
  目录