【问题标题】:Destroy std::vector without releasing memory销毁 std::vector 而不释放内存
【发布时间】:2014-10-24 17:21:51
【问题描述】:

假设我有一个函数可以将数据输入标准向量:

void getData(std::vector<int> &toBeFilled) {
  // Push data into "toBeFilled"
}

现在我想将此数据发送到另一个函数,完成后应该释放数据:

void useData(int* data)
{
  // Do something with the data...
  delete[] data;
}

这两个函数(getData 和 useData)都是固定的,不能更改。这在复制数据一次时效果很好:

{
  std::vector<int> data;
  getData(data);
  int *heapData = new int[data.size()];
  memcpy(heapData, data.data(), data.size()*sizeof(int));
  useData(heapData);
  data.clear();
}

但是,这个 memcpy 操作代价高昂,并不是真正需要的,因为数据已经在堆上。是否可以直接提取和使用标准向量分配的数据?类似(伪代码):

{
  std::vector<int> data;
  getData(data);
  useData(data.data());
  data.clearNoDelete();
}

编辑:

这个例子可能没有太多意义,因为可以在函数调用 useData 之后释放向量。但是,在真实的代码中,useData并不是一个函数,而是一个接收数据的类,而且这个类的寿命比vector长……

【问题讨论】:

  • 恐怕你做不到。没有释放它的内存就无法清空向量......
  • 这是什么疯狂的API?!
  • @LightnessRacesinOrbit 对此 +1,这是肯定的。 Jan,您不知道数据在堆上。该标准仅要求它是连续且可随机访问的(以及其他一些内容)。像std::string 一样,对于具有合理的小对象静态缓冲区的小项目计数向量来说,这并不是闻所未闻的,一旦该页面被认为太小,就会诉诸全动态。在您寻求的用途的支持下,这样的实现会严重崩溃。
  • 好吧,你总是可以 new() 那个向量,这样它的析构函数就不会被隐式调用。我认为图像足够大,可以排除向量数据的堆栈分配。我还假设(但需要验证)useData() 的 delete[](我假设)与 std::vector 的分配兼容。但是,永远不能调用向量的析构函数,因为它会再次尝试释放该内存。因此,动态分配的向量将是内存泄漏。如果您每秒调用 useData() 26 次,这可能会成为问题。
  • @WhozCraig:当前标准不允许“小向量优化”,因为移动操作不得使迭代器无效,并且移动小向量需要复制。

标签: c++ c++11 vector std stdvector


【解决方案1】:

没有。

您使用的 API 有一个合同,规定它拥有您提供给它的数据的所有权,并且这些数据是通过指针提供的。这基本上排除了使用标准向量。

Vector 将始终确保释放它分配的内存并安全地销毁它包含的元素。这是其保证合同的一部分,您不能将其关闭。

如果您希望获得数据的所有权,您必须制作数据副本...或者每个元素移出到您自己的容器中。或者首先从您自己的 new[] 开始(呃),尽管您至少可以将所有这些包装在某个模仿 std::vector 的类中并变成非拥有者。

【讨论】:

  • 嗯......我虽然也许有一些方法可以做“交换(数据,堆数据)”或类似......
  • “Vector 将始终确保释放它分配的内存并安全地销毁它包含的元素。...你不能关闭它。” - 你可以放置new一个空的vector&lt;int&gt; 覆盖在原来的vector 上,这样析构函数——在运行时——不知道之前的分配;这里真正的问题是默认分配器不使用new[],即使它使用.data() 可能不会产生相同的值,所以.data() 值上的delete[] 是不安全的,并且编写自定义分配器来修复所有涉及不可避免的低效率 + reinterpret_cast 调用 getData()
  • @TonyD:那是verrrry UB。我认为这是一个错误的解决方案。
  • @TonyD - 这里真正的问题是std::vector 拥有它的数据。时期。当然,你可以解决未定义的行为并得到一些似乎有效的东西,但如果你这样做,你就只能靠自己了。
  • “我认为这是一个错误的解决方案” - 我实际上是在解释,尽管问题的许多部分都可以解决,但没有可移植/标准的解决方案:考虑“.data()可能不会产生相同的价值”+ 需要“reinterpret_cast。正如皮特所说 - 这些是 vector 所有权的后果,但与您狭隘和错误的说法仍然不同,即“你不能转动 [vector 释放记忆和破坏元素] 关闭”。
