Warum können wir "std :: move" für ein "const" -Objekt verwenden?

111

In C ++ 11 können wir diesen Code schreiben:

struct Cat {
   Cat(){}
};

const Cat cat;
std::move(cat); //this is valid in C++11

Wenn ich anrufe std::move, bedeutet dies, dass ich das Objekt verschieben möchte, dh ich werde das Objekt ändern. Das Verschieben eines constObjekts ist unvernünftig. Warum wird std::movedieses Verhalten nicht eingeschränkt? Es wird in Zukunft eine Falle sein, oder?

Hier bedeutet Falle, wie Brandon im Kommentar erwähnt hat:

"Ich denke, er meint, es" fängt "ihn hinterhältig hinterhältig, denn wenn er es nicht merkt, erhält er eine Kopie, die nicht das ist, was er beabsichtigt hat."

In dem Buch 'Effective Modern C ++' von Scott Meyers gibt er ein Beispiel:

class Annotation {
public:
    explicit Annotation(const std::string text)
     : value(std::move(text)) //here we want to call string(string&&),
                              //but because text is const, 
                              //the return type of std::move(text) is const std::string&&
                              //so we actually called string(const string&)
                              //it is a bug which is very hard to find out
private:
    std::string value;
};

Wenn std::movees verboten war, an einem constObjekt zu arbeiten, konnten wir den Fehler leicht herausfinden, oder?

Camino
quelle
2
Aber versuchen Sie es zu bewegen. Versuchen Sie, den Status zu ändern. std::movean sich macht nichts mit dem Objekt. Man könnte argumentieren, std::moveist schlecht benannt.
Juanchopanza
3
Es bewegt eigentlich nichts. Alles, was es tut, ist auf eine r-Wert-Referenz gegossen. Versuchen Sie es CAT cat2 = std::move(cat);unter der Annahme, dass CATdie regelmäßige Zuweisung von Bewegungen unterstützt wird.
WhozCraig
11
std::moveist nur eine Besetzung, es bewegt eigentlich nichts
Red Alert
2
@WhozCraig: Vorsicht, da der von Ihnen veröffentlichte Code ohne Vorwarnung kompiliert und ausgeführt wird, was ihn irreführend macht.
Mooing Duck
1
@MooingDuck Ich habe nie gesagt, dass es nicht kompiliert werden würde. Dies funktioniert nur, weil der Standardkopierer aktiviert ist. Drücken Sie das zusammen und die Räder fallen ab.
WhozCraig

Antworten:

54
struct strange {
  mutable size_t count = 0;
  strange( strange const&& o ):count(o.count) { o.count = 0; }
};

const strange s;
strange s2 = std::move(s);

hier sehen wir eine Verwendung von std::moveauf einem T const. Es gibt a zurück T const&&. Wir haben einen Verschiebungskonstruktor strange, der genau diesen Typ annimmt.

Und es heißt.

Nun ist es wahr, dass dieser seltsame Typ seltener ist als die Fehler, die Ihr Vorschlag beheben würde.

Andererseits std::movefunktioniert das Vorhandene im generischen Code besser, wenn Sie nicht wissen, ob der Typ, mit dem Sie arbeiten, a Toder a ist T const.

Yakk - Adam Nevraumont
quelle
3
+1 für die erste Antwort, die tatsächlich versucht zu erklären, warum Sie ein Objekt aufrufen möchten . std::moveconst
Chris Drew
+1 für die Anzeige einer Funktionsübernahme const T&&. Dies drückt ein "API-Protokoll" der Art "Ich werde einen rvalue-ref nehmen, aber ich verspreche, dass ich ihn nicht ändern werde" aus. Ich denke, abgesehen von der Verwendung von veränderlichen, ist es ungewöhnlich. Möglicherweise besteht ein anderer Anwendungsfall darin, forward_as_tuplefast alles verwenden und später verwenden zu können.
F Pereira
106

Hier übersehen Sie einen Trick, std::move(cat) der eigentlich nichts bewegt . Es weist den Compiler lediglich an, zu versuchen, sich zu bewegen. Da Ihre Klasse jedoch keinen Konstruktor hat, der a akzeptiert const CAT&&, verwendet sie stattdessen den impliziten const CAT&Kopierkonstruktor und kopiert sicher. Keine Gefahr, keine Falle. Wenn der Kopierkonstruktor aus irgendeinem Grund deaktiviert ist, wird ein Compilerfehler angezeigt.

struct CAT
{
   CAT(){}
   CAT(const CAT&) {std::cout << "COPY";}
   CAT(CAT&&) {std::cout << "MOVE";}
};

int main() {
    const CAT cat;
    CAT cat2 = std::move(cat);
}

druckt COPYnicht MOVE.

http://coliru.stacked-crooked.com/a/0dff72133dbf9d1f

Beachten Sie, dass der Fehler in dem von Ihnen erwähnten Code ein Leistungsproblem und kein Stabilitätsproblem ist, sodass ein solcher Fehler niemals zu einem Absturz führen wird. Es wird nur eine langsamere Kopie verwendet. Darüber hinaus tritt ein solcher Fehler auch bei nicht konstanten Objekten auf, die keine Verschiebungskonstruktoren haben. Wenn Sie also nur eine constÜberladung hinzufügen, werden nicht alle erfasst. Wir könnten prüfen, ob das Konstrukt oder die Zuweisung vom Parametertyp verschoben werden kann, dies würde jedoch den generischen Vorlagencode beeinträchtigen, der auf den Kopierkonstruktor zurückgreifen soll . Und zum Teufel, vielleicht möchte jemand in der Lage sein zu konstruieren, von const CAT&&wem soll ich sagen, dass er es nicht kann?

