11. 字符串转换

# 11. 字符串转换

string_view并非C++17中与字符串相关的唯一特性。视图(views)能够减少临时副本的数量,同时C++17还有另一个便捷特性:转换工具。在新的C++标准中,有两组函数from_charsto_chars,它们属于底层函数,有望显著提升性能。

在本章中,你将学到:

  • 为什么我们需要底层的字符串转换例程?
  • 为什么标准库中现有的选项可能不够用?
  • 如何使用C++17的转换例程?
  • 新的例程能带来哪些性能提升?

# 基础字符串转换

像JSON或XML这类数据格式越来越多,这就需要高效的字符串处理和操作。当这些数据格式用于网络通信时,最高性能尤为关键,因为高吞吐量是其中的关键因素。

例如,你从网络数据包中获取字符,对其进行反序列化(将字符串转换为数字),然后处理数据,最后再将数据序列化为相同的文件格式(将数字转换为字符串)并作为响应通过网络发送出去。

标准库在这些方面表现不佳。人们通常认为它对于这种高级字符串处理来说速度太慢。开发人员往往更倾向于自定义解决方案或第三方库。

随着C++17的出现,这种情况可能会改变,因为我们有了两组函数:from_charsto_chars,它们支持底层的字符串转换。

在原始论文(P0067 (opens new window))中有一个很有用的表格,总结了当前所有的解决方案:

工具 缺点
sprintf 格式字符串、区域设置、缓冲区溢出
snprintf 格式字符串、区域设置
sscanf 格式字符串、区域设置
atol 区域设置、不报告错误
strtol 区域设置、忽略空白字符和0x前缀
strstream 区域设置、忽略空白字符
stringstream 区域设置、忽略空白字符、内存分配
num_put / num_get 方面 区域设置、虚函数
to_string 区域设置、内存分配
stoi 区域设置、内存分配、忽略空白字符和0x前缀、抛出异常

从上面的表格可以看出,有时转换函数做了过多工作,这使得整个处理过程变慢。通常,这些额外的功能并没有必要。

首先,所有这些函数都使用 “区域设置”。即使你处理的是与语言无关的字符串,也得为本地化支持付出一点代价。例如,在解析XML或JSON中的数字时,无需应用当前系统语言,因为这些格式具有通用性。

下一个问题是错误报告。有些函数可能会抛出异常,而其他函数仅返回转换后的值。抛出异常不仅成本高昂(因为抛出异常可能涉及额外的内存分配),而且通常解析错误并非异常情况。像atoi返回0、atof返回0.0这种简单返回值的方式也不尽人意,因为这样你无法判断解析是否成功。

第三个问题,尤其与C风格的API相关,就是你必须提供某种形式的 “格式字符串”。解析这样的字符串可能会带来一些额外开销。

还有 “空白字符” 处理的问题。像strtolstringstream这类函数可能会跳过字符串开头的空白字符。这可能很方便,但有时你并不想为这个额外功能付出代价。

另外一个关键因素是安全性。简单的函数没有提供任何防止缓冲区溢出的解决方案,并且它们仅适用于以空字符结尾的字符串。在这种情况下,你无法使用string_view来传递数据。

新的C++17 API解决了上述所有问题。它们并非提供众多功能,而是专注于提供底层支持。这样一来,你可以获得最高的速度,并根据自己的需求进行定制。

新函数具有以下保证:

  • 不抛出异常——在出现错误的情况下,它们不会抛出异常(与stoi不同)。
  • 不分配内存——整个处理过程在原地完成,无需任何额外的内存分配。
  • 不支持区域设置——字符串的解析就像使用默认(“C”)区域设置一样。
  • 内存安全——指定了输入和输出范围,以便进行缓冲区溢出检查。
  • 无需传递数字的字符串格式。
  • 错误报告——你将获得有关转换结果的信息。

总之,在C++17中,你有两组函数:

  • from_chars——用于将字符串转换为数字,包括整数和浮点数。
  • to_chars——用于将数字转换为字符串。接下来让我们更详细地了解这些函数。

# 从字符转换为数字:from_chars

from_chars是一组重载函数:用于整数类型和浮点类型。对于整数类型,我们有以下函数:

