Wie erzwinge ich die Bewegungssemantik, wenn ein Vektor wächst?

90

Ich habe std::vectorObjekte einer bestimmten Klasse A. Die Klasse ist nicht trivial und hat Kopierkonstruktoren und Verschiebungskonstruktoren definiert.

std::vector<A>  myvec;

Wenn ich den Vektor mit AObjekten fülle (z. B. myvec.push_back(a)), wird der Vektor größer und verwendet den Kopierkonstruktor A( const A&), um neue Kopien der Elemente im Vektor zu instanziieren.

Kann ich irgendwie durchsetzen, dass der Verschiebungskonstruktor der Klasse Astattdessen verwendet wird?

Bertwim van Beest
quelle
5
Sie können mithilfe einer bewegungsbewussten Vektorimplementierung.
K-Ballo
2
Können Sie bitte etwas genauer sagen, wie dies erreicht werden soll?
Bertwim van Beest
1
Sie verwenden einfach eine bewegungsbewusste Vektorimplementierung. Es hört sich so an, als ob Ihre Standardbibliotheksimplementierung (was ist es übrigens?) Nicht bewegungsbewusst ist. Sie können es mit bewegungsbewussten Containern von Boost versuchen.
K-Ballo
1
Nun, ich benutze gcc 4.5.1, das bewegungsbewusst ist.
Bertwim van Beest
In meinem Code hat es funktioniert, den Kopierkonstruktor privat zu machen, obwohl der Verschiebungskonstruktor nicht das explizite "noexcept" hatte.
Arne

Antworten:

124

Sie müssen C ++ (speziell std::vector) darüber informieren, dass Ihr Verschiebungskonstruktor und Destruktor nicht werfen noexcept. Dann wird der Verschiebungskonstruktor aufgerufen, wenn der Vektor wächst.

So deklarieren und implementieren Sie einen Verschiebungskonstruktor, der beachtet wird von std::vector:

A(A && rhs) noexcept { 
  std::cout << "i am the move constr" <<std::endl;
  ... some code doing the move ...  
  m_value=std::move(rhs.m_value) ; // etc...
}

Wenn dies nicht der Fall ist noexcept, std::vectorkann der Konstruktor ihn nicht verwenden, da er die vom Standard geforderten Ausnahmegarantien nicht gewährleisten kann.

Weitere Informationen zu den im Standard enthaltenen Informationen finden Sie unter C ++ Move-Semantik und Ausnahmen

Dank an Bo, der angedeutet hat, dass dies möglicherweise mit Ausnahmen zu tun hat. Beachten Sie auch den Rat und die Verwendung von Kerrek SB, emplace_backwenn dies möglich ist. Es kann schneller sein (ist es aber oft nicht), es kann klarer und kompakter sein, aber es gibt auch einige Fallstricke (insbesondere bei nicht expliziten Konstruktoren).

Bearbeiten , oft ist die Standardeinstellung das, was Sie wollen: Verschieben Sie alles, was verschoben werden kann, kopieren Sie den Rest. Um explizit danach zu fragen, schreiben Sie

A(A && rhs) = default;

Wenn Sie dies tun, erhalten Sie nach Möglichkeit noexcept: Ist der Standardkonstruktor Move als noexcept definiert?

Beachten Sie, dass frühere Versionen von Visual Studio 2015 und älter dies nicht unterstützten, obwohl es die Verschiebungssemantik unterstützt.

