为什么std :: queue :: pop不返回值?


123

我浏览了此页面,但无法获得相同的原因。那里提到

“更明智的做法是,它根本不返回任何值,并要求客户端使用front()来检查队列前端的值”

但是从front()检查元素还需要将该元素复制到左值中。例如在此代码段中

std::queue<int> myqueue;
int myint;
int result;
std::cin >> myint;
myqueue.push (myint);

/ *此处将在RHS上创建临时文件,并将其分配给结果,如果通过引用返回,则在弹出操作之后结果将变为无效* /

result = myqueue.front();  //result.
std::cout << ' ' << result;
myqueue.pop();

在第五行,cout对象首先创建myqueue.front()的副本,然后将其分配给结果。所以,有什么不同,pop函数可以做同样的事情。


因为是以这种方式实现的(即void std::queue::pop();)。
101010 2014年

问题已得到解答,但作为旁注:如果您真的想要弹出的弹出窗口,可以使用免费函数轻松实现:ideone.com/lnUzf6
eerorika 2014年

1
您的链接是STL文档。但是您在问有关C ++标准库的问题。不同的东西。
juanchopanza 2014年

5
“但是从中检查元素front()也需要将该元素复制到左值中”-不,不是这样。front返回引用,而不是值。您可以检查它引用的值,而无需复制它。
Mike Seymour 2014年

1
@KeminZhou您描述的模型需要副本。也许。如果要多路复用队列的使用,则是,必须在释放队列上的锁之前进行复制。但是,如果您只关心分离输入和输出,则不需要锁来检查正面。您可以等待锁定,直到完成使用它并需要调用为止pop()。如果您使用std::queue<T, std::list<T>>,则不必担心所提供的引用front()是否会被引起无效push()。但是您必须知道您的使用模式并应记录您的约束。
jwm

Answers:


105

所以,有什么不同,pop函数可以做同样的事情。

它确实可以做同样的事情。之所以没有这样做,是因为返回的pop元素在出现异常的情况下是不安全的(必须按值返回并因此创建一个副本)。

考虑这种情况(使用天真的/虚构的流行实现,以阐明我的观点):

template<class T>
class queue {
    T* elements;
    std::size_t top_position;
    // stuff here
    T pop()
    {
        auto x = elements[top_position];
        // TODO: call destructor for elements[top_position] here
        --top_position;  // alter queue state here
        return x;        // calls T(const T&) which may throw
    }

如果T的副本构造函数在返回时抛出,则您已经更改了队列的状态(top_position在我的幼稚实现中),并且该元素从队列中删除了(并且不返回)。出于所有目的和目的(无论如何在客户端代码中捕获异常),队列顶部的元素都会丢失。

如果您不需要弹出的值(即,它会创建一个没人使用的元素的副本),则此实现的效率也很低。

这可以通过两个单独的操作(void popconst T& front())安全有效地实现。


嗯...这很有意义
cbinder

37
C ++ 11注意:如果T具有便宜的noexcept move-constructor(在堆栈中放置对象的情况通常如此),则按值返回是高效且异常安全的。
罗马L

9
@DavidRodríguez-dribeas:我不建议任何更改,我的意思是,提到的某些缺点在C ++ 11中已不再是一个问题。
罗马L

11
但是为什么叫它pop呢?这是很违反直觉的。它可以命名为drop,然后每个人都可以清楚地知道它不会弹出该元素,而是将其删除...
UeliDeSchwert

12
@utnapistim:实际的“弹出”操作一直是从堆栈中取出顶部元素并返回它。至少当我初次接触STL堆栈时,我惊讶于pop不返回任何东西。参见例如维基百科
Cris Luengo

34

您链接到的页面回答了您的问题。

引用整个相关部分:

有人可能想知道为什么pop()返回void而不是value_type。也就是说,为什么必须使用front()和pop()来检查并删除队列最前面的元素,而不是将两者合并为一个成员函数?实际上,这种设计是有充分理由的。如果pop()返回front元素,则必须按值而不是按引用返回:按引用返回将创建一个悬空指针。但是,按值返回效率低下:它涉及至少一个冗余副本构造函数调用。由于pop()不可能以有效且正确的方式返回值,因此更明智的做法是根本不返回任何值,并要求客户使用front()来检查值队列的最前面。

C ++的设计考虑了效率,在程序员必须编写的代码行数之上。


16
也许是这样,但真正的原因是不可能为pop返回值的版本实现异常安全版本(具有强有力的保证)。
James Kanze 2014年

5

pop不能返回对要删除的值的引用,因为该值已从数据结构中删除,因此该引用应引用什么?它可以按值返回,但是如果pop的结果没有存储在任何地方怎么办?然后浪费时间不必要地复制该值。


4
真正的原因是异常安全。如果pop返回并且返回操作可能导致异常,则没有安全的方法与堆栈进行“事务处理”(元素保留在堆栈上,或者返回给您)。显然,它必须在返回元素之前先将其删除,然后,如果抛出该元素,则该元素可能会被不可挽回地丢失。
2014年

1
这种关注是否仍适用于noexcept move构造函数?(当然,0步比两步更有效,但是这可以为异常安全,高效的前+时结合开门)
peppe 2014年

1
@peppe,如果您知道value_type有一个nothrow move构造函数,则可以按值返回,但是队列接口将根据存储在其中的对象类型而有所不同,这将无济于事。
Jonathan Wakely 2014年

1
@peppe:这样做是安全的,但是stack是泛型库类,因此对元素类型的假设越多,它的用处就越小。
2014年

我知道STL不允许这样做,但这意味着人们可以使用二十一点和^ W ^ W ^ W来使用此功能构建自己的队列类:)
peppe 2014年

3

对于当前的实现,这是有效的:

int &result = myqueue.front();
std::cout << result;
myqueue.pop();

如果pop将返回引用,如下所示:

value_type& pop();

然后,以下代码可能会崩溃,因为该引用不再有效:

int &result = myqueue.pop();
std::cout << result;

另一方面,如果它将直接返回一个值:

value_type pop();

然后,您需要为该代码工作而制作一个副本,效率较低:

int result = myqueue.pop();
std::cout << result;

1

从C ++ 11开始,可以使用移动语义来存档所需的行为。像pop_and_move。因此不会调用复制构造函数,并且性能将仅取决于move构造函数。


2
不,移动语义不能神奇地使pop异常安全。
LF

0

您可以完全做到这一点:

std::cout << ' ' << myqueue.front();

或者,如果要在变量中使用值,请使用引用:

const auto &result = myqueue.front();
if (result > whatever) do_whatever();
std::cout << ' ' << result;

紧接着,“更明智”一词是“我们研究了使用模式并发现了更多分割需求”的主观形式。(请放心:C ++语言发展缓慢...)


0

我认为最好的解决方案是添加类似

std::queue::pop_and_store(value_type& value);

其中value将接收弹出的值。

优点是可以使用移动分配运算符来实现,而使用front + pop可以进行复制。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.