12

我目前正在学习 C++ 并试图习惯它附带的标准数据结构,但它们看起来都很简单。例如,list 没有像我在 Java 中习惯的 get(index) 这样的简单访问器。pop_back 和 pop_front 等方法也不返回列表中的对象。因此,您必须执行以下操作:

Object blah = myList.back();
myList.pop_back();

而不是像这样简单的东西: Object blah = myList.pop_back();

在 Java 中,几乎每个数据结构都会返回对象,因此您不必进行这些额外的调用。为什么 C++ 的 STL 容器是这样设计的?我在 Java 中执行的此类常见操作对于 C++ 来说不是很常见吗?

编辑:对不起,我想我的问题措辞很糟糕,无法获得所有这些反对意见,但肯定有人可以编辑它。为了澄清,我想知道为什么 STL 数据结构与 Java 相比是这样创建的。还是我一开始就使用了错误的数据结构集?我的观点是,这些看起来像是您可能在(在我的示例中)列表上使用的常见操作,并且每个人肯定不想每次都编写自己的实现。

编辑:将问题改写为更清楚。

4

7 回答 7

11

很多人已经回答了你提出的具体问题,所以我会试着看一下大图。

Java 和 C++ 之间必须的根本区别之一是 C++ 主要处理值,而 Java 主要处理引用。

例如,如果我有类似的东西:

class X {
    // ...
};

// ...
X x;

在 Java 中,x它只是对 X 类型对象的引用。为了有一个实际的类型对象X供它引用,我通常有类似的东西:X x = new X;. 然而,在 C++ 中,X x;本身定义了一个类型为 的对象X,而不仅仅是对对象的引用。我们可以直接使用该对象,而不是通过引用(即伪装的指针)。

尽管这最初看起来似乎是一个相当微不足道的差异,但其影响是巨大且普遍的。一个效果(在这种情况下可能是最重要的)是在 Java 中,返回值根本涉及复制对象本身。它只涉及复制对该值的引用。这通常被认为非常便宜并且(可能更重要的是)完全安全——它永远不会抛出异常。

在 C++ 中,您直接处理值。当您返回一个对象时,您不仅返回了对现有对象的引用,而且还返回了该对象的值,通常以该对象状态副本的形式。当然,如果您愿意,也可以返回一个引用(或指针),但要做到这一点,您必须使其明确。

标准容器(如果有的话)甚至更倾向于使用值而不是引用。当您向集合添加值时,添加的是您传递的值的副本,当您返回某些内容时,您将获得容器本身中值的副本。

除此之外,这意味着虽然返回值可能像在 Java 中一样便宜且安全,但它也可能很昂贵和/或引发异常。如果程序员想要存储指针,他/她当然可以这样做——但这种语言不像 Java 那样需要它。由于返回对象可能很昂贵和/或抛出异常,因此标准库中的容器通常是围绕确保在复制成本高昂时它们可以正常工作的,并且(更重要的是)即使在/如果复制引发异常时也能正常工作。

设计上的这种基本差异不仅说明了您指出的差异,而且还说明了更多差异。

于 2012-10-29T21:15:38.247 回答
5

back()返回对向量最后一个元素的引用,这使得它几乎可以自由调用。 pop_back()调用向量的最后一个元素的析构函数。

所以显然pop_back()不能返回对被破坏元素的引用。因此,要使您的语法正常工作,pop_back()必须在元素被销毁之前返回该元素的副本。

现在,如果您不想要该副本,我们只是不必要地制作了一个副本。

C++ 标准容器的目标是为您提供近乎裸机的性能,并以美观、易于使用的方式包装。在大多数情况下,它们不会为了易用性而牺牲性能——pop_back()返回最后一个元素的副本会为了易用性而牺牲性能。

可能有一个pop-and-get-back 方法,但它会复制其他功能。在许多情况下,它的效率会低于 back-and-pop。

作为一个具体的例子,

vector<foo> vec; // with some data in it
foo f = std::move( vec.back() ); // tells the compiler that the copy in vec is going away
vec.pop_back(); // removes the last element

