Macht das Verschieben eines Vektors Iteratoren ungültig?

70

Wenn ich einen Iterator in einen Vektor habe a, dann bewege ich einen Vektor baus oder bewege ihn zu. Zeigt adieser Iterator immer noch auf dasselbe Element (jetzt im Vektor b)? Folgendes meine ich im Code:

#include <vector>
#include <iostream>

int main(int argc, char *argv[])
{
    std::vector<int>::iterator a_iter;
    std::vector<int> b;
    {
        std::vector<int> a{1, 2, 3, 4, 5};
        a_iter = a.begin() + 2;
        b = std::move(a);
    }
    std::cout << *a_iter << std::endl; // Is a_iter valid here?
    return 0;
}

Ist es a_iternoch gültig, seit aes verschoben wurde b, oder ist der Iterator durch das Verschieben ungültig? Als Referenz werden std::vector::swap Iteratoren nicht ungültig .

David Brown
quelle
1
@chris Ich hoffe, dass a_iterjetzt auf ein Element verwiesen wird, bnachdem aes verschoben wurde.
David Brown
6
Pedantisch - Sie haben sich nicht bewegt, Sie haben sich bewegt.
John Dibling
7
@Thomash: Wenn die Antwort ist , dass es tut Invalidier Iteratoren, dann ist es nicht definiertes Verhalten sie dereferenzieren, so wie würden Sie es testen?
Benjamin Lindley
1
Ich kann mir keinen Grund vorstellen, warum Iteratoren ungültig werden würden, aber ich kann keine Anführungszeichen im Standard finden, die dies unterstützen ... Da die Gültigkeit von Iteratoren nach einem Tausch genau definiert ist, erscheint es vernünftig, dies zu glauben Die gleiche Argumentation kann beim Umzug gelten (noch mehr, wenn wir darüber nachdenken, wie sie vectorsimplementiert werden).
Luc Touraille
1
@Luc: Iteratoren könnten ungültig werden, wenn die Iteratorklasse selbst Zeiger auf die Vektorklasse zurückhält. Nur spucken.
John Dibling

Antworten:

25

Es ist zwar vernünftig anzunehmen, dass iterators nach a noch gültig sind move, aber ich glaube nicht, dass der Standard dies tatsächlich garantiert. Daher befinden sich die Iteratoren nach dem in einem undefinierten Zustand move.


Es gibt keine Referenz, die ich im Standard finden kann, die ausdrücklich besagt, dass Iteratoren, die vor a existierten move, nach dem noch gültig sind move.

An der Oberfläche scheint es durchaus sinnvoll zu sein , anzunehmen , dass ein iteratorwird typischerweise als Zeiger in der kontrollierten Sequenz implementiert. Wenn dies der Fall ist, sind die Iteratoren nach dem noch gültig move.

Die Implementierung eines iteratorist jedoch implementierungsdefiniert. Das heißt, solange die iteratorPlattform auf einer bestimmten Plattform die Anforderungen des Standards erfüllt, kann sie auf jede Art und Weise implementiert werden. Es könnte theoretisch als eine Kombination eines Zeigers zurück auf die vectorKlasse zusammen mit einem Index implementiert werden. Wenn das ist der Fall, dann würden die Iteratoren nach der ungültig move.

Ob ein iteratortatsächlich auf diese Weise implementiert wird oder nicht , spielt keine Rolle. Es könnte auf diese Weise implementiert werden. Ohne eine spezielle Garantie des Standards, dass movePostiteratoren weiterhin gültig sind, können Sie nicht davon ausgehen, dass dies der Fall ist . Denken Sie auch daran, dass es eine solche Garantie für Iteratoren nach a gibt swap. Dies wurde speziell aus dem vorherigen Standard klargestellt. Vielleicht war es einfach ein Versehen des Std-Ausschusses, nach a keine ähnliche Klarstellung für Iteratoren vorzunehmen move, aber auf jeden Fall gibt es keine solche Garantie.

