使用string和string_view(二)——数值转换、std::string_view和非标准字符串
目录
- 1. 数值转换
- 1.1 高级数值转换函数
- 1.1.1 数值转换为字符串
- 1.1.2 字符串转换为数值
- 1.2 低级数值转换函数
- 1.2.1 数值转换为字符串
- 1.2.2 字符串转换为数值
- 2. std::string_view
- 2.1 引入
- 2.2 string_view转换成string
- 2.3 std::string_view和临时字符串
- 2.4 std::string_view字面量
- 3. 非标准字符串
- 参考
1. 数值转换
C++标准库同时提供了高级数值转换函数和低级数值转换函数。
1.1 高级数值转换函数
1.1.1 数值转换为字符串
下面的函数可用于将数值转换为字符串,T可以是(unsigned) int、(unsigned) long、(unsigned) long long、float、double以及long double。所有这些函数都负责内存分配,它们会创建一个新的string对象并返回。
std::string to_string(T val);
这些函数的使用非常简单直观。例如,下面的代码将long double值转换为字符串。
long double d { 3.14L };
std::string s { std::to_string(d) };
1.1.2 字符串转换为数值
通过下面这组同样在std名称空间中定义的函数,可以将字符串转换为数值。在这些函数原型中,str表示要转换的字符串,idx是一个指针,这个指针接收第一个未转换的字符的索引,base表示转换过程中使用的进制。idx指针可以是空指针,如果是空指针,则被忽略。如果不能执行任何转换,这些函数会抛出invalid_argument异常。如果转换的值超出返回类型的范围,则抛出out_of_range异常。
int stoi(const string& str, size_t* idx=0, int base=10);
long stol(const string& str, size_t* idx=0, int base=10);
unsigned long stoul(const string& str, size_t* idx=0, int base=10);
long long stoll(const string& str, size_t* idx=0, int base=10);
unsigned long long stoull(const string& str, size_t* idx=0, int base=10);
float stof(const string& str, size_t* idx=0);
double stod(const string& str, size_t* idx=0);
long double stold(const string& str, size_t* idx=0);
示例如下:
const std::string toParse { " 123USD" };
size_t index { 0 };
int value { stoi(toParse, &index) };
std::cout << std::format("Parse value: {}\n", value);
std::cout << std::format("First non-parsed character: '{}'\n", toParse[index]);
输出如下:
Parsed value: 123
First non-parsed character: 'U'
stoi()、stol()、stoul()、stoll()和stoull()接收整数值并且有一个名为base的参数,表明了给定的数值应该用什么进制来表示。base的默认值为10,采用数字为0-9的十进制,base为16表示采用16进制。如果base被设为0,函数会按照以下规则自动计算给定数字的进制。
1. 如果数字以0x或0X开头,则被解析为十六进制数字。
2. 如果数字以0开头,则被解析为八进制数字。
3. 其他情况下,被解析为十进制数字。
1.2 低级数值转换函数
C++也提供了许多低级数值转换函数,这些都在<charconv>头文件中定义。这些函数不执行内存分配,也不直接使用std::string,而使用由调用者分配的缓存区。此外,它们还针对性能进行了优化,并且与语言环境无关。最终的结果是,这些函数可以比其他高级数值转换函数快几个数量级。这些函数也是为所谓的完美往返而设计的,这意味着将数值序列化为字符串表示,然后将结果字符串反序列为数值,结果与原始值完全相同。
如果希望实现高性能、完美往返、独立于语言环境的转换,则应当使用这些函数。例如,在数值数据与人类可读格式(如JSON、XML等)之间进行序列化/反序列化。
1.2.1 数值转换为字符串
要将整数转换为字符,可使用下面一组函数。
to_chars_result to_chars(char* first, char* last, IntegerT value, int base=10);
这里,IntegerT可以是任何有符号或者无符号的整数类型或char类型。结果是to_chars_result类型,类型定义如下。
struct to_chars_result {
char* ptr;
std::errc ec;
};
如果转换成功,ptr成员将等于所写入字符尾后一位置的指针。如果转换失败(即ec==std::errc::value_too_large),则它等于last。示例如下(out.data()返回的是指向out中第一个字符的指针):
const size_t BufferSize { 50 };
std::string out(BufferSize, ' '); // a string of BufferSize space character.
auto result { std::to_chars(out.data(), out.data() + out.size(), 12345) };
if ( result.ec == std::errc {} ) {
/* conversion successful. */
std::cout << out << "\n";
}
使用结构化绑定,可以将其写成:
const size_t BufferSize{ 50 };
std::string out (BufferSize, ' ');
auto [ptr, error] { std::to_chars(out.data(), out.data() + out.size(), 12345) };
if ( result.ec == std::errc {} ) {
/* conversion successful. */
std::cout << out << "\n";
}
类似地,下面的一组转换函数可用于浮点类型。
to_chars_result to_chars(char* first, char* last, FloatT value);
to_chars_result to_chars(char* first, char* last, FloatT value, chars_format format);
to_chars_result to_chars(char* first, char* last, FloatT value, chars_format format, int precision);
这里FloatT可以是float、double或long double。可使用chars_format标志的组合指定格式:
enum class chars_format {
scientific, // Style: (-)d.ddde加减dd
fixed, // Style: (-)ddd.ddd
hex, // Style: (-)h.hhhp加减d
general = fixed | scientific //看下面
};
默认格式是chars_format::general,这将导致to_chars()将浮点数转换为(-)ddd.ddd的十进制表示形式,或(-)d.ddde加减dd形式的十进制指数表示形式,得到最短的表示形式,小数点前至少有一位数字(如果存在)。如果指定了格式,但未指定精度,将为给定格式自动确定最简短的表示形式,最大精度为6个数字。例如:
const size_t BufferSize{ 50 };
double value { 0.314 };
std::string out(BufferSize, ' ');
auto [ptr, error] { std::to_chars(out.data(), out.data() + out.size(), value) };
if ( error == std::errc {} ) {
/* conversion successful */
std::cout << out << "\n";
}
1.2.2 字符串转换为数值
对于相反的转换,即将字符串转换为数值,可使用下面的一组函数。
from_chars_result from_chars(const char* first, const char* last, IntegerT& value, int base=10);
from_chars_result from_chars(const char* first, const char* last, FloatT& value, chars_format format=chars_format::general);
from_chars_result的类型定义如下:
struct from_chars_result {
const char* ptr;
std::errc ec;
};
结果类型的ptr成员指向第一个未转换字符的指针,如果所有字符转换成功,则它等于last。如果所有字符都未转换,则ptr等于first,错误代码的值将为std::errc::invalid_argument。如果解析后的值过大,无法由给定类型表示,则错误代码的值将是std::errc::result_out_of_range。注意,from_chars不会忽略任何前导空白。
to_chars()和from_chars()的完美往返特性可以表示如下:
const size_t BufferSize { 50 };
double value1 { 0.314 };
std::string out (BufferSize, ' ');
auto [ptr1, error1] { std::to_chars(out.data(), out.data() + out.size(), value1) };
if ( error1 == std::errc {} ) {
std::cout << out << "\n";
}
double value2 {};
auto [ptr2, error2] { std::from_chars(out.data(), out.data() + out.size(), value2) };
if ( error2 == std::errc {} ) {
if ( value1 == value2 ) {
std::cout << "Perfect roundtrip\n";
} else {
std::cout << "No Perfect roundtrip?!?\n";
}
}
2. std::string_view
2.1 引入
在C++17之前,为接收只读字符串的函数选择形参一直是一件进退两难的事情,它应当是const char*吗?那这样的话,如果客户使用std::string,则必须调用其上的c_str()或者data()来获取const char*。更糟糕的是,函数将失去std::string良好的面向对象的方面以及其良好的辅助方法。或许,形参应改用const std::string&?这种情况下,始终需要std::string。如果传递一个字符串字面量,编码器将默认创建一个临时字符串对象(其中包含字符串字面量的副本),并将该对象传递给函数,因此会增加一点开销。有时,人们会编写同一函数的多个重载版本,一个接收const char*,另一个接收const std::string&,但显然,这并不是一个优雅的解决方案。
在C++17中,通过引入std::string_view类解决了所有这些问题,std::string_view类是std::basic_string_view类模板的实例化,在<string_view>中定义。string_view基本上就是const std::string&的简单替代品,但不会产生开销。它从不复制字符串,string_view支持与std::string类似的接口。一个例外是缺少c_str(),但是data()是可用的。另外,string_view添加了remove_prefix(size_t)和remove_suffix(size_t)方法,前者将起始指针前移给定的偏移量来收缩字符串,后者则将结尾指针倒退给定的偏移量来收缩字符串。
如果知道如何使用std::string,那么使用std::string_view将变得十分简单,如下面的代码片段所示。extractExtension()函数提取给定文件名的扩展名(包括点号)并返回。注意,通常按值传递string_view,因为它的复制成本极低,只包含指向字符串的指针以及字符串的长度。
std::string_view extractExtension(std::string_view filename) {
return filename.substr(filename.rfind('.')); // rfind locates the last character '.' in the string.
}
该函数可用于所有类型的不同字符串:
using namespace std::literals;
std::string filename { R"(c:\temp\my file.ext)" };
std::cout << std::format("C++ String: {}\n", extractExtension(filename));
const char* cString { R"(c:\temp\my file.ext)" };
std::cout << std::format("C String: {}\n", extractExtension(cString));
std::cout << std::format("Literal: {}\n", extractExtension(R"(c:\temp\my file.ext)"));
在对extractExtension()的所有调用中,没有进行一次复制。extractExtension()函数的filename()参数只是指针和长度,该函数的返回类型也是如此,这十分高效。
还有一个string_view构造函数,它接收任意原始缓冲区和长度。这可用于从字符缓冲区(并非以NUL终止)构建string_view。如果确实有一个以NUL终止的字符串缓冲区,但你已经知道了字符串的长度,构造函数不必再次统计字符数目,这也是有用的。
const char* raw { /* ... */ };
size_t length { /* ... */ };
std::cout << std::format("Raw: {}\n", extractExtension( { raw, length } ));
最后一行代码也可以i写成这样:
std::cout << std::format("Raw: {}\n", extractExtension(std::string_view { raw, length } ));
注意:在每当函数需要将只读字符串作为一个参数时,可使用std::string_view替代const std::string&或const char*。
2.2 string_view转换成string
无法从string_view隐式构建一个string。要么使用一个显示的string构造函数,要么使用string_view::data()成员。例如,假设有以下接收const std::string&的函数。
void handleExtension(const std::string& extension) {
/* ... */
}
不能采用如下方式调用该函数:
handleExtension(extractExtension("my file.ext"));
下面是两个可供使用的选项:
handleExtension(extractExtension("my file.ext").data()); // data() method
handleExtension(std::string { extractExtension("my file.ext") }); // explicit constructor
由于同样的原因,无法连接一个string和string_view。下面的代码将无法编译:
std::string str { "Hello" };
std::string_view sv { "world" };
auto result { str + sv };
你可以对string_view使用data()方法,如下所示。
auto result1 { str + sv.data() };
或者你可以使用append():
std::string result2 { str };
result2.append(sv.data(), sv.size());
警告:返回字符串的函数应返回const std::string&或std::string,但不应该返回string_view。返回string_view会带来使返回的string_view无效的风险,例如当它指向的字符串需要重新分配时。
警告:将const string&或string_view存储为类的数据成员需要确保它们指向的字符串在对象的生命周期内保持有效状态。存储std::string更安全。
2.3 std::string_view和临时字符串
永远不要使用string_view保存临时字符串的视图,考虑以下示例:
std::string s { "Hello" };
std::string_view sv { s + " World!" };
std::cout << sv << "\n";
此代码段具有未定义的行为,即运行代码时发生的情况取决于编译器和编译器设置。它可能会打印"ello World!“,等等。为什么是未定义的行为?字符串视图sv的初始化表达式将生成一个临时字符串,其中包含”Hello World!”。然后,string_view存储指向此临时字符串的指针。在第二行代码的末尾,这个临时字符串被销毁,留下一个悬空指针的string_view。
2.4 std::string_view字面量
可使用标准用户定义的字面量sv,将字符串字面量解释为std::string_view。例如:
auto sv { "My string_view"sv };
标准用户定义的字面量sv需要以下几条using命令之一:
using namespace std::literals::string_view_literals;
using namespace std::string_view_literals;
using namespace std::literals;
using namespace std;
3. 非标准字符串
许多C++程序员都不使用C++风格的字符串,这有几个原因。一些程序员只是不知道有string类,因为它并不总是C++规范的一部分。其他程序员发现,C++String没有提供他们需要的行为,或他们不喜欢std::string对字符编码不感知这一事实,所以开发了自己的字符串类型。
也许最常见的原因是,开发框架和操作系统有自己的表达字符串的方式,例如Microsoft MFC中的CString类。它常用于向后兼容或解决遗留问题。在C++中启动新项目时,提前确定团队如何表达字符串是重要的。务必注意以下几点:
1. 不应当使用C风格的字符串表示。
2. 可对自己所使用框架中可用的字符串功能进行标准化,如MFC、QT内置的字符串功能。
3. 如果为字符串使用std::string,应当使用std::string_view将只读字符串作为参数传递给函数,否则,看一下你的框架是否支持类似于string_view的类。
参考
[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C++20高级编程(第五版)