LLVM 项目博客

LLVM 项目新闻和来自底层的细节

使用 libc++ 在 C++ 中进行文本格式化

从历史上看,使用标准库在 C++ 中格式化文本一直是一件不愉快的事情。可以使用流运算符获得不错的输出,但由于流操作符,它非常冗长。另一种选择是使用printf,它很方便,但只支持有限数量的类型,并且不可扩展。一个非标准选项是使用{fmt}库。本文简要介绍了该库在 C++20 中作为std::format标准化的部分,以及 LLVM 15 中的当前实现状态。

什么是std::format

std::format是一个文本格式化库,使用类似于 Python 的format的格式字符串,并且可以扩展以用于用户定义的类型。

#include <format>
#include <iostream>

int main() {
  std::cout << std::format("Hello {} in C++{}", "std::format", 20);
}

写入以下输出

Hello std::format in C++20

{}表示一个替换字段,类似于printf中的%。使用std::format,参数类型是已知的,因此不需要在替换字段中指定它们。

还可以指定所需的输出格式以及要用于每个替换字段的位置参数。(为了简洁起见,以下示例省略了必需的包含部分。)

使用不同的基数、前缀和零填充到 8 列,写入第一个位置参数。

int main() {
  std::cout << std::format("{0:#08b}, {0:#08o}, {0:08}, {0:#08x}", 16);
}
0b010000, 00000020, 00000016, 0x000010

可以使用大写前缀和十六进制数字。

int main() {
  std::cout << std::format("{0:#08B}, {0:#08o}, {0:08}, {0:#08X}", 15);
}
0B001111, 00000017, 00000015, 0X00000F

可以指定对齐方式和填充字符。

int main() {
  std::cout
     << std::format("{:#<8} {:*>8} {:-^5}", "Hello", "world", '!');
}
Hello### ***world --!--

在打印表格时,能够指定列的对齐方式和宽度会很不错。但是,格式化 Unicode 文本可能特别棘手,因为并非每个char(或wchar_t)都是一个“字符”。

例如,字母 Á 可以用两种方式书写

  • 拉丁大写字母 A 带重音
  • 拉丁大写字母 A + 组合重音

这种组合多个“字符”的方法在许多脚本和表情符号中使用。(这种“组合多个字符”在 Unicode 中被称为扩展字形簇。)该库已实现这些规则,因此它将把 Á 的两种形式都视为使用一列输出。

文本格式化的另一个问题是,并非每个“字符”都具有相同的列宽。根据“字符”,列宽估计为一列或两列。

以下是来自论文的一个示例,该论文介绍了std::format中的宽度估计算法

struct input {
  const char* text;
  const char* info;
};

int main() {
  input inputs[] = {
    {"Text", "Description"},
    {"-----",
     "------------------------------------------------------------------------"
     "--------------"},
    {"\x41", "U+0041 { LATIN CAPITAL LETTER A }"},
    {"\xC3\x81", "U+00C1 { LATIN CAPITAL LETTER A WITH ACUTE }"},
    {"\x41\xCC\x81",
     "U+0041 U+0301 { LATIN CAPITAL LETTER A } { COMBINING ACUTE ACCENT }"},
    {"\xc4\xb2", "U+0132 { LATIN CAPITAL LIGATURE IJ }"}, // IJ
    {"\xce\x94", "U+0394 { GREEK CAPITAL LETTER DELTA }"}, // Δ
    {"\xd0\xa9", "U+0429 { CYRILLIC CAPITAL LETTER SHCHA }"}, // Щ
    {"\xd7\x90", "U+05D0 { HEBREW LETTER ALEF }"}, // א
    {"\xd8\xb4", "U+0634 { ARABIC LETTER SHEEN }"}, // ش
    {"\xe3\x80\x89", "U+3009 { RIGHT-POINTING ANGLE BRACKET }"}, // 〉
    {"\xe7\x95\x8c", "U+754C { CJK Unified Ideograph-754C }"}, // 界
    {"\xf0\x9f\xa6\x84", "U+1F921 { UNICORN FACE }"}, // 🦄
    {"\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d"
     "\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6",
     "U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 "
     "{ Family: Man, Woman, Girl, Boy } "} // 👨‍👩‍👧‍👦
  };

  for (auto input: inputs) {
    std::cout << std::format("{:>5} | {}\n", input.text, input.info);
  }
}

(请注意,列宽旨在使终端上的外观良好。作者已经观察到输出质量因所用浏览器而异。)

 Text | Description
----- | --------------------------------------------------------------------------------------
    A | U+0041 { LATIN CAPITAL LETTER A }
    Á | U+00C1 { LATIN CAPITAL LETTER A WITH ACUTE }
    Á | U+0041 U+0301 { LATIN CAPITAL LETTER A } { COMBINING ACUTE ACCENT }
    IJ | U+0132 { LATIN CAPITAL LIGATURE IJ }
    Δ | U+0394 { GREEK CAPITAL LETTER DELTA }
    Щ | U+0429 { CYRILLIC CAPITAL LETTER SHCHA }
    א | U+05D0 { HEBREW LETTER ALEF }
    ش | U+0634 { ARABIC LETTER SHEEN }
   〉 | U+3009 { RIGHT-POINTING ANGLE BRACKET }
   界 | U+754C { CJK Unified Ideograph-754C }
   🦄 | U+1F921 { UNICORN FACE }
   👨‍👩‍👧‍👦 | U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 { Family: Man, Woman, Girl, Boy }

