如何高效清空std::queue?

211

我正在使用std::queue来实现JobQueue类(基本上该类以FIFO方式处理每个作业)。 在某种情况下,我想一次性清除队列(从队列中删除所有作业)。 我没有看到std::queue类中有任何可用的clear方法。

如何高效地实现JobQueue类的清除方法?

我有一个简单的解决方案,即通过循环弹出(pop)但我正在寻找更好的方法。

//Clears the job queue
void JobQueue ::clearJobs()
 {
  // I want to avoid pop in a loop
    while (!m_Queue.empty())
    {
        m_Queue.pop();
    }
}

4
注意:deque支持clear函数。 - bobobobo
12个回答

321

清空标准容器的常见习惯用法是将其与一个空容器进行交换:

void clear( std::queue<int> &q )
{
   std::queue<int> empty;
   std::swap( q, empty );
}

这也是唯一的一种方式来清除某些容器(如std::vector)中持有的内存。


52
更好的做法是 std::queue<int>().swap(q)。使用复制并交换惯用语,所有这些都应该等同于 q = std::queue<int>() - Alexandre C.
12
尽管 std::queue<int>().swap(q) 等同于上述代码,但 q = std::queue<int>() 不一定等效。因为在分配内存的赋值操作中没有所有权转移,所以某些容器(如 vector)可能只调用先前保存元素的析构函数,并设置 size(或使用存储的指针进行等效操作),而不会实际释放内存。 - David Rodríguez - dribeas
7
queue类没有swap(other)方法,所以queue<int>().swap(q)无法编译。我认为你需要使用通用的swap(a, b)函数。 - Dustin Boswell
3
在C++03中,你是正确的,但在C++11中,队列(queues)确实有swap作为成员函数,此外还有一个自由函数重载,可以交换两个相同类型的队列。 - David Rodríguez - dribeas
13
从原始队列的用户角度来看,这些元素不存在。你是正确的,它们将一个接一个地被销毁并且成本是线性的,但是除了本地函数之外,没有其他人能够访问它们。在多线程环境中,您需要锁定,使用非临时队列交换原始队列,解锁(以允许并发访问),然后让交换的队列消失。这样你就可以将销毁的成本移出关键部分。 - David Rodríguez - dribeas
显示剩余14条评论

56

是的 - 在我看来,队列类有点缺陷。这是我的做法:

#include <queue>
using namespace std;;

int main() {
    queue <int> q1;
    // stuff
    q1 = queue<int>();  
}

10
@Naszta,请详细说明如何理解 swap 更为“有效”。 - bobobobo
@bobobobo:q1.swap(queue<int>());q1.swap(queue<int>()); - Naszta
16
q1=queue<int>(); 这样写更简洁明了(你其实并不是真的想用 .swap(),而是想要清空队列)。 - bobobobo
41
使用新版的C++,只需写q1 = {}即可。 - Mykola Bohdiuk
2
@Ari 在list_initialization处的语法(2)和在operator_assignment处的语法(10)。默认的queue<T>构造函数匹配空参数列表{}并且是隐式的,因此它被调用,然后q1.operator=(queue<T>&&)消耗了新创建的queue - Mykola Bohdiuk
显示剩余4条评论

35

在C++11中,您可以通过以下方法清除队列:

std::queue<int> queue;
// ...
queue = {};

6
嘿,我在想这个复杂度是 O(n) 还是 O(1)? - Tilak Madichetti
1
在一般情况下,由于析构函数必须针对每个包含的元素进行调用,因此时间复杂度为O(n)。这也适用于std::vector中的clear()方法。对于POD元素,不需要进行逐个元素的析构函数调用,因此对于vector来说应该是O(1),但对于queue来说则取决于底层容器。默认的std::deque保留了M个内存块,其中M与元素数量n成比例。因此对于默认的std::queue,时间复杂度仍然为O(n)。 - phinz

35

主题作者询问如何“高效”地清除队列,因此我认为他希望具有比线性 O(队列大小) 更好的复杂度。由 David Rodriguezanon 提供的方法具有相同的复杂度: 根据STL参考,operator = 的复杂度为 O(队列大小)。 我个人认为这是因为队列的每个元素都是单独保留的,并且不像向量一样分配在一个大的内存块中。因此,要清除所有内存,我们必须单独删除每个元素。所以清除 std::queue 最直接的方法是一行代码:

while(!Q.empty()) Q.pop();

