Je crois que vous avez la bonne observation mais la mauvaise interprétation!
La copie ne se produira pas en renvoyant la valeur, car chaque compilateur intelligent normal utilisera (N) RVO dans ce cas. Depuis C ++ 17, cela est obligatoire, vous ne pouvez donc pas voir de copie en renvoyant un vecteur généré localement à partir de la fonction.
OK, permet de jouer un peu avec std::vector
et ce qui se passera pendant la construction ou en le remplissant étape par étape.
Tout d'abord, permet de générer un type de données qui rend chaque copie ou déplacement visible comme celui-ci:
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; }
};
Et maintenant, commençons quelques expériences:
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; }
}
Que pouvons-nous observer:
Exemple 1) Nous créons un vecteur à partir d'une liste d'initialisation et nous nous attendons peut-être à voir 4 fois la construction et 4 mouvements. Mais nous en avons 4 exemplaires! Cela semble un peu mystérieux, mais la raison en est la mise en œuvre de la liste d'initialisation! Simplement, il n'est pas autorisé de se déplacer de la liste car l'itérateur de la liste est un const T*
qui rend impossible le déplacement d'éléments de celle-ci. Une réponse détaillée sur ce sujet peut être trouvée ici: initializer_list et move semantics
Exemple 2) Dans ce cas, nous obtenons une construction initiale et 4 copies de la valeur. Ce n'est rien de spécial et c'est ce à quoi nous pouvons nous attendre.
Exemple 3) Ici aussi, nous avons la construction et certains mouvements comme prévu. Avec mon implémentation stl, le vecteur croît de facteur 2 à chaque fois. Nous voyons donc une première construction, une autre et parce que le vecteur se redimensionne de 1 à 2, nous voyons le déplacement du premier élément. En ajoutant le 3, nous voyons un redimensionnement de 2 à 4 qui nécessite un déplacement des deux premiers éléments. Tout comme prévu!
Exemple 4) Maintenant, nous réservons de l'espace et remplissons plus tard. Maintenant, nous n'avons plus de copie ni de mouvement!
Dans tous les cas, nous ne voyons aucun mouvement ni copie en renvoyant le vecteur à l'appelant! (N) RVO est en cours et aucune autre action n'est requise à cette étape!
Retour à votre question:
"Comment trouver des opérations de copie parasite C ++"
Comme vu ci-dessus, vous pouvez introduire une classe proxy entre les deux à des fins de débogage.
Rendre le copy-ctor privé peut ne pas fonctionner dans de nombreux cas, car vous pouvez avoir des copies désirées et des cachées. Comme ci-dessus, seul le code de l'exemple 4 fonctionnera avec un copieur privé! Et je ne peux pas répondre à la question, si l'exemple 4 est le plus rapide, car nous remplissons la paix par la paix.
Désolé de ne pas pouvoir proposer de solution générale pour trouver des copies "indésirables" ici. Même si vous creusez votre code pour les appels de memcpy
, vous ne trouverez pas tout car il memcpy
sera également optimisé et vous verrez directement quelques instructions d'assembleur faire le travail sans appeler votre memcpy
fonction de bibliothèque .
Mon indice n'est pas de se concentrer sur un problème aussi mineur. Si vous avez de vrais problèmes de performances, prenez un profileur et mesurez. Il y a tellement de tueurs potentiels de performances, qu'investir beaucoup de temps sur une memcpy
utilisation intempestive ne semble pas une idée aussi valable.
std::vector
par quelque moyen que ce soit n'est pas ce qu'elle prétend être . Votre exemple montre une copie explicite, et il est naturel, et la bonne approche, (encore une fois à mon humble avis) d'appliquer lastd::move
fonction comme vous le suggérez si une copie n'est pas ce que vous voulez. Notez que certains compilateurs peuvent omettre la copie si les indicateurs d'optimisation sont activés et que le vecteur est inchangé.