移动带有新位置的 std::string 后内存泄漏

Memory leak after moving a std::string with placement new

我有一个包含字符串的结构,并且该结构用在向量中。当向量增长时,所有元素都被移动到新的分配。不幸的是,此举也会导致 std::string 内存泄漏。

这里有一些最小的可重现案例。第一个示例将说明内存泄漏发生的位置,但可以理解它。第二个例子将解决困扰我的内存泄漏问题。第三个将更进一步。最后,我将展示实际用例来展示我什至在做什么以及我为什么问这个问题。

int main(void)
{
  char* allocation = new char[sizeof(std::string)];
  std::string start("start");
  std::string* move = (std::string*)allocation;
  new (move) std::string(std::move(start));
  delete[] allocation;
}

毫不奇怪,这会导致内存泄漏。 start 字符串构造完成,应该为数据分配内存。然后将该字符串移入 move 并在程序的其余部分保留在那里。因为它已被移动,所以 move 的析构函数将不会被调用,这将导致如下所示的内存泄漏。我创建字符数组的原因是为了避免对 move 字符串的默认构造函数和析构函数调用。

Detected memory leaks!
Dumping objects ->
{207} normal block at 0x00000278D16848C0, 16 bytes long.
 Data: <  g x           > D0 D7 67 D1 78 02 00 00 00 00 00 00 00 00 00 00
Object dump complete.

这里需要注意的一件有趣的事情是数据 < g x > 不包含用于初始化 start 的字符串,这可能表明此泄漏不是来自字符串的缓冲区。

现在我们将在前面的示例中添加一行以尝试消除内存泄漏。

int main(void)
{
  char* allocation = new char[sizeof(std::string)];
  std::string start("start");
  std::string* move = (std::string*)allocation;
  new (move) std::string(std::move(start));
  start = std::move(*move); // The new line.
  delete[] allocation;
}

现在,该字符串不再保留在 move 中直到程序结束,而是移回 start 并且由于应调用 start's 析构函数,字符串的分配应该被释放。但是,当我 运行 这样做时,我仍然遇到类似的内存泄漏。

Detected memory leaks!
Dumping objects ->
{207} normal block at 0x000001AE813FECC0, 16 bytes long.
 Data: <  @             > 80 15 40 81 AE 01 00 00 00 00 00 00 00 00 00 00
Object dump complete.

我也尝试过使用单个字符串分配而不是分配字符数组进行此测试,但我仍然遇到与下面的代码类似的内存泄漏。

int main(void)
{
  std::string start("start");
  std::string* move = new std::string;
  new (move) std::string(std::move(start));
  start = std::move(*move);
  delete move;
}

我做这一切的原因是因为我正在编写我自己的矢量版本。当我的向量增长,并且向量中包含的类型包含一个字符串时,就会发生我在上面突出显示的内存泄漏。这就是我的成长功能的样子。 Util::Move 是我自己版本的 std::move。出于好奇、学习和娱乐的目的,我做了很多这样的事情。

template<typename T>
void Vector<T>::Grow(int newCapacity)
{
  LogAbortIf(
    newCapacity <= mCapacity,
    "The new capacity must be greater than the current capacity.");

  T* oldData = mData;
  mData = CreateAllocation(newCapacity);
  for (int i = 0; i < mSize; ++i)
  {
    new (mData + i) T(Util::Move(oldData[i]));
  }
  mCapacity = newCapacity;
  DeleteAllocation(oldData);
}

// Using this struct in the vector would cause a leak.
struct Example
{
  std::string mString;
};

为什么会发生这种内存泄漏?关于 std::string 有什么我在这里遗漏的吗?或者我使用 placement new 不知何故是罪魁祸首?

补充说明: Memory leak in placement new of standard library string - 这个问题只涉及我的第一个例子。它没有解释或涵盖最后两个。

从对象移动并不意味着不需要调用析构函数。 任何个构造对象需要被析构:

{
    std::string s;
    std::move(s);
} // s’ destructor is run here!

您代码中通过 placement-new 显式构造的对象(地址 move)也是如此。问题是,因为它驻留在动态分配的内存中,所以它的析构函数不会自动 运行 ,并且当您删除内存底层的 char array 时,您告诉 C++ 不要考虑该存储位置的字节被 std::string 对象占用。

简而言之,解决方案是在释放存储之前手动 运行 std::string 析构函数:

move->std::string::~string();
delete[] allocation;

对每个新展示位置执行此操作。

Because it has been moved, the destructor for move will not be called

不,这是错误的。不会调用称为 *move 的对象的析构函数,因为 move 是原始指针。

如果要自动调用它,它会在您删除基础分配后发生,所以它无论如何都会导致 UB。这段代码完全没有意义。

The reason I am creating a character array is to avoid the constructor and destructor calls for the move string

你明明说placement是new的,这也是废话。故意调用move构造函数如何避免构造函数?

唯一避免的是析构函数,这是一个错误而不是优化。

... this means that start's destructor should be called and the string's allocation should be freed ...

start的析构函数无论你做什么都会被调用,因为它是一个自动本地。它也在第一个示例中被调用。但是,您假设移动 from *move 意味着可以不调用其析构函数。这不是真的,它仍然是一个错误(即使它没有泄漏仍然是一个错误)。

您的最终测试动态分配(和默认构造)一个字符串,然后在其存储上放置新的另一个字符串,而不是先销毁它。这也是胡说八道。


你遇到的问题依次是:

  1. 您没有显示泄漏的真实代码,因此我们无法真正解释该泄漏。你所有的例子都是错误的,所以我们只能推测你的真实代码可能以同样的方式出错。

  2. 您正在尝试优化可能不会从中受益的内容。许多库实现将对五个字符的字符串使用短字符串优化 - 看看你的是不是其中之一。

    就此而言,在开始优化代码之前不要忘记分析您的代码。

  3. 您正在尝试使用您不了解的技术来优化可能不需要它的内容。

    您需要了解对象生命周期规则,并且在调用构造函数和析构函数时,您开始尝试使用 placement new 之前。手动控制对象和存储生命周期是相当高级的,除非您首先了解默认行为,否则您无法正确地做到这一点。

    通常的学习实验是写一个玩具class,实现所有构造函数、赋值运算符和析构函数,并打印一些东西。然后,您可以轻松地 看到 将这些示例中的 std::string 替换为您的 class 会发生什么。