Daher können Sie nicht davon ausgehen, dass Ihre Iteratoren nach a noch gut sind move.

BEARBEITEN:

23.2.1 / 11 im Entwurf n3242 besagt, dass:

Sofern nicht anders angegeben (entweder explizit oder durch Definieren einer Funktion in Bezug auf andere Funktionen), darf das Aufrufen einer Containerelementfunktion oder das Übergeben eines Containers als Argument an eine Bibliotheksfunktion die Iteratoren für Objekte in diesem Container nicht ungültig machen oder deren Werte ändern .

Dies könnte zu dem Schluss führen, dass die Iteratoren nach a gültig sind move, aber ich bin anderer Meinung. In Ihrem Beispielcode a_iterwar ein Iterator in der vector a. Nach dem move, dieser Container, awurde sicherlich geändert. Mein Fazit ist, dass die obige Klausel in diesem Fall nicht gilt.

John Dibling
quelle
1
+1 Aber vielleicht können Sie davon ausgehen , dass sie nach einem Umzug immer noch gut sind - aber wissen Sie einfach, dass es möglicherweise nicht funktioniert, wenn Sie den Compiler wechseln. Es funktioniert in jedem Compiler, den ich gerade getestet habe, und wird es wahrscheinlich immer tun.
David
6
@ Dave: Das Vertrauen in undefiniertes Verhalten ist ein sehr rutschiger Hang und pedantisch technisch ungültig. Du machst es am besten einfach nicht.
John Dibling
3
Normalerweise würde ich zustimmen, aber es wäre schwierig, einen Swap zu schreiben, der gültige Iteratoren und eine Verschiebungszuweisung beibehält, die dies nicht tut. Es würde fast absichtliche Anstrengungen des Bibliotheksschreibers erfordern, um die Iteratoren ungültig zu machen. Außerdem ist undefiniert mein Lieblingsverhalten.
David
@Sven Marnach: Ich weiß nie Wetter, um Wetter zu verwenden oder ob :)
John Dibling
6
Dies ist LWG 2321
Dyp
12

Ich denke, die Bearbeitung, die die Verschiebungskonstruktion geändert hat, um die Zuweisung zu verschieben, ändert die Antwort.

Zumindest wenn ich Tabelle 96 richtig lese, wird die Komplexität für die Bewegungskonstruktion als "Anmerkung B" angegeben, was für alles andere als eine konstante Komplexität ist std::array. Die Komplexität für die Zugzuweisung wird jedoch als lineares gegeben.

Daher hat die Verschiebungskonstruktion im Wesentlichen keine andere Wahl, als den Zeiger von der Quelle zu kopieren. In diesem Fall ist schwer zu erkennen, wie die Iteratoren ungültig werden können.

Für die Bewegungszuweisung bedeutet dies jedoch, dass dies aufgrund der linearen Komplexität möglich ist , dass einzelne Elemente von der Quelle zum Ziel verschoben werden können. In diesem Fall werden die Iteratoren mit ziemlicher Sicherheit ungültig.

Die Möglichkeit der Bewegungszuweisung von Elementen wird durch die Beschreibung verstärkt: "Alle vorhandenen Elemente eines Zuges werden entweder einem Zug zugewiesen oder zerstört". Der "zerstörte" Teil würde dem Zerstören des vorhandenen Inhalts und dem "Stehlen" des Zeigers von der Quelle entsprechen - aber die "zugewiesene Verschiebung" würde stattdessen das Verschieben einzelner Elemente von der Quelle zum Ziel anzeigen.