Mooing Duck
quelle
Aufgeregt. Erwähnenswert ist das implizite Löschen des Kopierers nach benutzerdefinierter Definition des regulären Verschiebungskonstruktors oder Zuweisungsoperators. Dies wird auch durch eine fehlerhafte Kompilierung demonstriert. Gute Antwort.
WhozCraig
Erwähnenswert ist auch, dass ein Kopierkonstruktor, der einen constNichtwert benötigt, ebenfalls nicht hilft. [class.copy] §8: "Andernfalls hat der implizit deklarierte Kopierkonstruktor die Form X::X(X&)"
Deduplicator
9
Ich glaube nicht, dass er "Falle" in Bezug auf Computer / Montage meinte. Ich denke, er meint, es "fängt" ihn hinterhältig hinterhältig, denn wenn er es nicht merkt, erhält er eine Kopie, die nicht das ist, was er beabsichtigt hat. Ich denke ..
Brandon
Mögest du 1 weitere Gegenstimme bekommen, und ich bekomme 4 weitere für ein goldenes Abzeichen. ;)
Yakk - Adam Nevraumont
High Five in diesem Codebeispiel. Vermittelt den Punkt wirklich gut.
Brent schreibt
20

Ein Grund, warum die restlichen Antworten bisher übersehen wurden, ist die Fähigkeit des generischen Codes, angesichts von Bewegungen stabil zu sein. Nehmen wir zum Beispiel an, ich wollte eine generische Funktion schreiben, die alle Elemente aus einer Art von Container verschiebt, um eine andere Art von Container mit denselben Werten zu erstellen:

template <class C1, class C2>
C1
move_each(C2&& c2)
{
    return C1(std::make_move_iterator(c2.begin()),
              std::make_move_iterator(c2.end()));
}

Cool, jetzt kann ich relativ effizient ein vector<string>aus einem erstellen deque<string>und jeder Einzelne stringwird dabei bewegt.

Aber was ist, wenn ich von einem wechseln möchte map?

int
main()
{
    std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};
    auto v = move_each<std::vector<std::pair<int, std::string>>>(m);
    for (auto const& p : v)
        std::cout << "{" << p.first << ", " << p.second << "} ";
    std::cout << '\n';
}

Wenn std::moveauf einem Nichtargument bestanden wird const, wird die obige Instanziierung von move_eachnicht kompiliert, da versucht wird, ein const int(das key_typevon map) zu verschieben. Aber diesem Code ist es egal, ob er das nicht bewegen kann key_type. Es möchte das mapped_type( std::string) aus Leistungsgründen verschieben.

Für dieses Beispiel und unzählige andere Beispiele wie dieses in der generischen Codierung std::moveist es eine Aufforderung zum Verschieben , keine Aufforderung zum Verschieben.

Howard Hinnant
quelle
2

Ich habe die gleichen Bedenken wie das OP.

std :: move bewegt kein Objekt und garantiert auch nicht, dass das Objekt beweglich ist. Warum heißt es dann bewegen?

Ich denke, nicht beweglich zu sein kann eines der folgenden zwei Szenarien sein:

1. Der Bewegungstyp ist const.

Der Grund, warum wir das Schlüsselwort const in der Sprache haben, ist, dass der Compiler jede Änderung an einem Objekt verhindern soll, das als const definiert ist. Angesichts des Beispiels in Scott Meyers 'Buch:

    class Annotation {
    public:
     explicit Annotation(const std::string text)
     : value(std::move(text)) // "move" text into value; this code
     {  } // doesn't do what it seems to!    
     
    private:
     std::string value;
    };

Was bedeutet es wörtlich? Verschieben Sie eine const-Zeichenfolge in das Wertelement - zumindest verstehe ich das, bevor ich die Erklärung lese.

Wenn die Sprache beabsichtigt, keine Verschiebung durchzuführen oder nicht zu garantieren, dass die Verschiebung beim Aufruf von std :: move () anwendbar ist, ist dies bei Verwendung der Wortverschiebung buchstäblich irreführend.

Wenn die Sprache Menschen, die std :: move verwenden, zu einer besseren Effizienz ermutigt, muss sie solche Fallen so früh wie möglich verhindern, insbesondere bei dieser Art von offensichtlichen wörtlichen Widersprüchen.

Ich bin damit einverstanden, dass die Leute sich bewusst sein sollten, dass das Verschieben einer Konstante unmöglich ist, aber diese Verpflichtung sollte nicht bedeuten, dass der Compiler schweigen kann, wenn offensichtliche Widersprüche auftreten.

2. Das Objekt hat keinen Verschiebungskonstruktor

Persönlich denke ich, dass dies eine andere Geschichte ist als OPs Besorgnis, wie Chris Drew sagte

@hvd Das scheint mir ein bisschen kein Argument zu sein. Nur weil der Vorschlag von OP nicht alle Fehler auf der Welt behebt, heißt das nicht unbedingt, dass es eine schlechte Idee ist (wahrscheinlich, aber nicht aus dem von Ihnen angegebenen Grund). - Chris Drew

Bogeegee
quelle
1

Ich bin überrascht, dass niemand den Aspekt der Abwärtskompatibilität erwähnt hat. Ich glaube, std::movewurde absichtlich dafür in C ++ 11 entwickelt. Stellen Sie sich vor, Sie arbeiten mit einer älteren Codebasis, die stark von C ++ 98-Bibliotheken abhängt. Ohne den Fallback bei der Kopierzuweisung würde das Verschieben also Probleme verursachen.

mlo
quelle