Czy można bezpiecznie odepchnąć element z tego samego wektora?

126
vector<int> v;
v.push_back(1);
v.push_back(v[0]);

Jeśli drugie push_back spowoduje realokację, odwołanie do pierwszej liczby całkowitej w wektorze nie będzie już ważne. Więc to nie jest bezpieczne?

vector<int> v;
v.push_back(1);
v.reserve(v.size() + 1);
v.push_back(v[0]);

Czy to jest bezpieczne?

Neil Kirk
źródło
4
Uwaga: na forum standardowych propozycji toczy się obecnie dyskusja. W jej ramach ktoś podał przykładową implementacjępush_back . Inny plakat zauważył w nim błąd , że nie obsługiwał poprawnie opisywanego przypadku. O ile wiem, nikt inny nie argumentował, że to nie był błąd. Nie mówię, że to rozstrzygający dowód, tylko obserwacja.
Benjamin Lindley,
9
Przepraszam, ale nie wiem, którą odpowiedź zaakceptować, ponieważ nadal istnieją kontrowersje co do poprawnej odpowiedzi.
Neil Kirk
4
Zostałem poproszony o skomentowanie tego pytania przez piąty komentarz pod: stackoverflow.com/a/18647445/576911 . Robię to, podbijając każdą odpowiedź, która obecnie mówi: tak, można bezpiecznie odepchnąć element z tego samego wektora.
Howard Hinnant,
2
@BenVoigt: <shrug> Jeśli nie zgadzasz się z tym, co mówi norma, lub nawet jeśli zgadzasz się ze standardem, ale nie sądzisz, że mówi to wystarczająco jasno, zawsze jest to opcja dla Ciebie: cplusplus.github.io/LWG/ lwg-active.html # submit_issue Sam korzystałem z tej opcji więcej razy, niż pamiętam. Czasami z powodzeniem, czasami nie. Jeśli chcesz podyskutować o tym, co mówi norma lub co powinien mówić, SO nie jest skutecznym forum. Nasza rozmowa nie ma znaczenia normatywnego. Ale możesz mieć szansę na normatywny wpływ, klikając powyższy link.
Howard Hinnant
2
@ Polaris878 Jeśli push_back spowoduje, że wektor osiągnie swoją pojemność, wektor przydzieli nowy większy bufor, skopiuje stare dane, a następnie usunie stary bufor. Następnie wstawi nowy element. Problem w tym, że nowy element to odwołanie do danych w starym buforze, które właśnie zostały usunięte. O ile push_back nie utworzy kopii wartości przed usunięciem, będzie to złe odniesienie.
Neil Kirk,

Odpowiedzi:

31

Wygląda na to, że http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#526 zajęło się tym problemem (lub czymś bardzo podobnym) jako potencjalną wadą w standardzie:

1) Parametry pobierane przez stałą referencję mogą być zmieniane podczas wykonywania funkcji

Przykłady:

Biorąc pod uwagę std :: wektor v:

v.insert (v.begin (), v [2]);

v [2] można zmienić przesuwając elementy wektora

Proponowana rezolucja była taka, że ​​nie jest to wada:

vector :: insert (iter, value) jest wymagany do działania, ponieważ standard nie zezwala na to, aby nie działał.

Nate Kohl
źródło
Pozwolenie znajduję w 17.6.4.9: "Jeśli argument funkcji ma nieprawidłową wartość (na przykład wartość spoza domeny funkcji lub wskaźnik nieprawidłowy do zamierzonego zastosowania), zachowanie jest niezdefiniowane." Jeśli nastąpi ponowna alokacja, wszystkie iteratory i odwołania do elementów zostaną unieważnione, co oznacza, że ​​odwołanie do parametru przekazane do funkcji jest również nieprawidłowe.
Ben Voigt,
4
Myślę, że chodzi o to, że implementacja jest odpowiedzialna za realokację. Na nim spoczywa obowiązek upewnienia się, że zachowanie jest zdefiniowane, jeśli dane wejściowe są wstępnie zdefiniowane. Ponieważ specyfikacje jasno określają, że push_back tworzy kopię, implementacje muszą, być może kosztem czasu wykonania, buforować lub kopiować wszystkie wartości przed cofnięciem alokacji. Ponieważ w tym konkretnym pytaniu nie ma żadnych odniesień zewnętrznych, nie ma znaczenia, czy iteratory i odwołania są unieważnione.
OlivierD,
3
@NeilKirk Myślę, że to powinna być autorska odpowiedź, wspomina o niej również Stephan T. Lavavej na Reddicie, używając zasadniczo tych samych argumentów.
TemplateRex,
v.insert(v.begin(), v[2]);nie może wywołać realokacji. Jak to odpowiada na pytanie?
ThomasMcLeod
@ThomasMcLeod: tak, może to oczywiście spowodować realokację. Powiększasz rozmiar wektora, wstawiając nowy element.
Violet Giraffe
21

