Chapter31 std::to_chars()std::from_chars()

C++标准库中提供了把数字转换为字符序列和反向转换的底层操作。

31.1 底层字符序列和数字值转换的动机

把整数值转换为字符序列和反向转换自从C开始就是一个问题。 C提供了sprintf()sscanf(), C++一开始引入了字符串流,然而它需要消耗太多资源。 C++11中又引入了std::to_string()std::stoi()等函数, 后者只接受std::string参数。

C++17引入了有下列能力的新的底层字符串转换函数(引用自最初的提案):

  • 没有格式化字符串的运行时解析
  • 不需要动态内存分配
  • 不考虑locale
  • 不需要间接的函数指针
  • 防止缓冲区溢出
  • 当解析字符串时,如果发生错误可以和有效的数字区分
  • 当解析字符串时,空格或前后缀不会被悄悄忽略

另外,对于浮点数,这个特性还提供双向保证:即保证值被转换为字符序列之后如果再转换回来还是原来的值。

这些函数在头文件<charconv>中提供。

31.2 使用示例

标准库提供了两个重载的函数:

  • std::from_chars()把一个字符序列转换为数字值。
  • std::to_chars()把一个数字值转换为字符序列。

31.2.1 from_chars

std::from_chars()把一个字符序列转换为数字值。例如:

  1. #include <charconv>
  2. const char* str = "12 monkeys";
  3. int value;
  4. std::from_chars_result res = std::from_chars(str, str+10, value);

转换成功之后,value会含有解析后的值(这个例子中是12)。 返回值是一个如下的结构体:

  1. struct from_chars_result {
  2. const char* ptr;
  3. std::errc ec;
  4. };

调用之后,ptr指向没有被解析为数字的部分的第一个字符(或者,如果所有字符都被解析了就 等于传入的第二个参数),ec包含std::errc类型的错误条件,如果转换成功时 等于std::errc{}。因此,你可以按照如下方式检查是否成功:

  1. if (res.ec != std::errc{}) {
  2. ... // 错误处理
  3. }

