存储指向 std::string 数据的指针是否安全?



我的问题围绕着复制构建和重新分配的机制。

我有一个收集字符串的类。将字符串添加到集合后,将复制字符串并将其存储在向量中。但是由于我还需要访问所有字符串的集合作为const char * const*,我还通过.c_str()存储指向每个字符串数据的指针。

class MyStrings {
private:
std::vector<std::string> names;
std::vector<const char*> cStringPointers;
public:
const char *const *Data() const
{
return this->cStringPointers.data();
}
void Add(const std::string &name)
{
// copy [name] and store the copy in [this->names].
this->names.push_back(name); 
// Store the pointer to the data of the copy.
this->cStringPointers.push_back(this->names.back().c_str());
}
}

我知道,存储指向向量元素的指针是不好的,因为当向量被调整大小时,即必须重新分配他的内存,这些指针将不再有效。

但我只存储指向数据的指针。所以这就是我的想法:

如果调整names的大小,它将移动构造它包含的所有字符串,因此这些字符串不会分配新内存,而只是使用已分配的内存,因此我在cStringPointers中的指针仍然有效。

我的问题现在很简单:我是否错过了一些会使此代码不安全或导致未定义行为的内容?

(假设我没有使用任何外来的架构纹理和/或编译器。

我现在的问题很简单:我是否错过了一些会让 此代码不安全或导致未定义的行为?

是的:您错过了小字符串优化。它是标准允许并广泛实现的,并且当字符串实际将其数据移动到新位置时,将导致指针悬空。

这是不安全的。即使是cStringPointers也不安全。

请注意,大多数编译器的标准库实现了称为:小字符串优化(SSO(的东西。基本上在 SSO 中,如果字符串很小(在 gcc 15 个字符中(,该字符串的内存不会在堆中分配,而是直接保存在类std::basic_string内。要实现这一点,std::basic_string比指针所需的大小(开始、结束、容量(要大。

这意味着如果向量被重新定位,小字符串将改变它们的位置。 较长的字符串将保持有效,因为它们是在堆上分配的,不会被复制。

我现在的问题很简单:我是否错过了一些会使此代码不安全或导致未定义行为的内容?

是的。这个特定的假设取决于实现,因此 UB 即使std::string的任何常见实现都会移动字符串的数据并保持指针有效。只有当这样的细节真正得到标准的保证时,你才能依靠它。(常见于标题为"迭代器有效性"等的章节。在std::string的移动构造函数(第 2 号(的文档中,它明确指出:

与其他容器移动分配不同,对 str 的引用、指针和迭代器可能会失效。

在这里,对于大多数实现来说,这个假设实际上是错误的,因为这些实现使用一个小字符串优化。这将在string对象本身中存储最大大小的字符串("小字符串"(,而不是动态分配内存。因此,当移动string时,它只能避免复制动态分配的长字符串,而实际复制小字符串。因此c_str()将在移动小字符串后产生不同的指针。

只需添加来自C++标准 [string.require.4] 的相关引用:

引用

basic_­string序列元素的引用、指针和迭代器可能会因该basic_­string对象的以下用法而失效

— 作为参数传递给任何标准库函数,将对非常量basic_­string的引用作为参数。

向量重新分配期间字符串的移动构造正是这种情况,因为移动构造函数将对非常量字符串的引用作为参数。

最新更新