Recientemente tuve lo siguiente
struct data {
std::vector<int> V;
};
data get_vector(int n)
{
std::vector<int> V(n,0);
return {V};
}
El problema con este código es que cuando se crea la estructura se produce una copia y la solución es escribir return {std :: move (V)}
¿Hay linter o analizador de código que detectaría tales operaciones de copia espurias? Ni cppcheck, cpplint, ni clang-tidy pueden hacerlo.
EDITAR: Varios puntos para aclarar mi pregunta:
- Sé que se produjo una operación de copia porque utilicé el explorador de compiladores y muestra una llamada a memcpy .
- Pude identificar que se produjeron operaciones de copia mirando el sí estándar. Pero mi idea equivocada inicial fue que el compilador optimizaría esta copia. Estaba equivocado.
- No es (probablemente) un problema del compilador ya que tanto clang como gcc producen código que genera una memoria .
- La memoria puede ser barata, pero no puedo imaginar circunstancias en las que copiar memoria y eliminar el original sea más barato que pasar un puntero por un std :: move .
- La adición de std :: move es una operación elemental. Me imagino que un analizador de código podría sugerir esta corrección.
c++
code-analysis
static-code-analysis
cppcheck
Mathieu Dutour Sikiric
fuente
fuente
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.Respuestas:
¡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:
Y ahora comencemos algunos experimentos:
¿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ánticaEjemplo 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:
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énmemcpy
estará optimizado y verá directamente algunas instrucciones de ensamblador que hacen el trabajo sin una llamada a lamemcpy
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.fuente
¿Puso su aplicación completa en el explorador del compilador y habilitó las optimizaciones? Si no, entonces lo que viste en el explorador del compilador podría o no ser lo que está sucediendo con tu aplicación.
Un problema con el código que publicó es que primero crea un archivo
std::vector
y luego lo copia en una instancia dedata
. Sería mejor inicializardata
con el vector:Además, si solo le da al explorador del compilador la definición de
data
yget_vector()
, y nada más, tiene que esperar lo peor. Si realmente le da algún código fuente que usaget_vector()
, entonces mire qué ensamblado se genera para ese código fuente. Vea este ejemplo para saber qué puede causar la modificación anterior más el uso real más las optimizaciones del compilador.fuente