Tak, jest to bezpieczne, a standardowe implementacje bibliotek przeskakują przez obręcze, aby tak było.

Wierzę, że realizatorzy w jakiś sposób śledzą to wymaganie z powrotem do 23,2 / 11, ale nie mogę dowiedzieć się, jak, i nie mogę znaleźć czegoś bardziej konkretnego. Najlepsze, co mogę znaleźć, to ten artykuł:

http://www.drdobbs.com/cpp/copying-container-elements-from-the-c-li/240155771

Inspekcja implementacji libc ++ i libstdc ++ pokazuje, że są one również bezpieczne.

Sebastian Redl
źródło
9
Niektóre wsparcie naprawdę by tu pomogło.
chris,
3
To ciekawe, muszę przyznać, że nigdy nie rozważałem tego przypadku, ale rzeczywiście wydaje się to dość trudne do osiągnięcia. Czy to również dotyczy vec.insert(vec.end(), vec.begin(), vec.end());?
Matthieu M.,
2
@MatthieuM. Nie: Tabela 100 mówi: „pre: i i j nie są iteratorami do a”.
Sebastian Redl,
2
Głosuję teraz za, ponieważ to również moje wspomnienie, ale potrzebne jest odniesienie.
bames53
3
Czy 23.2 / 11 w używanej wersji "O ile nie określono inaczej (jawnie lub przez zdefiniowanie funkcji w kategoriach innych funkcji), wywołanie funkcji składowej kontenera lub przekazanie kontenera jako argumentu do funkcji bibliotecznej nie powinno unieważniać iteratorów aby zmienić wartości obiektów w tym kontenerze ”. ? Ale vector.push_backCZY określa inaczej. „Powoduje realokację, jeśli nowy rozmiar jest większy niż stara pojemność”. i (at reserve) „Realokacja unieważnia wszystkie odwołania, wskaźniki i iteratory odwołujące się do elementów w sekwencji”.
Ben Voigt
13

Norma gwarantuje, że nawet Twój pierwszy przykład będzie bezpieczny. Cytowanie C ++ 11

[sequence.reqmts]

3 W tabelach 100 i 101 ... Xoznacza klasę kontenera sekwencji, aoznacza wartość Xzawierającą elementy typu T, ... toznacza lwartość lub stałą rwartośćX::value_type

16 Tabela 101 ...

Wyrażenie a.push_back(t) Typ zwrotu void Semantyka operacyjna Dołącza kopię t. wymagań: T należy CopyInsertabledo X. Pojemnik basic_string , deque, list,vector

Więc nawet jeśli nie jest to całkiem trywialne, implementacja musi gwarantować, że nie unieważni odwołania podczas wykonywania push_back.

Angew nie jest już dumny z SO
źródło
7
Nie rozumiem, jak to gwarantuje, że będzie to bezpieczne.
jrok
4
@Angew: To absolutnie unieważnia t, jedyne pytanie brzmi, czy przed, czy po wykonaniu kopii. Twoje ostatnie zdanie jest z pewnością błędne.
Ben Voigt
4
@BenVoigt Ponieważ tspełnia wymienione warunki wstępne, opisane zachowanie jest gwarantowane. Implementacja nie może unieważnić warunku wstępnego, a następnie użyć go jako wymówki, aby nie zachowywać się zgodnie z opisem.
bames53
8
@BenVoigt Klient nie jest zobowiązany do przestrzegania warunku wstępnego przez cały czas trwania rozmowy; tylko po to, aby zapewnić, że zostanie spełniony na początku połączenia.
bames53
6
@BenVoigt To dobra uwaga, ale uważam, że funktor przekazany do for_eachnie musi unieważniać iteratorów. Nie mogę wymyślić odniesienia dla for_each, ale widzę na niektórych algorytmach tekst typu „op i binary_op nie powinny unieważniać iteratorów ani podzakresów”.
bames53
7

