Dlaczego std :: queue :: pop nie zwraca wartości.?


123

Przeszedłem przez tę stronę, ale nie mogę znaleźć powodu tego samego. Tam jest o tym mowa

„bardziej rozsądne jest, aby nie zwracał żadnej wartości i wymagał od klientów używania metody front () w celu sprawdzenia wartości na początku kolejki”

Ale sprawdzenie elementu z front () wymagało również skopiowania tego elementu w lvalue. Na przykład w tym segmencie kodu

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

/ * tutaj tymczasowe zostanie utworzone na RHS, które zostanie przypisane do wyniku, aw przypadku, gdy zwróci przez odniesienie, wynik zostanie unieważniony po operacji pop * /

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

w piątej linii obiekt cout najpierw tworzy kopię myqueue.front (), a następnie przypisuje ją do wyniku. Jaka jest różnica, funkcja pop mogłaby zrobić to samo.


Ponieważ jest zaimplementowany w ten sposób (tj void std::queue::pop();.).
101010

Odpowiedź na pytanie została udzielona, ​​ale na marginesie: jeśli naprawdę chcesz powracającego popu, możesz go łatwo zaimplementować za pomocą darmowej funkcji: ideone.com/lnUzf6
eerorika

1
Twój link prowadzi do dokumentacji STL. Ale pytasz o standardową bibliotekę C ++. Różne rzeczy.
juanchopanza

5
„Ale sprawdzenie elementu z front()również wymagało skopiowania tego elementu w lwartości” - nie, nie jest. frontzwraca odniesienie, a nie wartość. Możesz sprawdzić wartość, do której się odnosi, bez kopiowania jej.
Mike Seymour

1
@KeminZhou model, który opisujesz, wymaga kopii. Może. Jeśli chcesz multipleksować użycie kolejki, to tak, musisz wykonać kopię przed zwolnieniem blokady kolejki. Jeśli jednak zależy Ci tylko na oddzieleniu wejścia i wyjścia, nie potrzebujesz zamka do sprawdzenia frontu. Możesz poczekać z zamknięciem, aż skończysz go zużywać i musisz zadzwonić pop(). Jeśli używasz, std::queue<T, std::list<T>>nie ma obaw, że podane odniesienie front()zostanie unieważnione przez push(). Ale musisz znać swój wzorzec użytkowania i dokumentować swoje ograniczenia.
jwm

Odpowiedzi:


105

Jaka jest różnica, funkcja pop mogłaby zrobić to samo.

Rzeczywiście mógł zrobić to samo. Powodem tego jest to, że pop, który zwrócił popped element, jest niebezpieczny w obecności wyjątków (konieczność zwracania wartości, a tym samym tworzenie kopii).

Rozważmy następujący scenariusz (z naiwną / zmyśloną implementacją pop, aby zilustrować mój punkt widzenia):

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
    }

Jeśli konstruktor kopiujący T zgłasza po powrocie, to już zmieniłeś stan kolejki ( top_positionw mojej naiwnej implementacji) i element jest usuwany z kolejki (i nie zwracany). W każdym przypadku (bez względu na to, w jaki sposób wyłapiesz wyjątek w kodzie klienta) element na początku kolejki jest tracony.

Ta implementacja jest również nieefektywna w przypadku, gdy nie potrzebujesz wartości popped (tj. Tworzy kopię elementu, którego nikt nie będzie używał).

Można to wdrożyć bezpiecznie i wydajnie za pomocą dwóch oddzielnych operacji ( void popi const T& front()).


hmmm ... to ma sens
cbinder

37
Uwaga C ++ 11: jeśli T ma tani konstruktor przenoszenia noexcept (co często ma miejsce w przypadku obiektów umieszczanych w stosach), to zwracanie przez wartość jest wydajne i bezpieczne dla wyjątków.
Roman L,

9
@ DavidRodríguez-dribeas: Nie sugeruję żadnych zmian. Chodziło mi o to, że niektóre z wymienionych wad stają się mniejszym problemem w C ++ 11.
Roman L

11
Ale dlaczego to się nazywa pop? to jest dość sprzeczne z intuicją. Można by go nazwać drop, a wtedy jest jasne dla wszystkich, że nie
wystrzeliwuje

12
@utnapistim: de facto operacją „pop” zawsze było pobranie górnego elementu ze stosu i zwrócenie go. Kiedy po raz pierwszy natknąłem się na stosy STL, byłem co najmniej zaskoczony, że popnic nie zwróciłem . Zobacz na przykład na wikipedii .
Cris Luengo,

34

Strona, do której masz łącze, odpowiada na Twoje pytanie.

Cytując cały odpowiedni dział:

