Warum gibt std :: queue :: pop keinen Rückgabewert aus?

123

Ich habe diese Seite durchgesehen, kann aber den Grund dafür nicht ermitteln. Dort wird das erwähnt

"Es ist sinnvoller, überhaupt keinen Wert zurückzugeben und von Clients zu verlangen, dass sie front () verwenden, um den Wert an der Vorderseite der Warteschlange zu überprüfen."

Um ein Element von front () aus zu untersuchen, musste dieses Element jedoch auch in lvalue kopiert werden. Zum Beispiel in diesem Codesegment

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

/ * Hier wird temporär auf RHS erstellt, das dem Ergebnis zugewiesen wird. Falls Rückgaben als Referenz zurückgegeben werden, wird das Ergebnis nach der Pop-Operation ungültig. * /

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

In der fünften Zeile erstellt das cout- Objekt zuerst eine Kopie von myqueue.front () und weist diese dann dem Ergebnis zu. Was ist der Unterschied? Die Pop-Funktion hätte das Gleiche tun können.

cbinder
quelle
Weil auf diese Weise implementiert wird (dh void std::queue::pop();).
101010
Die Frage wurde beantwortet, aber als Nebenbemerkung: Wenn Sie wirklich einen Pop wollen, der zurückkehrt, kann er einfach mit einer kostenlosen Funktion implementiert werden: ideone.com/lnUzf6
eerorika
1
Ihr Link führt zur STL-Dokumentation. Sie fragen jedoch nach der C ++ - Standardbibliothek. Verschiedene Dinge.
Juanchopanza
5
"Aber um ein Element von zu inspizieren, front()musste dieses Element auch in lvalue kopiert werden" - nein, das tut es nicht. frontGibt eine Referenz zurück, keinen Wert. Sie können den Wert, auf den er sich bezieht, überprüfen, ohne ihn zu kopieren.
Mike Seymour
1
@KeminZhou Für das von Ihnen beschriebene Modell ist eine Kopie erforderlich. Vielleicht. Wenn Sie den Verbrauch der Warteschlange multiplexen möchten, müssen Sie eine Kopie erstellen, bevor Sie die Sperre für die Warteschlange aufheben. Wenn Sie jedoch nur Ein- und Ausgänge trennen möchten, benötigen Sie kein Schloss, um die Vorderseite zu überprüfen. Sie können warten, bis Sie gesperrt sind, bis Sie mit dem Konsumieren fertig sind und anrufen müssen pop(). Wenn Sie verwenden, std::queue<T, std::list<T>>besteht keine Sorge, dass die angegebene Referenz front()durch a ungültig wird push(). Aber Sie müssen wissen , Ihre Nutzungsmuster und Ihre Einschränkungen dokumentieren sollten.
JWM

Antworten:

105

Was ist der Unterschied? Die Pop-Funktion hätte das Gleiche tun können.

Es hätte tatsächlich das Gleiche tun können. Der Grund dafür ist, dass ein Pop, der das popped-Element zurückgegeben hat, bei Vorhandensein von Ausnahmen unsicher ist (er muss nach Wert zurückgegeben werden und somit eine Kopie erstellen).

Stellen Sie sich dieses Szenario vor (mit einer naiven / erfundenen Pop-Implementierung, um meinen Standpunkt zu verdeutlichen):

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
    }

Wenn der Kopierkonstruktor von T bei der Rückgabe ausgelöst wird, haben Sie den Status der Warteschlange bereits geändert ( top_positionin meiner naiven Implementierung) und das Element wird aus der Warteschlange entfernt (und nicht zurückgegeben). In jeder Hinsicht (unabhängig davon, wie Sie die Ausnahme im Clientcode abfangen) geht das Element oben in der Warteschlange verloren.

Diese Implementierung ist auch dann ineffizient, wenn Sie den Popup-Wert nicht benötigen (dh eine Kopie des Elements erstellt, das niemand verwenden wird).

Dies kann sicher und effizient mit zwei getrennten Operationen ( void popund const T& front()) implementiert werden .

utnapistim
quelle
hmmm ... das macht Sinn
cbinder
37
C ++ 11 Hinweis: Wenn T einen billigen noexcept-Verschiebungskonstruktor hat (was häufig bei der Art von Objekten der Fall ist, die in Stapeln abgelegt sind), ist die Rückgabe nach Wert effizient und ausnahmesicher.
Roman L
9
@ DavidRodríguez-dribeas: Ich schlage keine Änderungen vor. Mein Punkt war, dass einige der genannten Nachteile mit C ++ 11 weniger problematisch werden.
Roman L
11
Aber warum heißt es pop? das ist ziemlich kontraintuitiv. Es könnte benannt werden drop, und dann ist es für alle klar, dass es das Element nicht
platzt
12
@utnapistim: Die de-facto "Pop" -Operation bestand immer darin, das oberste Element von einem Stapel zu nehmen und zurückzugeben. Als ich zum ersten Mal auf STL-Stapel stieß, war ich gelinde gesagt überrascht, dass ich popnichts zurückgegeben habe. Siehe zum Beispiel auf Wikipedia .
Cris Luengo
34

Die Seite, auf die Sie verlinkt haben, beantwortet Ihre Frage.