Nie jest oczywiste, że pierwszy przykład jest bezpieczny, ponieważ najprostsza implementacja push_backpolegałaby na ponownym przydzieleniu wektora, jeśli to konieczne, a następnie skopiowaniu odwołania.

Ale przynajmniej wydaje się, że jest to bezpieczne w Visual Studio 2010. Jego implementacja push_backwykonuje specjalną obsługę przypadku, gdy przesuwasz z powrotem element w wektorze. Kod ma następującą strukturę:

void push_back(const _Ty& _Val)
    {   // insert element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
                    ...
        }
    else
        {   // push back a non-element
                    ...
        }
    }
Johan Råde
źródło
8
Chciałbym wiedzieć, czy specyfikacja wymaga, aby było to bezpieczne.
Nawaz,
1
Zgodnie z normą nie jest wymagane, aby był bezpieczny. Możliwe jest jednak wykonanie go w bezpieczny sposób.
Ben Voigt
2
@BenVoigt Powiedziałbym, że jest to wymagane dla bezpieczeństwa (zobacz moją odpowiedź).
Angew nie jest już dumny z SO
2
@BenVoigt W momencie przekazywania referencji jest ważne.
Angew nie jest już dumny z SO
4
@Angew: To nie wystarczy. Musisz przekazać referencję, która pozostaje ważna przez czas trwania połączenia, a ta nie.
Ben Voigt
3

To nie jest gwarancja ze standardu, ale jako kolejny punkt danych v.push_back(v[0])jest bezpieczny dla libc ++ LLVM .

std::vector::push_backWywołania libc ++,__push_back_slow_path gdy musi ponownie przydzielić pamięć:

void __push_back_slow_path(_Up& __x) {
  allocator_type& __a = this->__alloc();
  __split_buffer<value_type, allocator_type&> __v(__recommend(size() + 1), 
                                                  size(), 
                                                  __a);
  // Note that we construct a copy of __x before deallocating
  // the existing storage or moving existing elements.
  __alloc_traits::construct(__a, 
                            _VSTD::__to_raw_pointer(__v.__end_), 
                            _VSTD::forward<_Up>(__x));
  __v.__end_++;
  // Moving existing elements happens here:
  __swap_out_circular_buffer(__v);
  // When __v goes out of scope, __x will be invalid.
}
Nate Kohl
źródło
Kopia musi być wykonana nie tylko przed zwolnieniem istniejącej pamięci, ale przed przeniesieniem z istniejących elementów. Przypuszczam, że przenoszenie istniejących elementów odbywa się w __swap_out_circular_buffer, w którym to przypadku ta implementacja jest rzeczywiście bezpieczna.
Ben Voigt
@BenVoigt: słuszna uwaga i rzeczywiście masz rację, że ruch odbywa się wewnątrz __swap_out_circular_buffer. (Dodałem kilka komentarzy, aby to zauważyć.)
Nate Kohl,
1

Pierwsza wersja zdecydowanie NIE jest bezpieczna:

Operacje na iteratorach uzyskane przez wywołanie standardowego kontenera biblioteki lub funkcji składowej ciągu mogą uzyskać dostęp do podstawowego kontenera, ale nie mogą go modyfikować. [Uwaga: W szczególności operacje kontenera, które unieważniają iteratory, powodują konflikt z operacjami na iteratorach skojarzonych z tym kontenerem. - notatka końcowa]

od sekcji 17.6.5.9


Zwróć uwagę, że jest to sekcja dotycząca wyścigów danych, o których ludzie zwykle myślą w połączeniu z wątkami ... ale rzeczywista definicja obejmuje relacje „dzieje się przed” i nie widzę żadnego związku porządkującego między wieloma efektami ubocznymi push_backw grać tutaj, a mianowicie, że unieważnienie odniesienia nie wydaje się być zdefiniowane jako uporządkowane w odniesieniu do kopiowania konstruowania nowego elementu ogona.

