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:
template <typename DATA >
struct VisibleCopy
{
private:
DATA data;
public:
VisibleCopy( const DATA& data_ ): data{ data_ }
{
std::cout << "Construct " << data << std::endl;
}
VisibleCopy( const VisibleCopy& other ): data{ other.data }
{
std::cout << "Copy " << data << std::endl;
}
VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
{
std::cout << "Move " << data << std::endl;
}
VisibleCopy& operator=( const VisibleCopy& other )
{
data = other.data;
std::cout << "copy assign " << data << std::endl;
}
VisibleCopy& operator=( VisibleCopy&& other ) noexcept
{
data = std::move( other.data );
std::cout << "move assign " << data << std::endl;
}
DATA Get() const { return data; }
};
Und jetzt beginnen wir einige Experimente:
using T = std::vector< VisibleCopy<int> >;
T Get1()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
std::cout << "End init" << std::endl;
return vec;
}
T Get2()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec(4,0);
std::cout << "End init" << std::endl;
return vec;
}
T Get3()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
T Get4()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.reserve(4);
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
int main()
{
auto vec1 = Get1();
auto vec2 = Get2();
auto vec3 = Get3();
auto vec4 = Get4();
// All data as expected? Lets check:
for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}
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 semantics
Beispiel 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:
"So finden Sie falsche C ++ - Kopiervorgänge"
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 auch memcpy
optimiert werden, und Sie sehen direkt einige Assembler-Anweisungen, die die Arbeit ausführen , ohne Ihre Bibliotheksfunktion aufzurufen memcpy
.
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 .
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.