尝试将值格式化为错误的类型(例如,将字符串格式化为数字)将导致编译错误,而不是使用printf导致的运行时错误。大多数主要编译器提供警告以尝试检测printf中的不正确的格式说明符,但这不是规范的一部分,尤其是嵌入式编译器通常不提供该警告。相反,std::format指定为产生编译错误,该错误在库本身中使用 C++20 consteval函数实现。

int main() {
  std::cout << std::format("{0:#08B}, {0:#08o}, {0:08}, {0:#08X}", "15");
}

编译器输出以该错误开头,后面跟着许多不太有用的消息。

error: call to consteval function 'std::basic_format_string<char, const char (&)[3]>::basic_format_string<char[37]>' is not a constant expression
  std::cout << std::format("{0:#08B}, {0:#08o}, {0:08}, {0:#08X}", "15");

除了将格式化结果输出到字符串之外,还可以

  • 将结果输出到任意输出迭代器,
int main() {
  std::format_to(
    std::ostream_iterator<char>(std::cout, ""),
    "Hello {} in C++{}\n", "std::format", 20);
}
  • 确定输出大小,
int main() {
  std::cout << std::formatted_size("Hello {} in C++{}\n", "std::format", 20);
}
27
  • 或限制输出大小。
int main() {
  std::format_to(
    std::ostream_iterator<char>(std::cout, ""),
    11, "Hello {} in C++{}\n", "std::format", 20);
}
Hello std

在标准库中提供了一个格式化用户定义类型的示例。它对chrono库有格式化支持。(这在 libc++ 中尚不可用。)这些格式化程序非常复杂。对于其他类型,可以快速创建一个格式化程序。例如,对于以下enum class

enum class color { red, green, blue };

可以像这样添加基于现有格式化程序的格式化程序

template <>
struct std::formatter<color> : std::formatter<const char*> {
  static constexpr const char* color_names[] = {"red", "green", "blue"};

  auto format(color c, auto& ctx) const -> decltype(ctx.out()) {
    using base = formatter<const char*>;
    return base::format(color_names[static_cast<int>(c)], ctx);
  }
};

现在,const char*格式化程序的所有功能都可以在颜色格式化程序中使用

int main() {
  std::cout << std::format("{:#<10}\n{:+^10}\n{:->10}\n",
                           color::red, color::green, color::blue);
}
red#######
++green+++
------blue

可以在这个{fmt}备忘单中找到更多示例和规范详细信息。

LLVM 15 中的状态

在 LLVM 15 中,大多数基本文本格式化已完成。所有主要论文都已实施,但一些缺陷报告尚未实施。libc++ 团队还希望对性能改进和编译时错误改进进行研究。其中一些改进已在main中完成,并将包含在 LLVM 16 中,而另一些则仅计划中。

由于该库尚未完全完成,并且 ABI 可能需要更改(由于计划的改进,但也由于 C++ 委员会投票决定的更改),它在 LLVM 15 中作为实验性功能提供。要在 libc++ 中使用该代码,您需要像这样编译代码

clang -std=c++20 -stdlib=libc++ -fexperimental-library -ofoo foo.cpp

chrono的格式支持不可用。针对 LLVM 16 的初始工作已完成,但 LLVM 15 中尚未提供任何内容。chrono库本身缺乏对时区、闰秒以及某些不太常见的时钟的支持。在完成对chrono的格式化支持之前,这些功能必须可用。

C++23 中的格式化改进

在示例中,输出首先在std::string中格式化,然后再将其流式传输到输出。为了避免使用临时std::string,可以使用std::format_to,但它没有符合人体工程学的语法。在 C++23 中,将有std::print

int main() {
  std::print("Hello {} in C++{}", "std::format", 20);
}
Hello std::format in C++20

对格式化容器的支持很少。在 C++23 中,将能够格式化范围和容器。

int main() {
  std::print("{::*^5}", std::vector<int>{1, 2, 3});
}
[**1**, **2**, **3**]

对格式化范围进行了一些进展,但短期内的主要重点是完成 C++20 的格式实现。

结束语

从 C++20 开始,格式化文本变得更加令人愉快,C++23 还有更多改进。这应该为 C++ 提供期待已久的功能,并允许用更方便、更快速、更安全的替代方案替换<iostream>的几种用法。

致谢

衷心感谢{fmt}的作者 Victor Zverovich。他积极参与将std::format纳入 C++ 标准。他帮助审查了 libc++ 的实现,他的见解和评论提高了实现的质量。