我目前正在学习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 ++中不常见吗?
编辑:对不起,我想我的问题措辞很差,以获得所有这些downvotes,但肯定有人可以编辑它。为了澄清,我想知道为什么与Java相比,这样创建STL数据结构。或者我开始使用错误的数据结构集?我的观点是,这些似乎是您可能在(在我的示例中)列表中使用的常见操作,当然每个人都不想每次都编写自己的实现。
编辑:将问题重新编写为更清晰。
答案 0 :(得分: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那样需要。由于返回一个对象可能昂贵和/或抛出,标准库中的容器通常构建,以确保它们在复制成本高昂时能够合理地工作,并且(更多)重要的是)正常工作,即使/如果复制引发异常。
这种设计的基本差异不仅考虑了你所指出的差异,还考虑了不同的差异。
答案 1 :(得分:5)
back()
返回对向量的最后一个元素的引用,这使得它几乎可以自由调用。 pop_back()
调用向量的最后一个元素的析构函数。
很明显pop_back()
无法返回对被销毁元素的引用。因此,为了使您的语法有效,pop_back()
必须在元素被销毁之前返回该元素的副本。
现在,在你不想要那份副本的情况下,我们只是不必要地复制了一份。
C ++标准容器的目标是为您提供近乎裸露的金属性能,包裹在漂亮,易于使用的敷料中。在大多数情况下,它们不会牺牲性能以方便使用 - 并且返回最后一个元素副本的pop_back()
将牺牲性能以便于使用。
可能有一个pop-and-get-back方法,但它会复制其他功能。而且在许多情况下效率低于反弹。
作为一个具体的例子,
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()
必须在返回之前销毁该元素,并且在返回后将进行分配,这很浪费。
答案 2 :(得分:4)
列表没有get(index)方法,因为按索引访问链表是非常低效的。 STL的理念是只提供可以有效实施的方法。如果您希望尽管效率低下,仍然可以通过索引访问列表,那么您自己很容易实现。
pop_back不返回副本的原因是函数返回后将调用返回值的复制构造函数(不包括RVO / NRVO)。如果此复制构造函数抛出异常,则表示您已从列表中删除该项,而未正确返回副本。这意味着该方法不会是异常安全的。通过分离这两个操作,STL鼓励以异常安全的方式进行编程。
答案 3 :(得分:3)
为什么C ++的STL容器设计得像这样?
我认为Bjarne Stroustrup put it best:
C ++精益求精。基本原则是你不付钱 对于你不使用的东西。
如果pop()
方法将返回该项,请考虑为了同时删除该项并将其返回,该项无法通过引用返回。指称对象不再存在,因为它只是pop()
。它可以通过指针返回,但只有在您制作原始的new
副本时才会返回,这很浪费。因此很可能会返回值有可能进行深层复制的值。在许多情况下,它不会进行深层复制(通过复制省略),而在其他情况下,深层复制将是微不足道的。但在某些情况下,例如大缓冲区,这种复制可能非常昂贵,而且在一些情况下,例如资源锁,甚至可能是不可能的。
C ++旨在用于通用目的,并且旨在尽可能快。通用并不一定意味着“易于用于简单的用例”,而是“适用于最广泛应用的适当平台”。
答案 4 :(得分:2)
关于类似pop()
的函数,有两件事(至少)需要考虑:
1)对于没有任何内容可以返回的情况,返回pop_back()
或pop_front()
没有明确而安全的操作。
2)这些函数将按值返回。如果在容器中存储的类型的复制构造函数中抛出异常,则该项将从容器中删除并丢失。我想这被认为是不可取的和不安全的。
关于访问列表,标准库的一般设计原则是不避免提供低效的操作。 std::list
是一个双链表,按索引访问列表元素意味着从开头或结尾遍历列表,直到到达所需位置。如果您想这样做,您可以提供自己的帮助功能。但是如果你需要随机访问元素,那么你应该使用列表以外的结构。
答案 5 :(得分:2)
list甚至没有像get(index)
这样的简单访问器
为什么要这样?允许您从list
访问第n个元素的方法会隐藏操作的O(n)
的复杂性,这就是C ++不提供它的原因。出于同样的原因,C ++的std::vector
不提供pop_front()
函数,因为该函数的大小也是O(N)
。
pop_back和pop_front等方法也不会返回列表中的对象。
The reason is exception safety.此外,由于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;
抛出,你将有一个更改的容器和一个丢失的对象。
答案 6 :(得分:1)
在Java中,通用接口的pop
可以返回对弹出对象的引用。
在C ++中返回相应的东西是按值返回。
但是对于不可移动的非POD对象,复制构造可能会引发异常。然后,一个元素将被删除,但尚未被客户端代码访问。一个方便的按值返回popper总是可以用更基本的检查器和纯popper来定义,但不是反之亦然。
这也是哲学上的差异。
使用C ++,标准库仅提供基本构建块,而不是直接可用的功能(通常)。这个想法是你可以从成千上万的第三方库中自由选择,但是在可用性,可移植性,培训等方面,选择的自由是成本。相比之下,使用Java你在标准库中大部分都需要(对于典型的Java编程),但是你无法自由选择(这是另一种成本)。