Vor kurzem hatte ich Folgendes
struct data {
std::vector<int> V;
};
data get_vector(int n)
{
std::vector<int> V(n,0);
return {V};
}
Das Problem mit diesem Code ist, dass beim Erstellen der Struktur eine Kopie auftritt und die Lösung stattdessen darin besteht, return {std :: move (V)} zu schreiben.
Gibt es einen Linter oder Code-Analysator, der solche falschen Kopiervorgänge erkennen würde? Weder Cppcheck, Cpplint noch Clang-Tidy können das.
EDIT: Mehrere Punkte, um meine Frage klarer zu machen:
- Ich weiß, dass ein Kopiervorgang aufgetreten ist, weil ich den Compiler-Explorer verwendet habe und ein Aufruf von memcpy angezeigt wird .
- Ich konnte feststellen, dass ein Kopiervorgang stattgefunden hat, indem ich mir das Standard-Ja ansah. Meine anfängliche falsche Idee war jedoch, dass der Compiler diese Kopie optimieren würde. Ich lag falsch.
- Es ist (wahrscheinlich) kein Compilerproblem, da sowohl clang als auch gcc Code erzeugen, der einen memcpy erzeugt .
- Das Memcpy mag billig sein, aber ich kann mir keine Umstände vorstellen, in denen das Kopieren von Speicher und das Löschen des Originals billiger ist als das Übergeben eines Zeigers durch eine std :: move .
- Das Hinzufügen von std :: move ist eine elementare Operation. Ich würde mir vorstellen, dass ein Code-Analysator diese Korrektur vorschlagen kann.
c++
code-analysis
static-code-analysis
cppcheck
Mathieu Dutour Sikiric
quelle
quelle
std::vector
auf keinen Fall das ist , was es zu sein vorgibt . Ihr Beispiel zeigt eine explizite Kopie, und es ist nur natürlich und der richtige Ansatz (wieder imho), diestd::move
Funktion so anzuwenden , wie Sie sich selbst vorschlagen, wenn eine Kopie nicht Ihren Wünschen entspricht . Beachten Sie, dass einige Compiler das Kopieren möglicherweise unterlassen, wenn Optimierungsflags aktiviert sind und der Vektor unverändert bleibt.Antworten:
Ich glaube, Sie haben die richtige Beobachtung, aber die falsche Interpretation!
Die Kopie erfolgt nicht durch Rückgabe des Werts, da in diesem Fall jeder normale clevere Compiler (N) RVO verwendet . Ab C ++ 17 ist dies obligatorisch, sodass Sie keine Kopie sehen können, indem Sie einen lokal generierten Vektor von der Funktion zurückgeben.
OK, lass
std::vector
uns ein bisschen damit spielen und was während der Konstruktion passieren wird oder indem du es Schritt für Schritt füllst.Lassen Sie uns zunächst einen Datentyp generieren, der jede Kopie oder Bewegung wie folgt sichtbar macht:
Und jetzt beginnen wir einige Experimente:
Was können wir beobachten:
Beispiel 1) Wir erstellen einen Vektor aus einer Initialisierungsliste und erwarten möglicherweise, dass wir viermal Konstrukte und vier Züge sehen. Aber wir bekommen 4 Exemplare! Das klingt etwas mysteriös, aber der Grund ist die Implementierung der Initialisierungsliste! Es ist einfach nicht erlaubt, aus der Liste zu verschieben, da der Iterator aus der Liste ein
const T*
Element ist , das es unmöglich macht, Elemente aus der Liste zu verschieben. Eine ausführliche Antwort zu diesem Thema finden Sie hier: initializer_list und move semanticsBeispiel 2) In diesem Fall erhalten wir eine erste Konstruktion und 4 Kopien des Werts. Das ist nichts Besonderes und das können wir erwarten.
Beispiel 3) Auch hier haben wir die Konstruktion und einige Bewegungen wie erwartet. Bei meiner stl-Implementierung wächst der Vektor jedes Mal um den Faktor 2. Wir sehen also ein erstes Konstrukt, ein anderes, und da die Größe des Vektors von 1 auf 2 geändert wird, sehen wir die Bewegung des ersten Elements. Beim Hinzufügen der 3 sehen wir eine Größenänderung von 2 auf 4, bei der die ersten beiden Elemente verschoben werden müssen. Alles wie erwartet!
Beispiel 4) Jetzt reservieren wir Platz und füllen später. Jetzt haben wir keine Kopie und keine Bewegung mehr!
In allen Fällen sehen wir keine Bewegung oder Kopie, wenn der Vektor überhaupt an den Anrufer zurückgegeben wird! (N) RVO findet statt und in diesem Schritt sind keine weiteren Maßnahmen erforderlich!
Zurück zu Ihrer Frage:
Wie oben gezeigt, können Sie zum Debuggen zwischendurch eine Proxy-Klasse einführen.
In vielen Fällen funktioniert es möglicherweise nicht, den Kopierer privat zu machen, da Sie möglicherweise einige gewünschte und einige versteckte Kopien haben. Wie oben funktioniert nur der Code zum Beispiel 4 mit einem privaten Kopierer! Und ich kann die Frage nicht beantworten, ob das Beispiel 4 das schnellste ist, da wir Frieden durch Frieden füllen.
Leider kann ich hier keine allgemeine Lösung für das Auffinden "unerwünschter" Kopien anbieten. Selbst wenn Sie Ihren Code für Aufrufe von graben
memcpy
, werden Sie nicht alle finden, da diese auchmemcpy
optimiert werden, und Sie sehen direkt einige Assembler-Anweisungen, die die Arbeit ausführen , ohne Ihre Bibliotheksfunktion aufzurufenmemcpy
.Mein Hinweis ist, sich nicht auf ein so kleines Problem zu konzentrieren. Wenn Sie echte Leistungsprobleme haben, nehmen Sie einen Profiler und messen Sie. Es gibt so viele potenzielle Leistungskiller, dass es
memcpy
keine so lohnende Idee ist , viel Zeit in falsche Nutzung zu investieren .quelle
Haben Sie Ihre vollständige Anwendung in den Compiler-Explorer gestellt und Optimierungen aktiviert? Wenn nicht, kann es sein, dass das, was Sie im Compiler-Explorer gesehen haben, mit Ihrer Anwendung geschieht oder nicht.
Ein Problem mit dem von Ihnen veröffentlichten Code besteht darin, dass Sie zuerst einen Code erstellen
std::vector
und ihn dann in eine Instanz von kopierendata
. Es wäre besser, mit dem Vektor zu initialisierendata
:Wenn Sie dem Compiler-Explorer nur die Definition von
data
undget_vector()
und sonst nichts geben, muss er mit einer Verschlechterung rechnen. Wenn Sie ihm tatsächlich einen Quellcode geben, der verwendet wirdget_vector()
, schauen Sie sich an, welche Assembly für diesen Quellcode generiert wird. In diesem Beispiel erfahren Sie, was die obige Änderung plus tatsächliche Nutzung plus Compileroptimierungen dazu führen kann, dass der Compiler erzeugt.quelle