【问题标题】:Why is creating STL containers dynamically considered bad practice?为什么动态创建 STL 容器被认为是不好的做法?
【发布时间】:2011-02-21 18:38:43
【问题描述】:

标题说明了。

不良行为示例:

std::vector<Point>* FindPoints()
{
   std::vector<Point>* result = new std::vector<Point>();
   //...
   return result;
}

如果我稍后删除 vector 有什么问题?

我主要用 C# 编程,所以这个问题在 C++ 上下文中对我来说不是很清楚。

【问题讨论】:

  • 记住每个概括都是错误的(很好的概括,嗯?;-))
  • @6502 我给了一个样本,让问题不那么笼统

标签: c++ stl


【解决方案1】:

根据经验,您不会这样做,因为您在堆上分配的内存越少,内存泄漏的风险就越小。 :)

std::vector 也很有用,因为它以 RAII 方式自动管理用于向量的内存;现在通过在堆上分配它,您需要显式释放(使用delete result)以避免泄漏其内存。由于例外情况,事情变得复杂,这可能会改变您的返回路径并跳过您在路上放置的任何delete。 (在 C# 中你没有这样的问题,因为无法访问的内存只是被垃圾收集器定期回收)

如果您想返回一个 STL 容器,您有多种选择:

  • 只需按值返回即可;从理论上讲,由于在返回result 的过程中创建了临时文件,因此您应该受到复制惩罚,但是较新的编译器应该能够使用NRVO1 删除副本。也可能有std::vector 实现像许多std::string 实现一样实现写时复制优化,但我从未听说过。

    在 C++0x 编译器上,应该触发移动语义,避免任何复制。

  • 将结果指针存储在所有权转移智能指针中,如std::auto_ptr(或C++0x 中的std::unique_ptr),并将函数的返回类型更改为std::auto_ptr&lt;std::vector&lt;Point &gt; &gt;;这样,您的指针总是被封装在一个堆栈对象中,当函数退出时(以任何方式)自动销毁,如果 vector 仍归它所有,则销毁它。此外,返回的对象归谁所有,一目了然。

  • 使result 向量成为调用者通过引用传递的参数,并填充该参数而不是返回新向量。

  • 硬核 STL 选项:您可以将数据作为迭代器提供;然后,客户端代码将使用std::copy+std::back_inserter 或其他任何东西将此类数据存储在它想要的任何容器中。很少见(正确编码可能很棘手),但值得一提。


  1. 正如@Steve Jessop 在 cmets 中指出的那样,NRVO 只有在调用方法中直接使用返回值初始化变量时才能完全工作;否则,它仍然可以省略临时返回值的构造,但仍然可以调用分配返回值的变量的赋值运算符(有关详细信息,请参阅@Steve Jessop 的 cmets)。

【讨论】:

  • 感谢您的出色回答!我个人更喜欢第二种选择,但这会使类型非常长,所以你必须使用typedefs 或defines,我也不太喜欢 :)
  • 在这种情况下,使用 std::auto_ptr 不是一个好主意。它们不能很好地与 STL 容器配合使用。
  • @John Gaughan 如果你把auto_ptr inside 容器,他们不会,但事实并非如此
  • @John:我知道你不应该使用std::auto_ptr 作为STL 容器的元素,但我不明白为什么你不应该使用它们来管理在堆上分配的 STL 容器的生命周期。
  • "较新的编译器应该能够使用 NRVO 删除副本" - 仅当结果用于初始化变量时。如果它被分配给一个以前存在的变量,那么复制构造函数省略可以摆脱临时的,但仍然有一个不能被省略的赋值。 C++0x 移动语义也解决了这种情况(通过使赋值便宜)。因此,对于 C++0x,不再有太多理由避免按值返回标准容器,但对于 C++03,则存在涉及完整副本的尴尬情况。
【解决方案2】:

除非真的有必要,否则动态创建任何东西都是不好的做法。动态创建容器很少有充分的理由,因此通常不是一个好主意。

编辑:通常,大多数代码应该只处理容器中的一个(或两个)迭代器,而不是担心返回容器的速度有多快或多慢。

【讨论】:

  • 您几乎可以完全避免在代码中使用动态分配,但它会在每次调用时复制所有内容。我认为这对性能没有那么好。
  • 动态创建对象非常好,甚至是容器。这里的问题是返回那个指针——它打开了程序,发现了一类很难检测到的全新错误。
  • @Andrey:如果你通过引用传递一些东西,它也不会被复制。如果你不知道,“Accelerated C++”是一本很棒的 C++ 书,直到第 10 章才涉及指针,这应该告诉你一些事情;)
  • @etarion 我知道引用,但是你不能返回对局部变量的引用也不是个好主意。
  • @Andrey:当“每次通话”时,我认为您的意思是传递参数。对于返回值,有 RVO,几乎每个现代编译器都使用它。
【解决方案3】:

一般来说,动态创建对象在 C++ 中被认为是一种不好的做法。如果您的“//...”代码抛出异常怎么办?您将永远无法删除该对象。这样做更容易更安全:

