销毁std::vector而不释放内存。

12

假设我有一个用于将数据存入 std vector 的函数:

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分配的数据是否可行?类似以下伪代码:

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

编辑:

这个例子可能没有太多的意义,因为可以在调用useData函数后直接释放向量。然而,在真正的代码中,useData不是一个函数,而是一个接收数据的类,并且这个类的生命周期比向量更长...


4
恐怕你做不到。除非释放内存,否则无法清空向量的内容。 - jpo38
24
这是什么疯狂的API啊?! - Lightness Races in Orbit
1
@LightnessRacesinOrbit 对此表示赞同。而且Jan,你并不知道数据在堆上。标准只规定它是连续的和随机访问的(还有其他一些东西)。像std::string一样,对于一个具有合理小对象静态缓冲区的小项计数向量来说,如果该页面被认为太小,则不会听到完全动态的声音。这样的实现将在您寻求的使用前提下严重崩溃。 - WhozCraig
1
你可以使用new()来创建这个向量,这样它的析构函数就不会被隐式调用。我想这个图像足够大,可以排除向量数据的堆栈分配。我也假设(但需要验证)useData()的delete[](我假设)与std::vector的分配兼容。但是,永远不能调用向量的析构函数,因为它会尝试再次释放该内存。因此,动态分配的向量将成为内存泄漏。如果您每秒调用useData() 26次,那可能会成为一个问题。 - Peter - Reinstate Monica
3
当前标准不允许进行"小向量优化",因为移动操作不能使迭代器失效,而移动一个小向量则需要进行复制。 - MadScientist
显示剩余9条评论
2个回答

26

序号

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

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

如果您希望拥有这些数据,则必须复制这些数据...或者将每个元素移出到自己的容器中。或者从一开始就使用自己的new[](呕),尽管您至少可以将所有这些内容包装在类中,该类模仿了std::vector并变为非拥有。


嗯...我想或许有一种方法可以执行"swap(data, heapData)"或类似的操作... - Jan Rüegg
2
"Vector一定会确保释放其分配的内存并安全销毁包含的元素。...您无法关闭此功能。" - 您可以将一个空的vector<int>放置在原始的vector上,这样当析构函数运行时就不会知道先前的分配; 真正的问题在于默认分配器不使用new[],即使它使用.data()也可能不产生相同的值,因此对.data()值进行delete[]是不安全的,并且编写自定义分配器以修复所有这些都涉及到不可避免的低效率和reinterpret_cast来调用getData()。" - Tony Delroy
4
@TonyD:那太不好了。我认为这是一个错误的解决方案。 - Lightness Races in Orbit
1
@TonyD - 这里的真正问题在于std::vector拥有其数据。毫无疑问,你可以通过未定义行为进行黑客攻击,并获得似乎可行的结果,但如果你这样做,你就是自己的了。 - Pete Becker
1
我认为这是一种错误的解决方案 - 我实际上是在解释,尽管问题的许多部分可以得到解决,但并没有一个可移植/标准的解决方案:考虑“.data()可能不会产生相同的值”+需要“reinterpret_cast”。正如Pete所说 - 这些是vector所有权的后果,但仍然不同于您更狭窄和错误的声明,“您无法关闭[vector释放内存和销毁元素]”。 - Tony Delroy

7
这里有一个可怕的技巧,它可以让你实现你所需的功能,但它依赖于未定义行为执行最简单的操作。想法是创建一个与std :: allocator兼容的布局相同的分配器,并将vector类型转换。
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在其分配功能中使用new []。我自己不会在生产代码中使用它,但我认为这是一个(绝望的)解决方案。
@TonyD在评论中正确指出,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 但是您说得对,我可能需要进行某种大规模重构... - Jan Rüegg
这里有几个问题...首先,如果getData()触发了resize,那么未激活的deallocate将在数据被复制到新区域后泄漏旧内存区域。其次,分配器不应使用new[],而您的自定义分配器继承的allocate函数因此无法被delete[],即使相同的指针恰好由.data()产生。如果自定义分配器本身调用new[],并且还没有操作.contruct.destroy,我认为你只能用reinterpret_cast作为未定义行为的唯一来源 - 这很好。 - Tony Delroy
1
@TonyD 我认为里面还有很多未定义行为。getData()不会调用CheatingAllocator,它将(大概)调用所有在std::allocator上的操作,因为它认为自己在操作std::vector<int, std::allocator<int>>。这就是我说它依赖于std::allocator内部使用new[]的原因(鉴于std是头文件库,至少可以验证)。 - Angew is no longer proud of SO
1
实际上,有些情况下先使用reserve()可能是避免getData()使用分配器的一种实用方法。我认为任何理智的std::allocator实现都不会在内部使用new[] - 这意味着reserve必须默认构造所有元素,并且在清理期间运行capacity()而不是size()析构函数,这也很难与需要不同的.construct.destroy成员相协调。 - Tony Delroy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接