本文整理了C++中String-to-Int的9种方式,并对其性能进行了对比。
这些方式包含:
atoi
strtol
sscanf
sstream
lexical_cast
stoi
from_chars
spanstream
constexpr from_chars
simple compile time to_int
这个列表是按时间排序的,从C89到C++23。
据群内小调查,使用atoi和sstream的人最多,stoi和from_chars的其次。
下面就让我们全面地对比一下这些方式。
1
C89/C90 atoi, sscanf
atoi是C89/C90就存在的东西,包含在<stdlib.h>。使用起来很简单,效率也很高,一个例子:
#include <iostream>
#include <cstdlib>
#include <string>
int main() {
std::string s("20230324");
int res = atoi(s.c_str());
std::cout << res << "\n";
}
它的缺点在于,没有错误处理。你无法处理这种情况:
assert(atoi("0") != atoi("str"));
有许多人采用strtol(C99)来代替atoi,例子:
std::string s("str");
int res = (int)strtol(s.c_str(), NULL, 10);
if (errno != ERANGE)
std::cout << res << "\n";
如果转换的值超过了返回值范围,那么errno会被置为ERANGE;如果不可转换,则返回0。它优于atoi的点就在于支持边界错误检查。
此外,strtol还支持多值转换,以及指定转换基底,通用性也要比atoi强。提供一个多值转换的例子,其中基数是10表示十进制。
const char* s = "10 20 30 40";
for (;;) {
errno = 0;
char *end;
int val = (int)strtol(s, &end, 10);
if (s == end)
break;
if (errno != ERANGE)
printf("'%.*s' ==> %d\n", (int)(end-s), s, val);
s = ++end; // skip the whitespace
}
// Output:
// '10' ==> 10
// '20' ==> 20
// '30' ==> 30
// '40' ==> 40
另一个C89的可以用来转换的特性是sscanf,相信没人会陌生。但是它有缓冲区溢出和类型安全等等问题,C++ devs用得不多。
2
C++98 sstream
atoi和strtol都来自C,在C++中使用起来不是太自然。C++标准中第一个支持类型转换的组件就是sstream,功能还是比较强大的和完整的。
看一个多值转换的例子:
int main() {
std::string str = "10 20 30";
std::stringstream ss(str);
int x, y, z;
ss >> x >> y >> z;
std::cout << "x: " << x << " y: " << y << " z: " << z << "\n";
}
// Output:
// x: 10 y: 20 z: 30
sstream是String IO,易用、简洁、灵活,这种方式更加C++。
C++ devs早期就是借助sstream来编写转换工具,当年的一种经典实现:
template <class T, class U>
class is_convertible {
typedef char small_type;
struct big_type { char dummy[2]; };
static small_type test(T);
static big_type test(...);
static U makeU();
public:
enum { value = sizeof(test(makeU())) == sizeof(small_type) };
};
template <class T, class U,
typename std::enable_if<!is_convertible<T, U>::value>::type* = nullptr>
void convert(T& to, const U& from) {
std::stringstream ss;
ss << from;
ss >> to;
}
template <class T, class U,
typename std::enable_if<is_convertible<T, U>::value>::type* = nullptr>
void convert(T& to, const U& from) {
to = from;
}
int main() {
std::string str("230326");
int retval;
convert(retval, str);
std::string str2;
convert(str2, retval);
std::cout << retval << " --- " << str2 << "\n";
}
只要把std::enable_if自己实现一下,在C++11之前的项目中也可以编译这段代码。
万事都有代价。sstream如此灵活的代价,就是存在对象创建、内存分配和输入解析等等开销,导致性能较低。
3
boost lexical_cast
boost库在05/06年加入并完善了lexical_cast,用以支持类型转换,同时还为数值到数值之间的转换提供了numeric_cast。
lexical_cast要比sstream更加易用,且性能更高。一个例子看一下:
#include <iostream>
#include <string>
#include <boost/lexical_cast.hpp>
int main() {
std::string str = "123";
int retval = boost::lexical_cast<int>(str);
std::cout << retval << "\n";
}
使用人较少的原因,可能是因为没有进标准,一些开发者不喜欢依赖非标准以外的库。再者,后面C++也增加了一些转换相关的组件,性能远超lexical_cast。
4
C++11 stoi
通常来说,越是新加的组件,其必然在易用性或性能方面优于之前的组件,否则也就没有必要引入标准了。
C++11加入了一系列String转换函数,包含stoi, stol和stoll。顾名思义,stoi的转换范围是int,stol和stoll分别是long和long long。
一个例子:
#include <iostream>
#include <string>
#include <stdexcept>
int main() {
std::string s {"20230327214805"};
try {
int retval = std::stoi(s);
std::cout << "stoi: " << retval << "\n";
} catch (std::invalid_argument const& e) {
std::cout << "std::invalid_argument::what: " << e.what() << "\n";
} catch (std::out_of_range const& e) {
long long llretval = std::stoll(s);
std::cout << "stoll: " << llretval << "\n";
}
}
// output:
// stoll: 20230327214805
除了atoi,stox的性能要超过之前的其他所有方法,并且提供了良好的错误处理。此外,stox系列函数其实有三个参数,第二个参数能够得到实际处理的字符数,第三个参数能够指定基数。因此,它是一种比较好的方法。
5
C++17 from_chars
C++17又增加了一组工具:to/from_chars,它们既不会分配内存,也不会抛出异常,是一种更加轻量级的转换方式。
一个例子:
#include <charconv>
#include <iostream>
#include <string_view>
#include <system_error>
int main() {
for (std::string_view const str : {"10", "str", "20230327214805"}) {
int result{};
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);
if (ec == std::errc())
std::cout << "result: " << result << "\n";
else if (ec == std::errc::invalid_argument)
std::cerr << "invalid_argument\n";
else if (ec == std::errc::result_out_of_range)
std::cerr << "result_out_of_range\n";
}
}
// output:
// result: 10
// invalid_argument
// result_out_of_range
这是当前标准中最高效的方式,效率超过前面的任何一种方式。追求效率时,应该首选这种方式。
6
C++23 spanstream
C++23新增的spanstream和stringstream对应,不过后者是String IO,它是Array IO。
因此,它的用法和sstream一样,不过效率要稍高一点点。例子:
#include <iostream>
#include <span>
#include <spanstream>
int main() {
char input[] = "10 20 30";
std::ispanstream is{ std::span<char>{input} };
int x, y, z;
is >> x >> y >> z;
std::cout << "x: " << x << " y: " << y << " z: " << z << "\n";
}
7
C++23 constexpr from_chars
以上所有方式都是运行期的,C++23支持编译期转换:constexpr to/from_chars。
编译期方式的效率要远远高于运行期的方式,因此这种方式是最高效的。使用例子:
#include <charconv>
#include <optional>
#include <string_view>
constexpr std::optional<int> to_int(std::string_view s) {
int value;
if (auto [p, err] = std::from_chars(s.data(), s.data() + s.size(), value); err == std::errc{}) {
return value;
} else {
return std::nullopt;
}
}
int main () {
constexpr std::string_view sv = "20230317";
constexpr int result = *to_int(sv);
static_assert(result == 20230317, "failed to convert string to int at compile time.");
}
当前只有msvc 19.34+支持该特性,gcc13和clang16离发布还得一段时间。
8
simple compile time to_int
自己也可以借助constexpr来实现简单的编译期转换函数,下面是一种实现方式:
int main () {
constexpr auto to_int = [](this auto self, const char *str, int h = 0) -> int {
return *str ? self(str + 1, h * 10 + *str - '0') : h;
};
constexpr char s[] = "20230317";
constexpr int result = to_int(s);
static_assert(result == 20230317, "failed to convert string to int at compile time.");
}
这种方式也没有范围检查和错误处理等等额外操作,但它发生于编译期,效率很高。这里还使用了recursive lambda,若要在低版本使用,只需将lambda替换为常规函数就可以。
9
Benchmarking
现在来测试一下所有的方法,对比一下他们的效率。测试代码如下:
1#include <benchmark/benchmark.h>
2#include <string>
3#include <string_view>
4#include <cassert>
5
6
7#include <cstdlib>
8static void atoi_bench(benchmark::State& state) {
9 std::string s {"20230317"};
10 for (auto _ : state) {
11 int result = std::atoi(s.c_str());
12 assert(result != 0);
13 benchmark::DoNotOptimize(result);
14 }
15}
16BENCHMARK(atoi_bench);
17
18
19#include <cerrno>
20static void strtol_bench(benchmark::State& state) {
21 std::string s {"20230328"};
22 for (auto _ : state) {
23 int result = (int)std::strtol(s.c_str(), NULL, 10);
24 assert(errno != ERANGE);
25 benchmark::DoNotOptimize(result);
26 }
27}
28BENCHMARK(strtol_bench);
29
30
31#include <cstdio>
32static void sscanf_bench(benchmark::State& state) {
33 std::string s {"20230317"};
34 for (auto _ : state) {
35 int result{};
36 int ec = sscanf(s.c_str(), "%d", &result);
37 assert(ec == 1);
38 benchmark::DoNotOptimize(result);
39 }
40}
41BENCHMARK(sscanf_bench);
42
43
44#include <sstream>
45static void sstream_bench(benchmark::State& state) {
46 std::string s {"20230317"};
47 for (auto _ : state) {
48 std::istringstream ss(s);
49 int result{};
50 ss >> result;
51 assert(!ss.fail());
52 benchmark::DoNotOptimize(result);
53 }
54}
55BENCHMARK(sstream_bench);
56
57
58#include "boost/lexical_cast.hpp"
59static void lexical_cast_bench(benchmark::State& state) {
60 std::string s {"20230317"};
61 for (auto _ : state) {
62 int result{};
63 result = boost::lexical_cast<int>(s);
64 benchmark::DoNotOptimize(result);
65 }
66}
67BENCHMARK(lexical_cast_bench);
68
69
70static void stoi_bench(benchmark::State& state) {
71 std::string s {"20230317"};
72 for (auto _ : state) {
73 int result = std::stoi(s);
74 benchmark::DoNotOptimize(result);
75 // catch std::invalid_argument or std::out_of_range exceptions to check the result.
76 }
77}
78BENCHMARK(stoi_bench);
79
80
81#include <charconv>
82static void from_chars_bench(benchmark::State& state) {
83 std::string s {"20230317"};
84 for (auto _ : state) {
85 int result{};
86 auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), result);
87 benchmark::DoNotOptimize(result);
88 }
89}
90BENCHMARK(from_chars_bench);
91
92
93#include <span>
94#include <spanstream>
95static void spanstream_bench(benchmark::State& state) {
96 char input[] = "20230317";
97 for (auto _ : state) {
98 std::ispanstream is{ std::span<char>{input} };
99 int result{};
100 is >> result;
101 benchmark::DoNotOptimize(result);
102 }
103}
104BENCHMARK(spanstream_bench);
105
106
107// Compile-time convert string to int
108constexpr int to_int(const char *str, int h = 0) {
109 return *str ? to_int(str + 1, h * 10 + *str - '0') : h;
110}
111
112static void simple_compile_time_to_int_bench(benchmark::State& state) {
113 constexpr char s[] = "20230317";
114 for (auto _ : state) {
115 constexpr int result = to_int(s);
116 benchmark::DoNotOptimize(result);
117 }
118}
119BENCHMARK(simple_compile_time_to_int_bench);
120
121
122// constexpr from_chars
123// #include <optional>
124// constexpr std::optional<int> to_int(std::string_view s) {
125// int value;
126// if (auto [p, err] = std::from_chars(s.data(), s.data() + s.size(), value); err == std::errc{}) {
127// return value;
128// } else {
129// return std::nullopt;
130// }
131// }
132
133// static void constexpr_from_chars_bench(benchmark::State& state) {
134// constexpr std::string_view sv = "20230317";
135// for (auto _ : state) {
136// constexpr int result = *to_int(sv);
137// benchmark::DoNotOptimize(result);
138// }
139// }
140//BENCHMARK(constexpr_from_chars_bench);
141
142BENCHMARK_MAIN();
由于现在只有msvc v19.34+才支持constexpr from_chars,gcc和clang尚不支持,所以暂时将它加了注释。
测试结果为:
---------------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------------
atoi_bench 43.6 ns 43.6 ns 16152578
strtol_bench 45.9 ns 45.9 ns 15233262
sscanf_bench 220 ns 220 ns 3239227
sstream_bench 731 ns 731 ns 876900
lexical_cast_bench 54.6 ns 54.6 ns 12817159
stoi_bench 44.3 ns 44.3 ns 15799663
from_chars_bench 19.9 ns 19.9 ns 35240157
spanstream_bench 422 ns 422 ns 1577471
simple_compile_time_to_int_bench 0.632 ns 0.632 ns 1000000000
可以看到,运行期方法中C++17 from_chars效率是最高的,其次是atoi和stoi,再是strtol和lexical_cast,sstream与sscanf的开销要远远大于其他方式,spanstream较sstream效率有所提高。编译期方法不出意料,效率最高。
10
Conclusion
C++98追求效率使用strtol,追求易用性使用sstream,允许用boost则使用lexical_cast。C++11使用stox系列函数,C++17以上使用from_chars是最佳选择。若是支持C++23,那么还可以使用constexpr from_chars。
多值转换追求方便的话可以使用sstream/spanstream,追求效率的话使用strtol或是from_chars+Ranges。