在 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++ 零成本抽象理念的具体体现。

最后修改:2025 年 05 月 21 日
如果觉得我的文章对你有用,请随意赞赏