Wie lösche ich die std :: queue effizient?


166

Ich verwende std :: queue zum Implementieren der JobQueue-Klasse. (Grundsätzlich verarbeitet diese Klasse jeden Job auf FIFO-Weise). In einem Szenario möchte ich die Warteschlange auf einmal löschen (alle Jobs aus der Warteschlange löschen). In der Klasse std :: queue ist keine eindeutige Methode verfügbar.

Wie implementiere ich die Clear-Methode für die JobQueue-Klasse effizient?

Ich habe eine einfache Lösung, um in eine Schleife zu springen, aber ich suche nach besseren Wegen.

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

3
Hinweis dequeunterstützt klar
Bobobobo

Antworten:


257

Eine gängige Redewendung zum Löschen von Standardcontainern ist das Austauschen mit einer leeren Version des Containers:

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

Dies ist auch die einzige Möglichkeit, den in einigen Containern gespeicherten Speicher tatsächlich zu löschen (std :: vector).


41
Besser noch ist std::queue<int>().swap(q). Mit Copy and Swap Idiom sollte dies alles gleichbedeutend sein mit q = std::queue<int>().
Alexandre C.

12
Dies std::queue<int>().swap(q)entspricht zwar dem obigen Code, q = std::queue<int>()muss jedoch nicht gleichwertig sein. Da bei der Zuweisung des zugewiesenen Speichers keine Eigentumsübertragung stattfindet, rufen einige Container (wie der Vektor) möglicherweise nur die Destruktoren der zuvor gehaltenen Elemente auf und legen die Größe (oder eine äquivalente Operation mit den gespeicherten Zeigern) fest, ohne den Speicher tatsächlich freizugeben.
David Rodríguez - Dribeas

6
queuehat keine swap(other)Methode, queue<int>().swap(q)kompiliert also nicht. Ich denke, Sie müssen das Generikum verwenden swap(a, b).
Dustin Boswell

3
@ ThorbjørnLindeijer: In C ++ 03 haben Sie Recht, in C ++ 11 haben Warteschlangen Swap als Mitgliedsfunktion, und zusätzlich gibt es eine freie Funktionsüberladung, die zwei Warteschlangen desselben Typs austauscht.
David Rodríguez - Dribeas

10
@ ThorbjørnLindeijer: Aus Sicht der Benutzer der ursprünglichen Warteschlange existieren diese Elemente nicht. Sie haben insofern Recht, als sie nacheinander zerstört werden und die Kosten linear sind, aber sie sind nur für die lokale Funktion zugänglich. In einer Multithread-Umgebung würden Sie sperren, eine nicht temporäre Warteschlange gegen die ursprüngliche austauschen, entsperren (um gleichzeitige Zugriffe zu ermöglichen) und die ausgetauschte Warteschlange sterben lassen. Auf diese Weise können Sie die Kosten der Zerstörung außerhalb des kritischen Abschnitts verschieben.
David Rodríguez - Dribeas

46

Ja - ein bisschen eine Fehlfunktion der Warteschlangenklasse, IMHO. Das ist was ich mache:

#include <queue>
using namespace std;;

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

8
@ Naszta Bitte erläutern Sie, wie swap"effektiver" ist
Bobobobo

@ Bobobobo:q1.swap(queue<int>());
Naszta

12
q1=queue<int>();ist sowohl kürzer als auch klarer (Sie versuchen es nicht wirklich.swap , Sie versuchen es .clear).
Bobobobo

28
Mit dem neuen C ++ ist gerade q1 = {}genug
Mykola Bogdiuk

2
@ Ari-Syntax (2) bei list_initialization und (10) bei operator_assignment . Der Standardkonstruktor queue<T>entspricht der leeren Argumentliste {}und ist implizit, daher wird er aufgerufen und q1.operator=(queue<T>&&)verbraucht dann die neu erstelltequeue
Mykola Bogdiuk

