为什么将stl :: list复制元素添加到列表中?

时间:2012-10-16 14:05:26

标签: c++ stl

list的标准模板库文档说:

  

void push_back(const T& x);

     

在末尾添加元素在列表末尾添加一个新元素,   紧接其当前的最后一个元素。这个新元素的内容   被初始化为x的副本。

这些语义与Java语义有很大不同,让我感到困惑。我错过了STL的设计原则吗? “一直复制数据”?这吓到我了。如果我添加对象的引用,为什么要复制该对象?为什么不只是传递的对象?

这里必须有一个语言设计决策,但我在Stack Overflow和其他网站上发现的大部分评论都集中在与所有这些对象复制都会抛出异常这一事实相关的异常抛出问题上。如果你不复制,只是处理引用,那么所有这些异常问题就会消失。很困惑。

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

5 个答案:

答案 0 :(得分:11)

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

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

答案 1 :(得分:6)

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

答案 2 :(得分:4)

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

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

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

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

答案 3 :(得分: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::list Object*,那么您可以传入指针,然后会生成该指针的副本并传递到push_back函数中。然后,在该函数内部,该指针的另一个副本被创建并存储到容器中。

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

答案 4 :(得分:1)

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

考虑一下你想要将堆栈分配的对象添加到一个比它更长的列表的常见情况:

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

列表不能保留原始对象,因为该对象在超出范围时将被销毁。通过复制,列表获得其自身的对象,其寿命完全由其自己控制。如果是这样的话,这种直截了当的用法会崩溃并且无用,我们总是必须使用指针列表。

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