std::from_chars_result from_chars(const   char* first,
                                  const   char* last,
                                  TYPE &value,
                                  int   base = 10);
1
2
3
4

其中TYPE可以是所有可用的有符号和无符号整数类型以及char类型。base的取值范围可以是2到36。还有浮点型版本:

std::from_chars_result from_chars(const   char* first,
                                  const   char* last,
                                  FLOAT_TYPE& value,
                                  std::chars_format fmt = std::chars_format::general);
1
2
3
4

FLOAT_TYPE可以是floatdoublelong doublechars_format是一个枚举类型,具有以下值:

enum   class   chars_format    {
    scientific = /*unspecified*/,
    fixed = /*unspecified*/,
    hex = /*unspecified*/,
    general = fixed | scientific
};
1
2
3
4
5
6

它是一种位掩码类型,所以枚举值是由实现定义的。默认情况下,格式设置为general,因此输入字符串既可以使用 “普通” 浮点格式,也可以使用科学计数法格式。

所有这些函数(对于整数和浮点数)的返回值都是from_chars_result

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

from_chars_result包含有关转换过程的重要信息。总结如下:

  • 转换成功时,from_chars_result::ptr指向第一个不匹配模式的字符,如果所有字符都匹配,则其值等于last,并且from_chars_result::ec为值初始化状态。
  • 转换无效时,from_chars_result::ptr等于firstfrom_chars_result::ec等于std::errc::invalid_argumentvalue未被修改。
  • 数值超出范围时——数字太大,无法存储在目标值类型中。from_chars_result::ec等于std::errc::result_out_of_rangefrom_chars_result::ptr指向第一个不匹配模式的字符,value未被修改 。

# 示例

为了总结本节内容,下面给出两个使用from_chars将字符串转换为数字的示例。第一个示例将字符串转换为int类型,第二个示例将其转换为浮点型。

  1. 整数类型:在String Conversions/from_chars_basic.cpp文件中:
#include <charconv> 
#include <iostream>
#include <string>