26

Der Autor des Themas fragte, wie die Warteschlange "effizient" gelöscht werden kann, daher gehe ich davon aus, dass er eine bessere Komplexität als lineares O (Warteschlangengröße) wünscht . Verfahren nach diente David Rodriguez , Anon die gleiche Komplexität: gemäß STL Referenz, operator =hat die Komplexität O (Warteschlangengrße) . IMHO liegt es daran, dass jedes Element der Warteschlange separat reserviert ist und nicht wie im Vektor in einem großen Speicherblock zugeordnet ist. Um den gesamten Speicher zu löschen, müssen wir jedes Element separat löschen. Der einfachste Weg zum Löschen std::queueist also eine Zeile:

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

5
Sie können nicht nur die O-Komplexität des Vorgangs betrachten, wenn Sie mit realen Daten arbeiten. Ich würde einen O(n^2)Algorithmus über einen O(n)Algorithmus stellen, wenn die Konstanten der linearen Operation ihn für alle langsamer als das Quadrat machen, es n < 2^64sei denn, ich hätte einen starken Grund zu der Annahme, dass ich den IPv6-Adressraum oder ein anderes spezifisches Problem durchsuchen muss. Leistung in der Realität ist mir wichtiger als Leistung am Limit.
David Stone

2
Diese Antwort ist besser als die akzeptierte Antwort, da die interne Warteschlange dies ohnehin tut, wenn sie zerstört wird. Die akzeptierte Antwort lautet also O (n) und es werden zusätzliche Zuweisungen und Initialisierungen für die brandneue Warteschlange vorgenommen.
Shital Shah

Denken Sie daran, dass O (n) weniger als oder gleich n Komplexität bedeutet. Ja, in einigen Fällen wie in der Warteschlange <vector <int>> muss jedes Element 1 zu 1 zerstört werden, was in beiden Fällen langsam ist. In der Warteschlange <int> wird der Speicher jedoch tatsächlich in einem großen Block zugewiesen. Daher müssen die internen Elemente nicht zerstört werden, sodass der Destruktor der Warteschlange eine einzige effiziente free () - Operation verwenden kann, die mit ziemlicher Sicherheit weniger als O (n) Zeit benötigt.
Benjamin

15

Anscheinend gibt es zwei naheliegendste Möglichkeiten zum Löschen std::queue: Tauschen mit leerem Objekt und Zuweisen zu leerem Objekt.

Ich würde vorschlagen, die Zuweisung zu verwenden, da sie einfach schneller, lesbarer und eindeutiger ist.

Ich habe die Leistung anhand des folgenden einfachen Codes gemessen und festgestellt, dass das Auslagern in der C ++ 03-Version 70-80% langsamer funktioniert als das Zuweisen zu einem leeren Objekt. In C ++ 11 gibt es jedoch keinen Leistungsunterschied. Wie auch immer, ich würde mit Auftrag gehen.

#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;
}

8

In C ++ 11 können Sie die Warteschlange folgendermaßen löschen:

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

4

Sie können eine Klasse erstellen, die von der Warteschlange erbt, und den zugrunde liegenden Container direkt löschen. Das ist sehr effizient.

template<class T>
class queue_clearable : public std::queue<T>
{
public:
    void clear()
    {
        c.clear();
    }
};

Möglicherweise ermöglicht Ihre Implementierung auch, dass Ihr Warteschlangenobjekt (hier JobQueue) erbt, std::queue<Job>anstatt die Warteschlange als Mitgliedsvariable zu haben. Auf diese Weise hätten Sie direkten Zugriff auf c.clear()Ihre Mitgliedsfunktionen.


9
STL-Container sind nicht für die Vererbung ausgelegt. In diesem Fall sind Sie wahrscheinlich in Ordnung, weil Sie keine zusätzlichen Mitgliedsvariablen hinzufügen, aber es ist im Allgemeinen keine gute Sache.
Bstamour