注意std::errc没有到bool的隐式转换,所以你不能像下面这样检查:

  1. if (res.ec) { // ERROR:没有到bool的隐式转换

或者:

  1. if (!res.ec) { // ERROR:没有定义operator!

然而,通过使用结构化绑定和带初始化的if语句,你可以写:

  1. if (auto [ptr, ec] = std::from_chars(str, str+10, value); ec != std::errc{}) {
  2. ... // 错误处理
  3. }

对于整数值,你可以传递一个进制数作为最后的可选参数。进制数可以从2到26(包括26)。例如:

  1. #include <charconv>
  2. const char* str = "12 monkeys";
  3. int value;
  4. std::from_chars_result res = std::from_chars(str, str+10, // 字节序列
  5. value, // 存储转换后的值
  6. 16); // 可选的进制数

对于其他的例子,见

  • 位序列到std::byte的转换
  • 解析传入的字符串视图

31.2.2 to_chars()

std::to_chars()把数字值转换为字符序列。例如:

  1. #include <charconv>
  2. int value = 42;
  3. char str[10];
  4. std::to_chars_result res = std::to_chars(str, str+9, value);
  5. *res.ptr = '\0';

转换成功之后,str开头的字符序列包含了传入的整数值(这个例子中是42)的字符表示, 注意没有结尾的空字符。

同理,对于整数值,你还可以传递一个进制数作为可选的最后一个参数。 进制数可以是2到26(包含26)。例如:

  1. #include <charconv>
  2. int value = 42;
  3. char str[10];
  4. std::to_chars_result res = std::to_chars(str, str+9, // 存储结果的字符序列
  5. value, // 要转换的值
  6. 16); // 可选的进制数
  7. *res.ptr = '\0'; // 保证结尾有一个空字符

对于另一个例子,见std::byte转换为位序列。

返回值是一个如下的结构体:

  1. struct to_chars_result {
  2. char* ptr;
  3. std::errc ec;
  4. };

调用之后,ptr指向最后一个被写入的字符的下一个位置,ec可能会包含一个 std::errc类型的错误条件,如果转换成功,它的值等于std::errc{}

因此,你可以像这样检查结果:

  1. if (res.ec != std::errc{}) {
  2. ... // 错误处理
  3. }
  4. else {
  5. process(str, res.ptr - str); // 传递字符序列和长度
  6. }

再次注意std::errc没有到bool的隐式转换,这意味着你不能像下面这样检查:

  1. if (res.ec) { // ERROR:没有到bool的隐式转换

或者:

  1. if (!res.ec) { // ERROR:没有定义operator!

因为不会自动写入末尾的空字符,所以你必须保证只使用被写入的那些字符或者像上面的例子一样 使用返回值的ptr成员手动添加一个末尾的空字符:

  1. *res.ptr = '\0'; // 确保最后有一个空字符

再一次,通过使用结构化绑定和带初始化的if语句,你可以写:

  1. if (auto [ptr, ec] = std::to_chars(str, str+10, value); ec != std::errc{}) {
  2. ... // 错误处理
  3. }
  4. else {
  5. process(str, ptr - str); // 传递字符和长度
  6. }

注意使用std::to_string()可以更安全更方便的实现这个功能。 只有进一步处理时需要被写入的字符序列时使用std::to_chars()才有意义。

31.3 浮点数和双向支持

如果没有指定精度,to_chars()from_chars()保证浮点数的双向支持。 这意味着值被转换为字符序列之后,再转换回来的值和一开始完全相同。 然而,只有当在同一个实现上读写时这个保证才成立。

为了实现保证,浮点数必须以最好的粒度和最高的精度写入字符序列。 因此,写入的字节序列可能非常长。

考虑如下函数:

  1. #include <iostream>
  2. #include <charconv>
  3. #include <cassert>
  4. void d2str2d(double value1)
  5. {
  6. std::cout << "in: " << value1 << '\n';
  7. // 转换为字符序列:
  8. char str[1000];
  9. std::to_chars_result res1 = std::to_chars(str, str+999, value1);
  10. *res1.ptr = '\0'; // 添加末尾的空字符
  11. std::cout << "str: " << str << '\n';
  12. assert(res1.ec == std::errc{});
  13. // 从字符序列中转换回来:
  14. double value2;
  15. std::from_chars_result res2 = std::from_chars(str, str+999, value2);
  16. std::cout << "out: " << value2 << '\n';
  17. assert(res2.ec == std::errc{});
  18. assert(value1 == value2); // 应该绝不会失败
  19. }

这里我们把一个传入的double值转换为了字符序列,又把它解析回来。 最后的断言再一次检查两个值是相同的。

下面的程序演示了效果:

  1. #include "charconv.hpp"
  2. #include <iostream>
  3. #include <iomanip>
  4. #include <vector>
  5. #include <numeric>
  6. int main()
  7. {
  8. std::vector<double> coll{0.1, 0.3, 0.00001};
  9. // 创建两个有微小不同的浮点数:
  10. auto sum1 = std::accumulate(coll.begin(), coll.end(), 0.0, std::plus<>());
  11. auto sum2 = std::accumulate(coll.rbegin(), coll.rend(), 0.0, std::plus<>());
  12. // 看起来相同:
  13. std::cout << "sum1: " << sum1 << '\n';
  14. std::cout << "sum2: " << sum2 << '\n';
  15. // 但事实上不同:
  16. std::cout << std::boolalpha << std::setprecision(20);
  17. std::cout << "equal: " << (sum1 == sum2) << '\n'; // false!!
  18. std::cout << "sum1: " << sum1 << '\n';
  19. std::cout << "sum2: " << sum2 << '\n';
  20. std::cout << '\n';
  21. // 检查双向转换
  22. d2str2d(sum1);
  23. d2str2d(sum2);
  24. }

我们按照不同的顺序累加了几个浮点数。sum1是从左到右累加,sum2是 从右到左累加(使用反向迭代器)。最后这两个数看起来相同但事实上不是:

  1. sum1: 0.40001
  2. sum2: 0.40001
  3. equal: false
  4. sum1: 0.40001000000000003221
  5. sum2: 0.40000999999999997669

当把值传给d2str2d()之后,你可以看到这两个值被按照必要的精度被存储为不同长度的字节序列:

  1. in: 0.40001000000000003221
  2. str: 0.40001000000000003
  3. out: 0.40001000000000003221
  4. in: 0.40000999999999997669
  5. str: 0.40001
  6. out: 0.40000999999999997669

再重复一次,注意粒度(决定了存储字符序列必须的长度)依赖于平台。

双向支持应该支持包含NANINFINITY在内的所有浮点数值。 例如,使用INFINITY调用d2st2d()应该有如下效果:

  1. in: inf
  2. str: inf
  3. out: inf

然而,注意d2str2d()中的断言会失败,因为NAN不能跟任何东西比较,即使是它自己也不行。