¡Creo que tienes la observación correcta pero la interpretación incorrecta!
La copia no se producirá al devolver el valor, porque cada compilador inteligente normal usará (N) RVO en este caso. Desde C ++ 17 esto es obligatorio, por lo que no puede ver ninguna copia devolviendo un vector generado localmente desde la función.
Bien, juguemos un poco std::vector
y lo que sucederá durante la construcción o rellenándolo paso a paso.
En primer lugar, generemos un tipo de datos que haga que cada copia o movimiento sea visible como este:
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; }
};
Y ahora comencemos algunos experimentos:
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; }
}
¿Qué podemos observar?
Ejemplo 1) Creamos un vector a partir de una lista de inicializadores y tal vez esperamos ver 4 construcciones y 4 movimientos. ¡Pero tenemos 4 copias! Eso suena un poco misterioso, pero la razón es la implementación de la lista de inicializadores. Simplemente no está permitido moverse de la lista ya que el iterador de la lista es un elemento const T*
que hace que sea imposible mover elementos de ella. Puede encontrar una respuesta detallada sobre este tema aquí: initializer_list y move semántica
Ejemplo 2) En este caso, obtenemos una construcción inicial y 4 copias del valor. Eso no es nada especial y es lo que podemos esperar.
Ejemplo 3) También aquí, realizamos la construcción y algunos movimientos como se esperaba. Con mi implementación stl, el vector crece por factor 2 cada vez. Entonces vemos una primera construcción, otra y debido a que el vector cambia de tamaño de 1 a 2, vemos el movimiento del primer elemento. Al agregar el 3, vemos un cambio de tamaño de 2 a 4 que necesita un movimiento de los dos primeros elementos. Todo como se esperaba!
Ejemplo 4) Ahora reservamos espacio y rellenamos más tarde. ¡Ahora ya no tenemos copia ni movimiento!
En todos los casos, no vemos ningún movimiento ni copia al devolver el vector a la persona que llama. (N) ¡RVO está teniendo lugar y no se requieren más acciones en este paso!
De vuelta a su pregunta:
"Cómo encontrar operaciones de copia espurias en C ++"
Como se vio anteriormente, puede introducir una clase de proxy en el medio para fines de depuración.
Hacer que el copiador sea privado puede no funcionar en muchos casos, ya que puede tener algunas copias deseadas y algunas ocultas. Como arriba, ¡solo el código del ejemplo 4 funcionará con un copiador privado! Y no puedo responder la pregunta, si el ejemplo 4 es el más rápido, ya que llenamos paz por paz.
Lamento no poder ofrecer una solución general para encontrar copias "no deseadas" aquí. Incluso si excava su código para llamadas de memcpy
, no encontrará todo, ya que también memcpy
estará optimizado y verá directamente algunas instrucciones de ensamblador que hacen el trabajo sin una llamada a la memcpy
función de su biblioteca .
Mi sugerencia es no centrarse en un problema tan menor. Si tiene problemas reales de rendimiento, tome un perfilador y mida. Hay tantos posibles asesinos de rendimiento, que invertir mucho tiempo en el memcpy
uso espurio no parece ser una idea que valga la pena.
std::vector
de ninguna manera es no ser lo que pretende ser . Su ejemplo muestra una copia explícita, y es natural, y el enfoque correcto, (nuevamente en mi humilde opinión), para aplicar lastd::move
función tal como se sugiere si una copia no es lo que desea. Tenga en cuenta que algunos compiladores pueden omitir la copia si las banderas de optimizaciones están activadas y el vector no cambia.