Johan Lundberg
quelle
Aus Interesse, wie sich die impl „wissen“ , ob die value_type‚s move Ctor ist noexcept? Vielleicht schränkt die Sprache den Funktionsaufrufkandidaten ein, wenn der aufrufende Bereich auch eine noexceptFunktion ist?
Leichtigkeitsrennen im Orbit
1
@LightnessRacesinOrbit Ich gehe davon aus, dass es nur so etwas wie en.cppreference.com/w/cpp/types/is_move_constructible macht . Es kann nur einen Verschiebungskonstruktor geben, daher sollte dieser in der Deklaration klar definiert sein.
Johan Lundberg
@LightnessRacesinOrbit, ich habe seitdem erfahren, dass es keine (Standard- / nützliche) Möglichkeit gibt, wirklich zu wissen, ob es einen noexceptVerschiebungskonstruktor gibt. is_nothrow_move_constructiblewird wahr sein, wenn es einen nothrowKopierkonstruktor gibt. Mir ist kein wirklicher Fall von teuren nothrowKopierkonstruktoren bekannt, daher ist nicht klar, dass es wirklich wichtig ist.
Johan Lundberg
Funktioniert bei mir nicht Meine Destruktor-, Verschiebungskonstruktor- und Verschiebungszuweisungsfunktionen sind noexceptsowohl im Header als auch in der Implementierung markiert. Wenn ich einen push_back (std :; move) ausführe, wird der Kopierkonstruktor weiterhin aufgerufen. Ich reiße mir hier draußen die Haare.
AlastairG
1
@ Joan Ich habe das Problem gefunden. Ich habe std::move()beim falschen push_back()Anruf verwendet. Eine dieser Situationen, in denen Sie so intensiv nach einem Problem suchen, dass Sie den offensichtlichen Fehler nicht direkt vor sich sehen. Und dann war es Mittag und ich habe vergessen, meinen Kommentar zu löschen.
AlastairG
17

Interessanterweise verwendet der Vektor von gcc 4.7.2 nur den Verschiebungskonstruktor, wenn sowohl der Verschiebungskonstruktor als auch der Destruktor vorhanden sind noexcept. Ein einfaches Beispiel:

struct foo {
    foo() {}
    foo( const foo & ) noexcept { std::cout << "copy\n"; }
    foo( foo && ) noexcept { std::cout << "move\n"; }
    ~foo() noexcept {}
};

int main() {
    std::vector< foo > v;
    for ( int i = 0; i < 3; ++i ) v.emplace_back();
}

Dies gibt das erwartete aus:

move
move
move

Allerdings, wenn ich entferne noexceptaus ~foo(), das Ergebnis ist anders:

copy
copy
copy

Ich denke, das beantwortet auch diese Frage .

Nikola Benes
quelle
Es scheint mir, dass die anderen Antworten nur über den Verschiebungskonstruktor sprechen, nicht darüber, dass der Destruktor keine Ausnahme sein muss.
Nikola Benes
Nun, es sollte sein, aber wie sich herausstellte, war es in gcc 4.7.2 nicht so. Dieses Problem war also tatsächlich spezifisch für gcc. Es sollte jedoch in gcc 4.8.0 behoben sein. Siehe verwandte Frage zum Stapelüberlauf .
Nikola Benes
-1

Es scheint, dass die einzige Möglichkeit (für C ++ 17 und früher), die std::vectorVerwendung der Verschiebungssemantik bei der Neuzuweisung zu erzwingen , darin besteht, den Kopierkonstruktor zu löschen :). Auf diese Weise werden Ihre Kompilierungskonstruktoren verwendet oder beim Kompilieren versucht :).

Es gibt viele Regeln, bei denen der std::vectorVerschiebungskonstruktor bei der Neuzuweisung NICHT verwendet werden darf, aber nichts darüber, wo er verwendet werden darf .

template<class T>
class move_only : public T{
public:
   move_only(){}
   move_only(const move_only&) = delete;
   move_only(move_only&&) noexcept {};
   ~move_only() noexcept {};

   using T::T;   
};

Leben

oder

template<class T>
struct move_only{
   T value;

   template<class Arg, class ...Args, typename = std::enable_if_t<
            !std::is_same_v<move_only<T>&&, Arg >
            && !std::is_same_v<const move_only<T>&, Arg >
    >>
   move_only(Arg&& arg, Args&&... args)
      :value(std::forward<Arg>(arg), std::forward<Args>(args)...)
   {}

   move_only(){}
   move_only(const move_only&) = delete;   
   move_only(move_only&& other) noexcept : value(std::move(other.value)) {};    
   ~move_only() noexcept {};   
};

Live-Code

Ihre TKlasse muss über einen noexceptVerschiebungskonstruktor / Zuweisungsoperator und einen noexceptDestruktor verfügen . Andernfalls wird ein Kompilierungsfehler angezeigt.

std::vector<move_only<MyClass>> vec;
Turm120
quelle
1
Es ist nicht erforderlich, den Kopierkonstruktor zu löschen. Wenn der Verschiebungskonstruktor keine Ausnahme ist, wird er verwendet.
Balki
@ Balki Es kann verwendet werden. Standard erfordert dies jetzt nicht. Hier ist Diskussion groups.google.com/a/isocpp.org/forum/…
Tower120