CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • C++17 详解 说明
  • 第一部分——语言特性
  • 1. 快速入门
  • 2. 移除或修正的语言特性
  • 3. 语言澄清(Language Clarification)
  • 4. 通用语言特性
  • 5. 模板(Templates)
  • 6. 代码标注
  • 第二部分 - 标准库的变化
  • 7. std::optional
  • 8. std::variant
  • 9. std::any
  • 10. std::string_view
  • 11. 字符串转换
    • 11. 字符串转换
      • 基础字符串转换
      • 从字符转换为数字:from_chars
      • 示例
      • 解析命令行
      • 将数字转换为字符:to_chars
      • 示例
      • 基准测试
      • 总结
      • 编译器支持情况
  • 12. 搜索器与字符串匹配
  • 13. 文件系统
  • 14. 并行STL算法
  • 15. 标准库中的其他变化
  • 16. 移除和弃用的库特性
  • 第三部分 - 更多示例和用例
  • 17. 使用std::optional和std::variant进行重构
  • 18. 使用[[nodiscard]]强制执行代码契约
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
  • 20. 如何实现CSV读取器的并行化
目录

11. 字符串转换

# 11. 字符串转换

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

在本章中,你将学到:

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

# 基础字符串转换

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

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

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

随着C++17的出现,这种情况可能会改变,因为我们有了两组函数:from_chars和to_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相关,就是你必须提供某种形式的 “格式字符串”。解析这样的字符串可能会带来一些额外开销。

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

另外一个关键因素是安全性。简单的函数没有提供任何防止缓冲区溢出的解决方案,并且它们仅适用于以空字符结尾的字符串。在这种情况下,你无法使用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可以是float、double或long double。chars_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等于first,from_chars_result::ec等于std::errc::invalid_argument,value未被修改。
  • 数值超出范围时——数字太大,无法存储在目标值类型中。from_chars_result::ec等于std::errc::result_out_of_range,from_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格式是fixed和scientific的组合,因此它既可以处理常规的浮点数字符串,也支持e+num语法。

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

# 解析命令行

在std::variant章节中,有一个解析命令行参数的示例。该示例使用from_chars来匹配最合适的类型:int、float或std::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的输出数字会用小写字母a到z表示。

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

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

FLOAT_TYPE可以是float、double或long 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::errc,ptr指向写入字符的末尾的下一个位置。注意,字符串没有以NULL结尾。
  • 发生错误时,ptr等于first,ec等于std::errc::invalid_argument,value未被修改。
  • 数值超出范围时,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_chars和to_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 8.2——使用-O2 -Wall -pedantic编译,MinGW发行版 (opens new window)
  • Clang 7.0——使用-O2 -Wall -pedantic编译,适用于Windows的Clang (opens new window)
  • Visual Studio 2017 15.8——发布模式,x64

一些说明:

  • 在GCC上,to_chars比to_string快近3倍,比sprintf快2.6倍,比ostringstream快12倍!
  • 在Clang上,to_chars比to_string稍慢,但比sprintf快约7倍,令人惊讶的是,比ostringstream快近40倍!
  • 在MSVC上,与to_string相比性能也较慢,但to_chars比sprintf快约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)》。

上次更新: 2025/04/01, 13:21:34
10. std::string_view
12. 搜索器与字符串匹配

← 10. std::string_view 12. 搜索器与字符串匹配→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式