在 C++23 中,std::string 增加了 resize_and_overwrite 成员函数,这个函数用于消除 std::string 扩容时的额外开销。下面看一个例子。
如果要将一个 std::string 初始化为 0 ~ 127 所有 ASCII 字符,在 C++23 前可能会这么写:
#include <string>
int main() {
std::string s(128, '\0');
for (std::size_t i = 0; i < s.size(); i++)
s[i] = static_cast<char>(i);
}
第一步,将 s 初始化为 128 个空字符。
第二步,使用循环为 s 的每个字节赋值。
在这个过程中,第一步的初始化是完全没有必要的,因为每个字节最终都会被新的值覆盖。
在 C++23 以前,第一步是无法省略的,简单来说,STL 提供的抽象是“元素管理器”,而不是“内存管理器”,因此 STL 保存的是已经被初始化的元素,而不会是“刚被分配还未初始化的内存”。
注意,以下写法是错误的:
#include <string>
int main() {
std::string s;
s.reserve(128);
for (std::size_t i = 0; i < s.size(); i++)
s[i] = static_cast<char>(i);
}
reserve
成员函数仅改变底层分配的内存大小,而不改变 std::string 中的元素数量。在调用 reserve
后,字符串 s 依然是空字符串,此时往里面写内容属于越界写。
在 C++23 以后,使用 resize_and_overwrite
函数可以省略无意义的初始化。下面看示例:
#include <string>
int main() {
std::string s;
s.resize_and_overwrite(128, [](char *s, std::size_t n){
for (int i = 0; i < 128; i++)
s[i] = static_cast<char>(i);
return n;
});
}
resize_and_overwrite
接受两个参数,第一个参数 n 是最大分配内存,第二个参数 op 是一个可调用对象(函数指针、重载 ()
的类、lambda 表达式等),这个对象的调用返回一个整型值。
在调用 resize_and_overwrite
时,首先会为字符串分配 n 个字节的空间,接着会调用可调用对象 op,将 s.data() 和 n 作为参数传递给 op,最后使用 op 的返回值作为字符串的实际元素数量。
结合上面的代码,首先 s 分配 128 字节的空间,然后调用第二个参数 lambda 表达式,将 s.data() 和 128 作为 lambda 表达式的参数。在 lambda 表达式里,使用 for 循环向字符串中写入 0 ~ 127 共 128 个 ASCII 字符,最后返回 n,也就是 128,表示字符串的实际元素数量是 128。调用完毕后,s.size() 为 128,s.capacity() 为 1024。
在使用这个函数时有几点需要注意:
- 可调用对象 op 的返回值不能大于第一个参数 n
- 可调用对象 op 里执行的操作不能越界写,即不能写超过 n 个字符
- 在可调用对象 op 里必须确保每个元素都被初始化
- 可调用对象 op 不能抛出异常,否则是未定义行为
使用 resize_and_overwrite
避免了字符串 resize 时多余的初始化,这也是 C++ 零成本抽象理念的具体体现。