5
如果您正在处理真实数据,就不能只看操作的O复杂度。如果线性操作的常数使其在所有n < 2^64的情况下比二次方操作更慢,那么我会选择一个O(n^2)算法而不是一个O(n)算法,除非我有一些强烈的理由相信我必须搜索IPv6地址空间或某个特定的问题。对我而言,实际表现比极限性能更重要。 - David Stone
4
这个答案比被采纳的答案更好,因为当队列被销毁时,内部队列会自动执行这个操作。因此,被采纳的答案的时间复杂度是O(n),并且对于全新的队列还需要额外的内存分配和初始化过程。 - Shital Shah
1
请记住,O(n) 表示小于或等于 n 的复杂度。因此,在某些情况下,例如 queue<vector<int>>,它需要逐个销毁每个元素,这将无论如何都很慢,但在 queue<int> 中,内存实际上是分配在一个大块中的,因此不需要销毁内部元素,因此队列的析构函数可以使用单个高效的 free() 操作,这几乎肯定需要少于 O(n) 的时间。 - Benjamin

20

显然,清空 std::queue 的方法有两种:与空对象交换和赋值为空对象。

我建议使用赋值方式,因为它更快,更易读,并且没有歧义。

我使用以下简单代码测量了性能,发现 C++03 版本中的交换比赋值慢 70-80%。在 C++11 中,性能没有区别。无论如何,我会选择赋值方式。

#include <algorithm>
#include <ctime>
#include <iostream>
#include <queue>
#include <vector>

int main()
{
    std::cout << "Started" << std::endl;

    std::queue<int> q;

    for (int i = 0; i < 10000; ++i)
    {
        q.push(i);
    }

    std::vector<std::queue<int> > queues(10000, q);

    const std::clock_t begin = std::clock();

    for (std::vector<int>::size_type i = 0; i < queues.size(); ++i)
    {
        // OK in all versions
        queues[i] = std::queue<int>();

        // OK since C++11
        // std::queue<int>().swap(queues[i]);

        // OK before C++11 but slow
        // std::queue<int> empty;
        // std::swap(empty, queues[i]);
    }

    const double elapsed = double(clock() - begin) / CLOCKS_PER_SEC;

    std::cout << elapsed << std::endl;

    return 0;
}

2
C++11的结果:queues[i] = std::queue<int>();: 1.168,std::queue<int>().swap(queues[i]);: 1.151,std::queue<int> empty; std::swap(empty, queues[i]);: 1.164,while (!queues[i].empty()) queues[i].pop();: 0.189。最后一个明显是最快的。感谢测试代码! - Burak

8
你可以创建一个继承自queue的类,并直接清除底层容器。这非常高效。
template<class T>
class queue_clearable : public std::queue<T>
{
public:
    void clear()
    {
        c.clear();
    }
};

也许你的实现还允许你的队列对象(这里是JobQueue)继承std::queue<Job>而不是将队列作为成员变量。这样,你就可以直接在成员函数中访问c.clear()了。

14
STL容器并不是设计成可以被继承的。在这种情况下,如果您没有添加任何额外的成员变量,那么可能还好,但总体来说这并不是一个好做法。 - bstamour

3
假设你的 m_Queue 包含整数:
std::queue<int>().swap(m_Queue)

否则,如果它包含指向Job对象的指针,则:
std::queue<Job*>().swap(m_Queue)

这样做可以用空队列交换你的m_Queue,因此m_Queue变为空。

3
我用 C++14 实现了以下代码:
std::queue<int> myqueue;
myqueue = decltype(myqueue){};

如果您有一个非常规的队列类型,不想为其构建别名/typedef,则可以使用此方法。但我总是在这种用法周围留下注释,以向不知情/维护程序员解释这并不疯狂,而是代替实际的clear()方法。


2
为什么在赋值运算符中要显式声明类型?我认为 myqueue = { }; 也能正常工作。 - Joel Bodenmann

2
我宁愿不依赖于swap()或将队列设置为新创建的队列对象,因为队列元素没有被正确销毁。调用pop()会调用相应元素对象的析构函数。这在<int>队列中可能不是问题,但在包含对象的队列中可能会产生副作用。
因此,如果你想防止可能的副作用,对于包含对象的队列,循环使用while(!queue.empty()) queue.pop();似乎是最有效的解决方案。

4
swap()或赋值操作会调用已废弃队列的析构函数,从而调用队列中所有对象的析构函数。如果你的队列中有实际上是指针的对象,那么这就是另一个问题 - 但是简单的pop()也不能解决这个问题。 - jhfrontz
1
为什么不幸呢?它既优雅又简单。 - user3091673

1
使用unique_ptr可能是可以的。
您可以将其重置以获取空队列并释放第一个队列的内存。 至于复杂度?我不确定,但猜测它是O(1)。
可能的代码:
typedef queue<int> quint;

unique_ptr<quint> p(new quint);

// ...

p.reset(new quint);  // the old queue has been destroyed and you start afresh with an empty queue

1
如果您选择通过删除队列来清空它,那没问题,但这不是问题的关键,我也不明白为什么要使用unique_ptr。 - manuell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接