int   main() {
    const   std::string str { "12345678901234" };
    int   value = 0;
    const   auto    res = std::from_chars(str.data(),
                                          str.data() + str.size(),
                                          value);
    if    (res.ec == std::errc()) {
        std::cout << "value: " << value
                  << ", distance: " << res.ptr - str.data() << '\n ';
    }
    else   if    (res.ec == std::errc::invalid_argument) {
        std::cout << "invalid argument!\n ";
    }
    else   if    (res.ec == std::errc::result_out_of_range) {
        std::cout << "out of range! res.ptr distance: "
                  << res.ptr - str.data() << '\n ';
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这个示例很直观。它将字符串str传递给from_chars,然后尽可能显示结果以及附加信息。

以下是不同str值的输出结果:

str 输出
12345 value: 12345, distance 5
-123456 value: -123456, distance: 7
12345678901234 out of range! res.ptr distance: 14
hfhfyt invalid argument!

12345678901234这个例子中,转换例程能够解析这个数字(所有14个字符都被检查了),但它太大,无法存储在int类型中,因此我们得到了out_of_range错误。 2. 浮点类型:为了进行浮点型测试,我们可以将上一个示例的开头几行替换为:在String Conversions/from_chars_basic_float.cpp文件中:

const   std::string str { "16.78" }; 
double   value = 0;
const   auto   format = std::chars_format::general;
const   auto    res = std::from_chars(str.data(),
                                      str.data() + str.size(),
                                      value,
                                      format);
1
2
3
4
5
6
7

主要的区别在于最后一个参数:format。下面是得到的示例输出:

str format 输出
1.01 fixed value: 1.01, distance 4
-67.90000 fixed value: -67.9, distance: 9
1e+10 fixed value: 1, distance: 1 - scientific notation not supported
1e+10 fixed value: 1, distance: 1 - scientific notation not supported
20.9 scientific invalid argument!, res.p distance: 0
20.9e+0 scientific value: 20.9, distance: 7
-20.9e+1 scientific value: -209, distance: 8
F.F hex value: 15.9375, distance: 3
-10.1 hex value: -16.0625, distance: 5

general格式是fixedscientific的组合,因此它既可以处理常规的浮点数字符串,也支持e+num语法。

你已经对将字符串转换为数字有了基本的了解,接下来让我们看看如何进行相反的操作。

# 解析命令行

std::variant章节中,有一个解析命令行参数的示例。该示例使用from_chars来匹配最合适的类型:intfloatstd::string,然后将其存储在std::variant中。

你可以在std::variant章节的 “解析命令行” 部分找到这个示例。

# 将数字转换为字符:to_chars

to_chars是一组针对整数和浮点类型的重载函数。对于整数类型,有如下声明:

std::to_chars_result to_chars(char* first, char* last,
                              TYPE value, int    base = 10);
1
2

其中TYPE可以是所有可用的有符号和无符号整数类型以及char类型。

由于base的取值范围是2到36,大于9的输出数字会用小写字母az表示。

对于浮点数,有更多选项。首先是一个基本函数:

std::to_chars_result to_chars(char* first, char* last, FLOAT_TYPE value);
1

FLOAT_TYPE可以是floatdoublelong double

这种转换方式与printf相同,并且使用默认(“C”)区域设置,它会使用%f%e格式说明符,优先选择最短的表示形式。

下一个函数添加了std::chars_format fmt参数,用于指定输出格式:

std::to_chars_result to_chars(char* first, char* last,
                              FLOAT_TYPE value,
                              std::chars_format fmt);
1
2
3

还有一个 “完整” 版本,允许指定精度:

std::to_chars_result to_chars(char* first, char* last,
                              FLOAT_TYPE value,
                              std::chars_format fmt, int   precision);
1
2
3

当转换成功时,[first, last)范围内会被填充转换后的字符串。

所有函数(包括整数和浮点类型支持的函数)的返回值都是to_chars_result,其定义如下:

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

该类型包含有关转换过程的信息:

  • 转换成功时,ec等于值初始化的std::errcptr指向写入字符的末尾的下一个位置。注意,字符串没有以NULL结尾。
  • 发生错误时,ptr等于firstec等于std::errc::invalid_argumentvalue未被修改。
  • 数值超出范围时,ec等于std::errc::value_too_large[first, last)处于未指定状态。

# 示例

总之,下面是一个to_chars的基本示例。

在撰写本文时,还不支持浮点型的重载,所以这个示例仅使用整数。

String Conversions/to_chars_basic.cpp文件中:

#include <iostream>
#include <charconv> // from_chars, to_chars
#include <string>

int main() {
    std::string str { &quot;xxxxxxxx&quot; };
    const int value = 1986;
    const auto res = std::to_chars(str.data(), str.data() + str.size(), value);
    
    if (res.ec == std::errc()) {
        std::cout << str << &quot;, filled: &quot; << res.ptr - str.data() << &quot; characters\n&quot;;
    } else {
        std::cout << &quot;value too large!\n&quot;;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

下面是一组数字的示例输出:

value 输出
1986 1986xxxx, filled: 4 characters
-1986 -1986xxx, filled: 5 characters
19861986 19861986, filled: 8 characters
-19861986 value too large! (the buffer is only 8 characters)

# 基准测试

到目前为止,本章提到了新例程巨大的性能潜力。那么,最好能看看实际的数据!

本节介绍一个基准测试,用于衡量from_charsto_chars与其他转换方法的性能。

基准测试的工作方式如下:

  • 生成大小为VECSIZE的随机整数向量。
  • 每对转换方法将整数输入向量转换为字符串向量,然后再转换回另一个整数向量。这个往返转换过程会进行验证,确保输出向量与输入向量相同。
  • 转换过程执行ITER次。
  • 不检查转换函数的错误。
  • 代码测试以下转换方法:
    • from_char/to_chars
    • to_string/stoi
    • sprintf/atoi
    • ostringstream/istringstream

你可以在String Conversions/conversion_benchmark.cpp文件中找到完整的基准测试代码。下面是from_chars/to_chars的代码:

const auto numIntVec = GenRandVecOfNumbers(vecSize);
std::vector<std::string> numStrVec(numIntVec.size());
std::vector<int> numBackIntVec(numIntVec.size());
std::string strTmp(15, ' ');

RunAndMeasure(&quot;to_chars&quot;, [&]() {
    for (size_t iter = 0; iter < ITERS; ++iter) {
        for (size_t i = 0; i < numIntVec.size(); ++i) {
            const auto res = std::to_chars(strTmp.data(),
                                            strTmp.data() + strTmp.size(),
                                            numIntVec[i]);
            numStrVec[i] = std::string_view(strTmp.data(),
                                             res.ptr - strTmp.data());
        }
    }
    return numStrVec.size();
});

RunAndMeasure(&quot;from_chars&quot;, [&]() {
    for (size_t iter = 0; iter < ITERS; ++iter) {
        for (size_t i = 0; i < numStrVec.size(); ++i) {
            std::from_chars(numStrVec[i].data(),
                            numStrVec[i].data() + numStrVec[i].size(),
                            numBackIntVec[i]);
        }
    }
    return numBackIntVec.size();
});

CheckVectors(numIntVec, numBackIntVec);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

CheckVectors函数用于检查两个输入的整数向量是否包含相同的值,如果有错误则打印出不匹配的地方。

该基准测试将vector<int>转换为vector<string>,我们测量的是整个转换过程,其中也包括字符串对象的创建。

以下是在一个包含1000个元素的向量上运行1000次迭代的结果(时间单位为毫秒):

方法 GCC 8.2 Clang 7.0 Win VS 2017 15.8 x64
to_chars 21.94 18.15 24.81
from_chars 15.96 12.74 13.43
to_string 61.84 16.62 20.91
stoi 70.81 45.75 42.40
sprintf 56.85 124.72 131.03
atoi 35.90 34.81 32.50
ostringstream 264.29 681.29 575.95
stringstream 306.17 789.04 664.90

测试机器配置:Windows 10 x64,i7 8700,基础频率3.2 GHz,6核/12线程(尽管基准测试仅使用一个线程进行处理)。

一些说明:

  • 在GCC上,to_charsto_string快近3倍,比sprintf快2.6倍,比ostringstream快12倍!
  • 在Clang上,to_charsto_string稍慢,但比sprintf快约7倍,令人惊讶的是,比ostringstream快近40倍!
  • 在MSVC上,与to_string相比性能也较慢,但to_charssprintf快约5倍,比ostringstream快约23倍。

现在来看from_chars

  • 在GCC上,它比stoi快约4.5倍,比atoi快2.2倍,比istringstream快近20倍。
  • 在Clang上,它比stoi快约3.5倍,比atoi快2.7倍,比istringstream快60倍!
  • 在MSVC上,它比stoi快约3倍,比atoi快约2.5倍,比istringstream快近50倍!

如前所述,该基准测试也包含了字符串对象创建的开销。这就是为什么to_string(针对字符串进行了优化)可能比to_chars表现稍好一些。如果你已经有一个字符缓冲区,并且不需要创建字符串对象,那么to_chars应该会更快。

下面是根据上述表格生成的两个图表。

基准测试(配图)

图 11.1 将字符串向量转换为整数向量(时间,单位:毫秒)

基准测试(配图)

图 11.2 将整数向量转换为字符串向量(时间,单位:毫秒)

一如既往,在做出最终判断之前,建议你自己运行这些基准测试。在你的环境中,可能会因为使用不同的编译器或STL库实现而得到不同的结果。

# 总结

本章展示了如何使用两组函数:from_chars用于将字符串转换为数字,to_chars用于将数字转换为文本表示形式。

这些函数可能看起来非常底层,甚至带有C语言风格。这是为获得底层支持、性能、安全性和灵活性所付出的 “代价”。好处是你可以提供一个简单的包装器,只暴露你需要的部分。

(此处应有一张图片,但文档中未提供图片相关信息,故无法准确翻译。)

额外信息

相关变更提案在:P0067 (opens new window)

# 编译器支持情况

特性 GCC Clang MSVC
基础字符串转换 8.0⁵ 7.0⁶ VS 2017 15.7/15.8⁷

批注:

⁵ 开发中,仅支持整数类型 ⁶ 开发中,仅支持整数类型 ⁷ 在15.7版本中,from_chars/to_chars支持整数类型;在15.8版本中,from_chars支持浮点类型。15.9版本中to_chars对浮点类型的支持应该会完成。详见《Visual C++团队博客:VS 2017 15.8中的STL特性和修复 (opens new window)》。

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>