std::vector<Point> FindPoints()
{
  std::vector<Point> result;
  //...
  return result;
} 

更短、更安全、更直接...至于性能,现代编译器会在返回时优化掉副本,如果无法优化,移动构造函数将被执行,因此这仍然是一个廉价的操作。

【讨论】:

  • 我不会说“一般”。 std::unique_ptr&lt;C&gt; c(new C); 很好。问题在于“愚蠢的指针”。
  • 整个底层容器将在返回时被复制。我简直不敢相信这是廉价的操作。我仍然完全同意异常问题,但可以使用auto_ptr 解决
  • 移动构造函数只存在于最近打开了 C++0x 扩展的编译器中。一般来说,如果对象的创建和返回发生在一个序列点,编译器将优化复制构造。
  • @Andrey:不要使用auto_ptr真的是不好的做法。不,它不会被复制。 Google 用于命名返回值优化。
  • @etarion 这是一个全新的主题,但auto_ptr 有什么问题,尤其是在简单的情况下(比如这个)
【解决方案4】:

也许您指的是最近的这个问题:C++: vector<string> *args = new vector<string>(); causes SIGABRT

一条线:这是一种不好的做法,因为它是一种容易发生内存泄漏的模式。

您正在强制调用者接受动态分配并负责其生命周期。从声明中可以看出,返回的指针是静态缓冲区、其他 API(或对象)拥有的缓冲区还是调用者现在拥有的缓冲区。你应该避免在任何语言(包括纯 C)中使用这种模式,除非从函数名称中可以清楚地知道发生了什么(例如 strdup、malloc)。

通常的方法是这样做:

void FindPoints(std::vector<Point>* ret) {
   std::vector<Point> result;
   //...
   ret->swap(result);
}

void caller() {
  //...
  std::vector<Point> foo;
  FindPoints(&foo);
  // foo deletes itself
}

所有对象都在堆栈上,所有删除都由编译器负责。或者只是按值返回,如果您正在运行 C++0x 编译器+STL,或者不介意副本。

【讨论】:

  • 哦,不仅仅是内存泄漏:如果另一个函数看起来像 FindPoints() 但返回了一个指向缓冲区的指针,该缓冲区不是只为调用者分配的?然后调用者错误地删除它,你会得到双重删除和堆损坏。
  • 是的,这个问题启发了我 :) 好吧,通常的方式看起来有点奇怪,因为它会混淆返回值和参数
  • 通过作为非常量指针传递来推断参数是返回值是标准做法(无论如何都是“Effective C++”风格)。它是旧编译器和 C++0x 编译器的有效返回机制。
【解决方案5】:

我喜欢 Jerry Coffin 的回答。此外,如果您想避免返回副本,请考虑将结果容器作为引用传递,有时可能需要 swap() 方法。

void FindPoints(std::vector<Point> &points)
{
    std::vector<Point> result;
    //...
    result.swap(points);
}

【讨论】:

    【解决方案6】:

    编程是寻找良好折衷方案的艺术。动态分配的内存当然可以有一些地方,我什至可以考虑使用std::vector&lt;std::vector&lt;T&gt;*&gt;在代码复杂性和效率之间取得良好折衷的问题。

    然而std::vector 在隐藏动态分配数组的大多数需求方面做得很好,托管指针很多时候只是动态分配单个实例的完美解决方案。这意味着,非托管动态分配容器(或实际上是动态分配的容器)是 C++ 中的最佳折衷方案的情况并不常见。

    在我看来,这不会使动态分配“不好”,但如果您在代码中看到它只是“怀疑”,因为很有可能会有更好的解决方案。

    以您为例,我认为没有理由使用动态分配;只是让函数返回一个 std::vector 将是高效和安全的。使用任何体面的编译器Return Value Optimization 将在分配给新声明的向量时使用,如果您需要将结果分配给现有向量,您仍然可以执行以下操作:

    FindPoints().swap(myvector);
    

    这不会对数据进行任何复制,而只是进行一些指针旋转(请注意,您不能使用显然更自然的myvector.swap(FindPoints()),因为有时令人讨厌的 C++ 规则禁止将临时对象作为非常量引用传递)。

    根据我的经验,动态分配对象的最大需求来源是复杂的数据结构,其中可以使用多个访问路径访问同一个实例(例如,实例同时位于双向链表中并由映射索引) .在标准库中,容器始终是所包含对象的唯一所有者(C++ 是一种复制语义语言),因此如果没有指针和动态分配概念,可能很难有效地实现这些解决方案。

    通常,您仍然可以使用标准容器进行足够合理的折衷(可能需要支付一些您本可以避免的额外 O(log N) 查找费用),并且考虑到更简单的代码,这可能是 IMO 的最佳折衷方案在大多数情况下。

    【讨论】:

      猜你喜欢
      • 2010-10-22
      • 2011-11-20
      • 1970-01-01
      • 2010-11-04
      • 2022-07-05
      相关资源
      最近更新 更多