Można by się zastanawiać, dlaczego pop () zwraca void zamiast value_type. To znaczy, dlaczego trzeba używać front () i pop (), aby sprawdzić i usunąć element na początku kolejki, zamiast łączyć te dwa elementy w jednej funkcji składowej? W rzeczywistości istnieje dobry powód dla tego projektu. Gdyby pop () zwróciło element frontowy, musiałby zwrócić wartość zamiast referencji: powrót przez referencję utworzyłby wiszący wskaźnik. Zwrot przez wartość jest jednak nieefektywny: wymaga co najmniej jednego nadmiarowego wywołania konstruktora kopiującego. Ponieważ pop () nie może zwrócić wartości w taki sposób, aby była zarówno wydajna, jak i poprawna, rozsądniej jest, aby nie zwracała żadnej wartości i wymagała od klientów użycia metody front () do sprawdzenia wartości w z przodu kolejki.

C ++ został zaprojektowany z myślą o wydajności, biorąc pod uwagę liczbę linii kodu, które musi napisać programista.


16
Być może, ale prawdziwym powodem jest to, że niemożliwe jest zaimplementowanie wersji bezpiecznej dla wyjątków (z silną gwarancją) dla wersji, popktóra zwraca wartość.
James Kanze,

5

pop nie może zwrócić odwołania do usuwanej wartości, ponieważ jest ona usuwana ze struktury danych, więc do czego powinno odnosić się odwołanie? Może zwracać wartość, ale co, jeśli wynik pop nie jest nigdzie przechowywany? Wtedy marnuje się czas na niepotrzebne kopiowanie wartości.


4
Prawdziwym powodem jest wyjątkowe bezpieczeństwo. Nie ma bezpiecznego sposobu na „transakcję” ze stosem (albo element pozostaje na stosie, albo jest zwracany do ciebie), jeśli popzwraca, a czynność zwracania może spowodować wyjątek. Musiałby oczywiście usunąć element, zanim go zwróci, a następnie, jeśli coś wyrzuci, element może zostać nieodwracalnie utracony.
Jon

1
Czy ten problem nadal dotyczy konstruktora fi a noexcept move? (Oczywiście 0 kopii jest bardziej wydajnych niż dwa ruchy, ale to mogłoby otworzyć drzwi dla wyjątkowo bezpiecznego, wydajnego połączonego frontu + pop)
peppe

1
@peppe, możesz zwrócić wartość, jeśli wiesz, że value_typema konstruktor przenoszenia nothrow, ale interfejs kolejki będzie wtedy inny w zależności od typu obiektu, który w nim przechowujesz, co nie byłoby pomocne.
Jonathan Wakely

1
@peppe: To byłoby bezpieczne, ale stos jest ogólną klasą biblioteczną i dlatego im więcej musi zakładać na temat typu elementu, tym jest mniej przydatny.
Jon

Wiem, że STL by na to nie pozwolił, ale to oznacza, że ​​można zbudować własną klasę kolejki z blackjackiem i ^ W ^ W ^ W z tą funkcją :)
peppe

3

Przy obecnej implementacji jest to ważne:

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

Jeśli pop zwróciłby odwołanie, takie jak to:

value_type& pop();

Następnie następujący kod może ulec awarii, ponieważ odwołanie jest już nieprawidłowe:

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

Z drugiej strony, gdyby zwrócił wartość bezpośrednio:

value_type pop();

Następnie musisz zrobić kopię, aby ten kod działał, co jest mniej wydajne:

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

1

Począwszy od C ++ 11 możliwe byłoby zarchiwizowanie żądanego zachowania przy użyciu semantyki przenoszenia. Lubię pop_and_move. Więc konstruktor kopiujący nie zostanie wywołany, a wydajność będzie zależeć tylko od konstruktora przenoszenia.


2
Nie, semantyka przenoszenia w magiczny sposób nie czyni popwyjątków bezpiecznymi.
LF

0

Możesz to całkowicie zrobić:

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

Lub, jeśli chcesz, aby wartość była zawarta w zmiennej, użyj odwołania:

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

Poza tym: sformułowanie „bardziej sensowne” jest subiektywną formą stwierdzenia, że ​​„przyjrzeliśmy się wzorcom użycia i stwierdziliśmy większą potrzebę podziału”. (Zapewniamy: język C ++ nie rozwija się lekko ...)


0

Myślę, że najlepszym rozwiązaniem byłoby dodanie czegoś takiego

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

gdzie value otrzyma wartość popped.

Zaletą jest to, że można go zaimplementować za pomocą operatora przypisania ruchu, podczas gdy użycie front + pop spowoduje utworzenie kopii.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.