2

Angenommen, Ihre m_Queueenthält ganze Zahlen:

std::queue<int>().swap(m_Queue)

Andernfalls, wenn es z. B. Zeiger auf JobObjekte enthält, dann:

std::queue<Job*>().swap(m_Queue)

Auf diese Weise tauschen Sie eine leere Warteschlange mit Ihrer aus m_Queueund werden somit m_Queueleer.


1

Ich möchte mich lieber nicht auf swap()ein neu erstelltes Warteschlangenobjekt verlassen oder es auf die Warteschlange setzen , da die Warteschlangenelemente nicht ordnungsgemäß zerstört werden. Der Aufruf pop()ruft den Destruktor für das jeweilige Elementobjekt auf. Dies ist möglicherweise kein Problem in <int>Warteschlangen, hat jedoch möglicherweise Nebenwirkungen auf Warteschlangen, die Objekte enthalten.

Daher while(!queue.empty()) queue.pop();scheint eine Schleife mit leider die effizienteste Lösung zu sein, zumindest für Warteschlangen, die Objekte enthalten, wenn Sie mögliche Nebenwirkungen vermeiden möchten.


3
swap()oder Zuweisung ruft den Destruktor in der jetzt nicht mehr existierenden Warteschlange auf, der die Destruktoren aller Objekte in der Warteschlange aufruft. Wenn Ihre Warteschlange Objekte enthält, die tatsächlich Zeiger sind, ist dies ein anderes Problem - aber ein einfaches pop()hilft Ihnen auch dort nicht weiter.
Jhfrontz

Warum leider? Es ist elegant und einfach.
OS2

1

Ich mache das (mit C ++ 14):

std::queue<int> myqueue;
myqueue = decltype(myqueue){};

Diese Methode ist nützlich, wenn Sie einen nicht trivialen Warteschlangentyp haben, für den Sie keinen Alias ​​/ Typedef erstellen möchten. Ich hinterlasse jedoch immer einen Kommentar zu dieser Verwendung, um ahnungslosen / Wartungsprogrammierern zu erklären, dass dies nicht verrückt ist und anstelle einer tatsächlichen clear()Methode erfolgt.


Warum geben Sie den Typ beim Zuweisungsoperator explizit an? Ich gehe davon aus, dass myqueue = { };das gut funktionieren wird.
Joel Bodenmann

0

Die Verwendung von a ist unique_ptrmöglicherweise in Ordnung.
Anschließend setzen Sie es zurück, um eine leere Warteschlange zu erhalten und den Speicher der ersten Warteschlange freizugeben. Was die Komplexität betrifft? Ich bin mir nicht sicher - aber ich denke, es ist O (1).

Möglicher Code:

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

Wenn Sie die Warteschlange durch Löschen leeren möchten, ist das in Ordnung, aber darum geht es nicht, und ich verstehe nicht, warum unique_ptr hereinkommt.
manuell

0

Eine andere Möglichkeit besteht darin, einen einfachen Hack zu verwenden, um den zugrunde liegenden Container abzurufen std::queue::cund ihn aufzurufen clear. Dieses Mitglied muss std::queuegemäß Standard anwesend sein , ist es aber leider protected. Der Hack hier wurde aus dieser Antwort genommen .

#include <queue>

template<class ADAPTER>
typename ADAPTER::container_type& get_container(ADAPTER& a)
{
    struct hack : ADAPTER
    {
        static typename ADAPTER::container_type& get(ADAPTER& a)
        {
            return a .* &hack::c;
        }
    };
    return hack::get(a);
}

template<typename T, typename C>
void clear(std::queue<T,C>& q)
{
    get_container(q).clear();
}

#include <iostream>
int main()
{
    std::queue<int> q;
    q.push(3);
    q.push(5);
    std::cout << q.size() << '\n';
    clear(q);
    std::cout << q.size() << '\n';
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.