【解决方案2】:

这是一个可怕的 hack,它应该可以让你做你需要做的事情,但它依赖于 Undefined Behavior 做最简单的事情。这个想法是创建您自己的分配器,它与 std::allocator 布局兼容,并对向量进行类型双关:

template <class T>
struct CheatingAllocator : std::allocator<T>
{
  using typename std::allocator<T>::pointer;
  using typename std::allocator<T>::size_type;

  void deallocate(pointer p, size_type n) { /* no-op */ }

  // Do not add ANY data members!!
};


{
  std::vector<int, CheatingAllocator<int>> data;
  getData(reinterpret_cast<std::vector<int>&>(data)); // type pun, `getData()` will use std::allocator internally
  useData(data.data());
  // data actually uses your own allocator, so it will not deallocate anything
}

请注意,这与黑客行为一样骇人听闻且不安全。它依赖于内存布局不变,它依赖于std::allocator 在其allocate 函数中使用new[]。我自己不会在生产代码中使用它,但我相信这是一个(绝望的)解决方案。


@TonyD 在 cmets 中正确指出 std::allocator 很可能在内部使用 new[]。因此,上述操作很可能在useData() 内的delete[] 上失败。同样的@TonyD 也提出了一个很好的观点,即使用reserve() 来(希望)防止getData() 内部的重新分配。所以更新后的代码如下所示:

template <class T>
struct CheatingAllocator : std::allocator<T>
{
  using typename std::allocator<T>::pointer;
  using typename std::allocator<T>::size_type;

  pointer allocate(size_type n) { return new T[n]; }

  void deallocate(pointer p, size_type n) { /* no-op */ }

  // Do not add ANY data members!!
};


{
  std::vector<int, CheatingAllocator<int>> data;
  data.reserve(value_such_that_getData_will_not_need_to_reallocate);
  getData(reinterpret_cast<std::vector<int>&>(data)); // type pun, `getData()` will use std::allocator internally
  useData(data.data());
  // data actually uses your own allocator, so it will not deallocate anything
}

【讨论】:

  • 非常感谢这个(非常有创意的)解决方案 :D 但你说得对,我可能不得不进行一些大的重构......
  • 这有几个问题...首先,如果getData() 执行任何操作来触发resize,则停用的deallocate 将在数据复制到后泄漏旧内存区域新地区。其次,分配器不应使用new[],因此即使.data() 恰好产生了相同的指针,您的自定义函数继承的allocate 函数也不能是delete[]-ed。如果自定义分配器调用 new[] 本身,并且还没有操作 .contruct.destroy,我认为你只剩下 reinterpret_cast 是未定义行为的唯一来源 - 非常好。
  • @TonyD 我认为那里还有很多 UB。 getData()不会调用CheatingAllocator,它将(可能)调用std::allocator 上的所有操作,因为它认为会在std::vector&lt;int, std::allocator&lt;int&gt;&gt; 上进行操作。这就是为什么我说它依赖于std::allocator 在内部使用new[](鉴于std 的仅标题性质,至少可以验证)。
  • 固然重要,但在某些情况下,之前的reserve() 可能是避免getData() 使用分配器的实用方法。我认为std::allocator 的任何理智实现都不会在内部使用new[] - 这意味着reserve 必须默认构造所有元素,并且capacity() 而不是size() 在清理期间运行的析构函数,也很难满足不同 .construct.destroy 成员的需求。
猜你喜欢
  • 2014-11-19
  • 2023-03-22
  • 1970-01-01
  • 2016-05-07
  • 1970-01-01
  • 2014-09-12
  • 2019-10-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多