Ben Voigt
źródło
1
Należy rozumieć, że to uwaga, a nie reguła, więc wyjaśnia konsekwencję powyższej zasady ... a konsekwencje są identyczne dla odniesień.
Ben Voigt
5
Wynik v[0]nie jest iteratorem, podobnie push_back()nie ma iteratora. Zatem z punktu widzenia prawnika językowego twój argument jest nieważny. Przepraszam. Wiem, że większość iteratorów to wskaźniki, a cel unieważnienia iteratora jest prawie taki sam, jak w przypadku odniesień, ale część standardu, którą cytujesz, nie ma znaczenia dla obecnej sytuacji.
cmaster
-1. Jest to zupełnie nieistotny cytat i tak czy siak nie odpowiada. Komisja twierdzi, że x.push_back(x[0])jest BEZPIECZNA.
Nawaz
0

To jest całkowicie bezpieczne.

W swoim drugim przykładzie masz

v.reserve(v.size() + 1);

co nie jest potrzebne, ponieważ jeśli wektor przekroczy swój rozmiar, będzie to oznaczać reserve.

To Vector jest odpowiedzialny za to, a nie Ty.

Zaffy
źródło
-1

Oba są bezpieczne, ponieważ push_back skopiuje wartość, a nie odwołanie. Jeśli przechowujesz wskaźniki, jest to nadal bezpieczne, jeśli chodzi o wektor, ale po prostu wiedz, że będziesz mieć dwa elementy wektora wskazujące na te same dane.

Sekcja 23.2.1 Ogólne wymagania dotyczące kontenerów

16
  • a.push_back (t) Dołącza kopię t. Wymaga: T powinno być CopyInsertable do X.
  • a.push_back (rv) Dołącza kopię rv. Wymaga: T musi być MoveInsertable do X.

Implementacje push_back muszą zatem zapewniać wstawienie kopii v[0] . Dla kontrprzykładu, zakładając implementację, która zmieniłaby alokację przed skopiowaniem, z pewnością nie dołączyłaby kopii v[0]i jako taka naruszyłaby specyfikacje.

OlivierD
źródło
2
push_backjednak zmieni również rozmiar wektora, aw naiwnej implementacji unieważni to odniesienie przed kopiowaniem. Więc jeśli nie możesz tego poprzeć cytatem ze standardu, uznam to za niewłaściwe.
Konrad Rudolph
4
Mówiąc „to”, masz na myśli pierwszy czy drugi przykład? push_backskopiuje wartość do wektora; ale (o ile widzę) może się to zdarzyć po ponownym przydziale, w którym to momencie odniesienie, z którego próbuje skopiować, nie jest już ważne.
Mike Seymour
1
push_backotrzymuje swój argument przez odniesienie .
bames53
1
@OlivierD: Musiałby (1) przydzielić nowe miejsce (2) skopiować nowy element (3) przenieść-skonstruować istniejące elementy (4) zniszczyć przeniesione z elementów (5) zwolnić stary magazyn - w TAKIEJ kolejności - aby pierwsza wersja działała.
Ben Voigt
1
@BenVoigt, dlaczego jeszcze kontener wymagałby, aby typ był CopyInsertable, jeśli i tak ma całkowicie zignorować tę właściwość?
OlivierD
-2

Od 23.3.6.5/1: Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid.

Ponieważ wstawiamy na końcu, żadne odwołania nie zostaną unieważnione, jeśli wektor nie zostanie zmieniony. Więc jeśli wektor capacity() > size()to na pewno zadziała, w przeciwnym razie gwarantowane będzie niezdefiniowane zachowanie.

Mark B.
źródło
Uważam, że specyfikacja faktycznie gwarantuje to działanie w obu przypadkach. Czekam jednak na referencje.
bames53
W pytaniu nie ma wzmianki o iteratorach ani o bezpieczeństwie iteratorów.
OlivierD
3
@OlivierD część iteratora jest tutaj zbędna: interesuje mnie referencesczęść cytatu.
Mark B
2
W rzeczywistości jest to bezpieczne (patrz moja odpowiedź, semantyka push_back).
Angew nie jest już dumny z SO