【问题标题】:Why does stl::list copy elements added to the list?为什么 stl::list 复制元素添加到列表中?
【发布时间】:2012-10-06 15:47:36
【问题描述】:

list 的标准模板库文档说:

void push_back ( const T& x );

Add element at the end 在列表末尾添加一个新元素, 在它当前的最后一个元素之后。这个新元素的内容 被初始化为 x 的副本。

这些语义与 Java 语义有很大不同,让我很困惑。我缺少 STL 中的设计原则吗? “一直在复制数据”?这让我害怕。如果我添加对对象的引用,为什么要复制对象?为什么不只是对象通过?

这里必须有一个语言设计决定,但我在 Stack Overflow 和其他网站上找到的大多数评论都集中在与所有这些对象复制都可能引发异常的事实相关的异常引发问题上。如果您不复制,而只处理引用,那么所有这些异常问题都会消失。很困惑。

请注意:在我使用的这个遗留代码库中,boost 不是一个选项。

【问题讨论】:

    标签: c++ stl


    【解决方案1】:

    STL 始终准确地存储您告诉它存储的内容。 list<T> 始终是 T 的列表,因此所有内容都将按值存储。如果您想要一个指针列表,请使用list<T*>,这将类似于 Java 中的语义。

    这可能会诱使您尝试list<T&>,但这是不可能的。 C++ 中的引用与 Java 中的引用具有不同的语义。在 C++ 中,引用必须初始化为指向一个对象。一个引用被初始化后,它会一直指向这个对象。你永远不能让它指向不同的对象。这使得在 C++ 中不可能有一个引用容器。 Java 引用与 C++ 指针更密切相关,因此您应该改用 list<T*>

    【讨论】:

    • @John list<T&> 不是有效类型。但是,list<T*> 是。
    • @John 不,在 C++ 中不可能有一个引用容器。 C++ 引用比 Java 中的引用受限得多。它们只能被初始化一次,然后永远不会改变。 Java 引用更类似于 C++ 指针。
    • 为什么list<T&> 不是有效类型?
    • @John 基本上,因为没有语法可以复制 C++ 引用。引用是对象的另一个名称,并且该引用的 target 永远不会改变。它的行为与该语言中的任何其他容器不同。
    • 请记住,即使您有list<T*>,将元素添加到列表也不会“转移所有权”指针。 IE。每次要销毁指针列表中的元素之一时,您仍然必须手动调用 delete,因为 std::list 不会为您执行此操作。 (但boost::ptr_list 会。)
    【解决方案2】:

    这被称为“价值语义”。 C++ 通常被编码为复制值,与 Java 不同的是,除了原始类型之外,您复制引用。它可能会吓到你,但我个人觉得 Java 的引用语义更让我害怕。但是在 C++ 中,您可以选择,如果您想要引用语义,只需使用指针(最好是智能指针)。那么你会更接近你习惯的Java。但请记住,在 C++ 中没有垃圾收集(这就是为什么您通常应该使用智能指针)。

    【讨论】:

    • 问题中的方法 is 使用引用(即我假设引用语义)但文档表明值语义是真正使用的。还是我在这里遗漏了什么?
    • 引用用于避免将对象复制到函数中的开销,而不是避免将对象复制到容器中。如果您声明 std::list 其中 X 是某个类,那么您的列表将保存 X 类型的对象,而不是指向 X 的引用或指针。如果您想要在列表中指向 X 的指针,则声明 std::list .
    【解决方案3】:

    您不添加对对象的引用。您通过引用传递一个对象。那不一样。如果您没有通过引用传递,则可能在实际插入之前已经制作了额外的副本。

    它会复制,因为你需要一个副本,否则代码如下:

    std::list<Obj> x;
    {
       Obj o;
       x.insert(o);
    }
    

    会留下无效对象的列表,因为o 超出范围。如果您想要类似于 Java 的东西,请考虑使用shared_ptr。这为您提供了您在 Java 中习惯的优势 - 自动内存管理和轻量级复制。

    【讨论】:

      【解决方案4】:

      实际上,Java 的工作方式相同。请允许我解释一下:

      Object obj = new Object();
      List<Object> list = new LinkedList<Object>();
      list.add(obj);
      

      obj 的类型是什么?它是对Object引用。实际的对象在堆上的某个地方浮动——在 Java 中你唯一能做的就是传递对它的引用。您将对该对象的引用传递给列表的add 方法,并且列表本身会存储该引用的副本。您可以稍后修改命名引用obj,而不会影响存储在列表中的该引用的单独副本。 (当然,如果您修改对象本身,您可以通过任一引用看到更改。)

      C++ 有更多选择。您可以模拟 Java:

      class Object {};
      // ...
      Object* obj = new Object;
      std::list<Object*> list;
      list.push_back(obj);
      

      obj 的类型是什么?它是一个指向Object 的指针。当您将它传递给列表的push_back 方法时,列表本身会存储该指针的副本。这与 Java 具有相同的语义。

      但是,如果您从效率的角度考虑... C++ 指针/Java 引用有多大? 4 字节或 8 字节,具体取决于您的架构。如果您关心的对象大约是那个大小或更小,为什么还要费心把它放在堆上,然后到处传递指向它的指针呢?只需传递对象:

      class Object {};
      // ...
      Object obj;
      std::list<Object> list;
      list.push_back(obj);
      

      现在,obj 是一个实际对象。您将它传递给列表的push_back 方法,该方法本身存储该对象的副本。在某种程度上,这是一个 C++ 习语。它不仅对指针是纯开销的小对象有意义,而且在非 GC 语言中它也使事情变得更容易(堆上没有任何可能意外泄漏的东西),并且如果对象的生命周期自然绑定到列表中(即,如果它从列表中删除,那么在语义上它应该不再存在),那么您不妨将整个对象存储在列表中。它还具有缓存局部性优势(无论如何,当用于std::vector 时)。


      您可能会问,“那为什么push_back 采用引用参数呢?”有一个足够简单的理由。每个参数都按值传递(同样,在 C++ 和 Java 中)。如果你有一个std::listObject*,那很好——你传入你的指针,然后该指针的副本 被制作并传递给push_back 函数。然后,在该函数内部,创建该指针的另一个副本并将其存储到容器中。

      这对于指针来说很好。但是在 C++ 中,复制对象可以任意复杂。复制构造函数可以做任何事情。在某些情况下,将对象复制两次(一次复制到函数中,再复制到容器中)可能会导致性能问题。所以push_back 通过 const 引用获取它的参数——它制作一个副本,直接从原始对象到容器中。

      【讨论】:

      • C++11 移动语义值得一提,因为可以有效地消除 OP 似乎担心的通过复制引起的性能损失,并且确实可以“传递”对象。跨度>
      【解决方案5】:

      如果没有引用计数,就无法维持共享所有权,因此通过复制来维持单一所有权。

      考虑一个常见的情况,您希望将一个堆栈分配的对象添加到一个比它寿命更长的列表中:

      void appendHuzzah(list<string> &strs) {
        strs.push_back(string("huzzah!"));
      }
      

      列表无法保留原始对象,因为该对象超出范围时将被销毁。通过复制,列表获得了它自己的对象,其生命周期完全在它自己的控制之下。如果不是这样,那么这种直接的用法就会崩溃并且毫无用处,我们将总是必须使用指针列表。

      Java 区分原始类型和引用类型。在 C++ 中,所有类型都是原始类型。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-07-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多