Jerry Sarg
quelle
Ich sehe dasselbe wie Sie in Tabelle 96, aber ich bin schockiert, dass die Zugkonstruktion und die Zugzuweisung unterschiedliche Komplexitätsanforderungen haben! Gibt es eine konforme Implementierung hat die Komplexität in der Tabelle entsprechen, oder kann es besser? (AKA: ist ein std :: vector, der den Zeiger auf seine Daten in der Verschiebungszuweisung op standardkonform kopiert?)
David
@ Dave: Eine konforme Implementierung darf nicht schlechter abschneiden als die im Standard vorgeschriebenen Leistungsgarantien.
John Dibling
9
Ich denke tatsächlich, dass es in Bezug auf die Größe des Containers, dem zugewiesen wird, linear ist. Dies ist dasselbe wie der Destruktor, der linear ist. Er muss alle vorhandenen Elemente zerstören. Ein Problem, das der Verschiebungskonstruktor nicht hat, da keine Elemente vorhanden sind.
KillianDS
Warum benötigen sie keine konstante Komplexität für die Zuweisung von std :: vector-Bewegungen?! (und alle anderen Container ...)
David
3
Die Anzahl der zu zerstörenden Elemente ist nicht nur linear. Wie in [container.requirements.general] / 7 angegeben, verschiebt die Verschiebungskonstruktion immer den Allokator, die Verschiebungszuweisung verschiebt den Allokator nur propagate_on_container_move_assignment, wenn dies wahr ist. Wenn dies nicht der Fall ist und die Allokatoren nicht gleich sind, kann der vorhandene Speicher nicht verschoben werden mögliche Neuzuweisung und jedes Element wird einzeln verschoben.
Jonathan Wakely
4

Da es nichts gibt, was einen Iterator davon abhält, einen Verweis oder Zeiger auf den Originalcontainer zu behalten, würde ich sagen, dass Sie sich nicht darauf verlassen können, dass die Iteratoren gültig bleiben, es sei denn, Sie finden eine explizite Garantie im Standard.

Mark Ransom
quelle
+1: Ich würde dieser Einschätzung zustimmen, und für das, was es wert ist, habe ich in den letzten 30 Minuten nach einer solchen Referenz gesucht und kann nichts finden. :)
John Dibling
3
In Übereinstimmung mit John Dibling ist die engste Referenz, dass Iteratoren nicht ungültig werden, wenn zwei Container ausgetauscht werden, was darauf hindeutet, dass sie gültig sein sollten, aber ich habe keine solche Garantie gefunden. Es ist überraschend, wie leise der Standard bezüglich der Bewegung von Containern ist.
David Rodríguez - Dribeas
1
Ist es nicht umgekehrt? Können Sie nicht davon ausgehen, dass Iteratoren gültig bleiben, sofern der Standard nichts anderes bestimmt? Dies ist, was ich aus diesem Zitat verstehe: Unless otherwise specified (either explicitly or by defining a function in terms of other functions), invoking a container member function or passing a container as an argument to a library function shall not invalidate iterators to, or change the values of, objects within that container.[container.requirements.general]
Luc Touraille
4
Würde vector::swapeine konstante Zeit und auch keine ungültigen Iteratoren nicht verhindern vector::iterator, dass ein Zeiger auf den ursprünglichen Container enthalten ist?
David Brown
4
@ LucTouraille: Ich weiß es nicht. Ich darf nur so viel Standardese pro Monat verstehen und habe mein Limit überschritten.
Benjamin Lindley
3

tl; dr: Ja, das Verschieben von a macht std::vector<T, A>die Iteratoren möglicherweise ungültig

Der häufigste Fall (mit vorhanden std::allocator) ist, dass keine Ungültigmachung stattfindet, es jedoch keine Garantie gibt und das Wechseln von Compilern oder sogar das nächste Compiler-Update dazu führen kann, dass sich Ihr Code falsch verhält, wenn Sie sich darauf verlassen, dass Ihre Implementierung die Iteratoren derzeit nicht ungültig macht.


Zuweisung bei Umzug :

Die Frage, ob std::vectorIteratoren nach der Verschiebungszuweisung tatsächlich gültig bleiben können, hängt mit der Allokatorerkennung der Vektorvorlage zusammen und hängt vom Allokatortyp (und möglicherweise den jeweiligen Instanzen davon) ab.