Um den gesamten relevanten Abschnitt zu zitieren:

Man könnte sich fragen, warum pop () void anstelle von value_type zurückgibt. Das heißt, warum muss man front () und pop () verwenden, um das Element an der Vorderseite der Warteschlange zu untersuchen und zu entfernen, anstatt die beiden in einer einzigen Elementfunktion zu kombinieren? In der Tat gibt es einen guten Grund für dieses Design. Wenn pop () das vordere Element zurückgeben würde, müsste es eher nach Wert als nach Referenz zurückgegeben werden: Rückgabe nach Referenz würde einen baumelnden Zeiger erzeugen. Die Rückgabe nach Wert ist jedoch ineffizient: Es handelt sich um mindestens einen redundanten Kopierkonstruktoraufruf. Da es für pop () unmöglich ist, einen Wert so zurückzugeben, dass er sowohl effizient als auch korrekt ist, ist es sinnvoller, überhaupt keinen Wert zurückzugeben und von den Clients zu verlangen, front () zu verwenden, um den Wert bei zu überprüfen die Vorderseite der Warteschlange.

C ++ wurde unter Berücksichtigung der Effizienz über die Anzahl der Codezeilen entwickelt, die der Programmierer schreiben muss.

BPF2010
quelle
16
Vielleicht, aber der wahre Grund ist, dass es unmöglich ist, eine ausnahmesichere Version (mit der starken Garantie) zu implementieren, für die eine Version popeinen Wert zurückgibt.
James Kanze
5

pop kann keinen Verweis auf den Wert zurückgeben, der entfernt wird, da er aus der Datenstruktur entfernt wird. Worauf sollte sich der Verweis also beziehen? Es könnte nach Wert zurückgegeben werden, aber was ist, wenn das Ergebnis von Pop nirgendwo gespeichert wird? Dann wird Zeit damit verschwendet, den Wert unnötig zu kopieren.

Neil Kirk
quelle
4
Der wahre Grund ist die Ausnahmesicherheit. Es gibt keine sichere Möglichkeit, eine "Transaktion" mit dem Stapel durchzuführen (entweder bleibt das Element auf dem Stapel oder es wird an Sie zurückgegeben), wenn popRückgaben und das Zurückgeben zu einer Ausnahme führen können. Es müsste offensichtlich das Element entfernen, bevor es zurückgegeben wird, und wenn dann etwas wirft, könnte das Element unwiderruflich verloren gehen.
Jon
1
Gilt dieses Problem immer noch für einen No-Except-Move-Konstruktor? (Natürlich sind 0 Kopien effizienter als zwei Züge, aber das könnte die Tür für eine ausnahmsichere, effiziente kombinierte Front + Pop
öffnen
1
@peppe, Sie könnten nach Wert zurückgeben, wenn Sie wissen, dass der value_typeKonstruktor einen nothrow move-Konstruktor hat, aber die Warteschlangenschnittstelle würde sich dann unterscheiden, je nachdem, welchen Objekttyp Sie darin speichern, was nicht hilfreich wäre.
Jonathan Wakely
1
@peppe: Das wäre sicher, aber Stack ist eine generische Bibliotheksklasse. Je mehr es über den Elementtyp annehmen muss, desto weniger nützlich ist es.
Jon
Ich weiß, dass die STL das nicht zulassen würde, aber das bedeutet, dass man mit dieser Funktion seine eigene Warteschlangenklasse mit Blackjack und ^ W ^ W ^ W
erstellen kann
3

Mit der aktuellen Implementierung gilt dies:

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

Wenn Pop eine Referenz wie folgt zurückgeben würde:

value_type& pop();

Dann könnte der folgende Code abstürzen, da die Referenz nicht mehr gültig ist:

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

Auf der anderen Seite, wenn es einen Wert direkt zurückgeben würde:

value_type pop();

Dann müssten Sie eine Kopie erstellen, damit dieser Code funktioniert, was weniger effizient ist:

int result = myqueue.pop();
std::cout << result;
Jan Rüegg
quelle
1

Ab C ++ 11 wäre es möglich, das gewünschte Verhalten mithilfe der Verschiebungssemantik zu archivieren. Wie pop_and_move. Der Kopierkonstruktor wird also nicht aufgerufen, und die Leistung hängt nur vom Verschiebungskonstruktor ab.

Vyacheslav Putsenko
quelle
2
Nein, die Bewegungssemantik macht popAusnahmen nicht auf magische Weise sicher.
LF
0

Sie können dies vollständig tun:

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

Wenn Sie den Wert in einer Variablen haben möchten, verwenden Sie eine Referenz:

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

Daneben: Der Wortlaut "sinnvoller" ist eine subjektive Form von "Wir haben uns mit Nutzungsmustern befasst und festgestellt, dass mehr Spaltung erforderlich ist". (Seien Sie versichert: Die C ++ - Sprache entwickelt sich nicht leicht ...)

xtofl
quelle
0

Ich denke, die beste Lösung wäre, so etwas hinzuzufügen

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

Dabei erhält value den gepoppten Wert.

Der Vorteil ist, dass es mit einem Verschiebungszuweisungsoperator implementiert werden kann, während mit Front + Pop eine Kopie erstellt wird.

Masse Nicolas
quelle