请注意,移动必须在元素被销毁之前完成以避免创建额外的临时副本...... pop_back_and_get_value()必须在元素返回之前销毁元素,并且分配将在它返回之后发生,这是浪费的。

于 2012-10-29T20:05:50.230 回答
4

列表没有 get(index) 方法,因为通过索引访问链表效率非常低。STL 的理念是只提供可以在一定程度上有效实施的方法。如果您想在效率低下的情况下按索引访问列表,那么您自己很容易实现。

pop_back不返回副本的原因是函数返回后会调用返回值的拷贝构造函数(RVO/NRVO除外)。如果此复制构造函数引发异常,则您已从列表中删除该项目而没有正确返回副本。这意味着该方法不是异常安全的。通过分离这两个操作,STL 鼓励以异常安全的方式进行编程。

于 2012-10-29T20:11:22.650 回答
3

为什么 C++ 的 STL 容器是这样设计的?

我认为 Bjarne Stroustrup说得最好

C++ 精益求精。基本原则是你不用为你不使用的东西付费。

对于pop()将返回项目的方法,请考虑为了既删除项目又返回项目,该项目不能通过引用返回。所指对象不再存在,因为它刚刚被pop()编辑。它可以通过指针返回,但前提是您new复制原始文件,这是浪费。因此,它很可能按有可能进行深层复制的值返回。在许多情况下,它不会进行深度复制(通过复制省略),而在其他情况下,深度复制将是微不足道的。但在某些情况下,例如大缓冲区,该副本可能非常昂贵,而在少数情况下,例如资源锁,它甚至可能是不可能的。

C++ 旨在通用,并且旨在尽可能快。通用并不一定意味着“易于用于简单的用例”,而是“适用于最广泛的应用程序的平台”。

于 2012-10-29T20:58:02.827 回答
2

关于pop()-like 函数,有两件事(至少)需要考虑:

pop_back()1) 对于退货或pop_front()没有什么可退货的情况,没有明确和安全的行动。

2) 这些函数将按值返回。如果在容器中存储的类型的复制构造函数中抛出异常,则该项目将从容器中删除并丢失。我想这被认为是不受欢迎和不安全的。

关于列表的访问,标准库的一般设计原则是不避免提供低效的操作。std::list是一个双链表,通过索引访问列表元素意味着从头或尾遍历列表,直到到达所需的位置。如果你想这样做,你可以提供你自己的帮助函数。但是,如果您需要随机访问元素,那么您可能应该使用列表以外的结构。

于 2012-10-29T19:58:55.070 回答
2

list 甚至没有像 get(index) 这样的简单访问器

为什么要呢?一种允许您从 中访问第 n 个元素的方法list会隐藏操作的复杂性,O(n)这就是 C++ 不提供它的原因。出于同样的原因,C++std::vector不提供pop_front()函数,因为该函数也具有O(N)向量的大小。

pop_back 和 pop_front 等方法也不返回列表中的对象。

原因是异常安全。此外,由于 C++ 具有自由函数,因此不难为操作std::list或任何标准容器编写这样的扩展。

template<class Cont>
typename Cont::value_type return_pop_back(Cont& c){
  typename Cont::value_type v = c.back();
  c.pop_back();
  return v;
}

不过应该注意的是,上述函数不是异常安全的,这意味着如果return v;抛出异常,您将拥有一个已更改的容器和一个丢失的对象。

于 2012-10-29T20:06:24.263 回答
1

在 Javapop中一个通用接口可以返回对弹出对象的引用。

在 C++ 中,返回对应的是按值返回。

但在不可移动的非 POD 对象的情况下,复制构造可能会引发异常。然后,一个元素将被删除,但客户端代码无法访问。始终可以根据更基本的检查器和纯 popper 来定义便利的按值返回 popper,但反之则不然。

这也是哲学上的差异。

对于 C++,标准库只提供基本的构建块,而不是直接可用的功能(通常)。这个想法是您可以从数千个第三方库中自由选择,但选择自由需要付出巨大的代价,在可用性、可移植性、培训等方面。相比之下,使用 Java,您几乎拥有您需要的一切(对于标准库中的典型 Java 编程),但您实际上不能自由选择(这是另一种成本)。

于 2012-10-29T21:02:32.837 回答