In jeder Implementierung, die ich gesehen habe, macht die Verschiebungszuweisung einer std::vector<T, std::allocator<T>>1 Iteratoren oder Zeiger nicht ungültig. Es gibt jedoch ein Problem, wenn es darum geht, dies zu nutzen, da der Standard einfach nicht garantieren kann, dass Iteratoren für eine Verschiebungszuweisung einer std::vectorInstanz im Allgemeinen gültig bleiben , da der Container Allokator-fähig ist.

Benutzerdefinierte Zuweiser haben möglicherweise den Status. Wenn sie sich bei der Zuweisung von Verschiebungen nicht ausbreiten und nicht gleich vergleichen, muss der Vektor Speicher für die verschobenen Elemente mithilfe seines eigenen Zuweisers zuweisen.

Lassen:

std::vector<T, A> a{/*...*/};
std::vector<T, A> b;
b = std::move(a);

Nun wenn

  1. std::allocator_traits<A>::propagate_on_container_move_assignment::value == false &&
  2. std::allocator_traits<A>::is_always_equal::value == false &&( möglicherweise ab c ++ 17 )
  3. a.get_allocator() != b.get_allocator()

Dann bwird neuer Speicher zugewiesen und Elemente von verschobena nacheinander in diesen Speicher , wodurch alle Iteratoren, Zeiger und Referenzen ungültig werden.

Der Grund ist, dass die Erfüllung der obigen Bedingung 1 die Zuordnung des Allokators bei Containerumzug verbietet. Daher müssen wir uns mit zwei verschiedenen Instanzen des Allokators befassen. Wenn diese beiden Allokatorobjekte jetzt weder immer gleich ( 2. ) noch tatsächlich gleich vergleichen, haben beide Allokatoren einen unterschiedlichen Status. Ein Allokator ist xmöglicherweise nicht in der Lage, den Speicher eines anderen Allokators ymit einem anderen Status freizugeben, und daher kann ein Container mit Allokator xnicht einfach Speicher von einem Container stehlen, der seinen Speicher über zugewiesen hat y.

Wenn sich der Allokator bei der Verschiebungszuweisung ausbreitet oder wenn beide Allokatoren gleich sind, wird eine Implementierung sehr wahrscheinlich nur beigene aDaten erstellen, da sie sicher sein kann, den Speicher ordnungsgemäß freizugeben.

1 :std::allocator_traits<std::allocator<T>>::propagate_on_container_move_assignmentundstd::allocator_traits<std::allocator<T>>::is_always_equalbeide sind typdefs fürstd::true_type(für alle nicht spezialisiertenstd::allocator).


On-Move-Bau :

std::vector<T, A> a{/*...*/};
std::vector<T, A> b(std::move(a));

Der Verschiebungskonstruktor eines Allokator-fähigen Containers verschiebt seine Allokator-Instanz aus der Allokator-Instanz des Containers, aus dem der aktuelle Ausdruck verschoben wird. Auf diese Weise wird die ordnungsgemäße Freigabefähigkeit sichergestellt, und der Speicher kann (und wird) gestohlen werden, da die Bewegungskonstruktion (mit Ausnahme von std::array) eine konstante Komplexität aufweisen muss.

Hinweis: Es gibt noch keine Garantie dafür, dass Iteratoren auch für Verschiebungskonstruktionen gültig bleiben.


Beim Tausch :

Es ist einfach zu fordern, dass die Iteratoren von zwei Vektoren nach einem Swap gültig bleiben (jetzt nur auf den jeweiligen Swap-Container zeigen), da das Swapping nur dann ein definiertes Verhalten aufweist, wenn

  1. std::allocator_traits<A>::propagate_on_container_swap::value == true ||
  2. a.get_allocator() == b.get_allocator()

Wenn sich die Allokatoren beim Auslagern nicht ausbreiten und wenn sie nicht gleich sind, ist das Auslagern der Container in erster Linie ein undefiniertes Verhalten.

Pixelchemist
quelle