TL; DR: Pasar por referencia constante sigue siendo una buena idea en C ++, considerando todo. No es una optimización prematura.
TL; DR2: La mayoría de los adagios no tienen sentido, hasta que lo hacen.
Objetivo
Esta respuesta solo intenta extender un poco el elemento vinculado en las Pautas principales de C ++ (mencionado por primera vez en el comentario de amon).
Esta respuesta no trata de abordar el tema de cómo pensar y aplicar adecuadamente los diversos adagios que circularon ampliamente dentro de los círculos de programadores, especialmente el tema de la conciliación entre conclusiones o pruebas en conflicto.
Aplicabilidad
Esta respuesta se aplica solo a las llamadas a funciones (ámbitos anidados no desmontables en el mismo hilo).
(Nota al margen .) Cuando las cosas pasables pueden escapar del alcance (es decir, tener una vida útil que potencialmente excede el alcance externo), se vuelve más importante satisfacer la necesidad de la aplicación de la administración de la vida útil del objeto antes que cualquier otra cosa. Por lo general, esto requiere el uso de referencias que también sean capaces de administrar de por vida, como los punteros inteligentes. Una alternativa podría ser usar un gerente. Tenga en cuenta que lambda es un tipo de alcance desmontable; Las capturas lambda se comportan como si tuvieran un alcance de objeto. Por lo tanto, tenga cuidado con las capturas lambda. También tenga cuidado con la forma en que se pasa la lambda, ya sea por copia o por referencia.
Cuando pasar por valor
Para valores que son escalares (primitivas estándar que se ajustan dentro de un registro de máquina y tienen un valor semántico) para los cuales no hay necesidad de comunicación por mutabilidad (referencia compartida), pase por valor.
Para situaciones en las que la persona que llama requiere una clonación de un objeto o agregado, pase por valor, en el que la copia de la persona que llama satisface la necesidad de un objeto clonado.
Cuándo pasar por referencia, etc.
para todas las demás situaciones, pase por punteros, referencias, punteros inteligentes, manijas (ver: modismo de manijas-cuerpo), etc. Siempre que se siga este consejo, aplique el principio de corrección constante como de costumbre.
Las cosas (agregados, objetos, matrices, estructuras de datos) que son suficientemente grandes en la huella de la memoria siempre deben diseñarse para facilitar el paso por referencia, por razones de rendimiento. Este consejo definitivamente se aplica cuando tiene cientos de bytes o más. Este consejo es dudoso cuando tiene decenas de bytes.
Paradigmas inusuales
Existen paradigmas de programación de propósito especial que son pesados por intención. Por ejemplo, procesamiento de cadenas, serialización, comunicación de red, aislamiento, envoltura de bibliotecas de terceros, comunicación entre procesos de memoria compartida, etc. En estas áreas de aplicación o paradigmas de programación, los datos se copian de estructuras a estructuras o, a veces, se vuelven a empaquetar en conjuntos de bytes.
Cómo afecta la especificación del lenguaje a esta respuesta, antes de considerar la optimización.
Sub-TL; DR Propagar una referencia no debe invocar ningún código; pasar por const-reference satisface este criterio. Sin embargo, todos los demás idiomas satisfacen este criterio sin esfuerzo.
(Se recomienda a los programadores novatos de C ++ que omitan esta sección por completo).
(El comienzo de esta sección está inspirado en parte por la respuesta de gnasher729. Sin embargo, se llega a una conclusión diferente).
C ++ permite constructores de copia definidos por el usuario y operadores de asignación.
(Esta es (fue) una elección audaz que es (fue) a la vez sorprendente y lamentable. Definitivamente es una divergencia de la norma aceptable actual en el diseño del lenguaje).
Incluso si el programador de C ++ no define uno, el compilador de C ++ debe generar dichos métodos basados en los principios del lenguaje y luego determinar si es necesario ejecutar un código adicional memcpy
. Por ejemplo, un class
/ struct
que contiene un std::vector
miembro debe tener un constructor de copia y un operador de asignación que no sea trivial.
En otros lenguajes, se desaconsejan los constructores de copias y la clonación de objetos (excepto donde sea absolutamente necesario y / o significativo para la semántica de la aplicación), porque los objetos tienen semántica de referencia, por diseño de lenguaje. Estos lenguajes generalmente tendrán un mecanismo de recolección de basura que se basa en la accesibilidad en lugar de la propiedad basada en el alcance o el conteo de referencias.
Cuando se pasa una referencia o puntero (incluida la referencia constante) en C ++ (o C), el programador se asegura de que no se ejecutará ningún código especial (funciones definidas por el usuario o generadas por el compilador), aparte de la propagación del valor de la dirección (referencia o puntero). Esta es una claridad de comportamiento con la que los programadores de C ++ se sienten cómodos.
Sin embargo, el contexto es que el lenguaje C ++ es innecesariamente complicado, de modo que esta claridad de comportamiento es como un oasis (un hábitat sobrevivible) en algún lugar alrededor de una zona nuclear.
Para agregar más bendiciones (o insultos), C ++ introduce referencias universales (valores r) para facilitar los operadores de movimiento definidos por el usuario (constructores de movimiento y operadores de asignación de movimiento) con un buen rendimiento. Esto beneficia un caso de uso altamente relevante (el movimiento (transferencia) de objetos de una instancia a otra), al reducir la necesidad de copia y clonación profunda. Sin embargo, en otros idiomas, es ilógico hablar de tal movimiento de objetos.
(Sección fuera de tema) Una sección dedicada a un artículo, "¿Quieres velocidad? ¡Pasa por valor!" escrito en circa 2009.
Ese artículo fue escrito en 2009 y explica la justificación del diseño para el valor r en C ++. Ese artículo presenta un contraargumento válido a mi conclusión en la sección anterior. Sin embargo, el ejemplo de código del artículo y el reclamo de rendimiento han sido refutados durante mucho tiempo.
Sub-TL; DR El diseño de la semántica de valor r en C ++ permite una semántica sorprendentemente elegante del lado del usuario en una Sort
función, por ejemplo. Este elegante es imposible de modelar (imitar) en otros idiomas.
Una función de clasificación se aplica a una estructura de datos completa. Como se mencionó anteriormente, sería lento si hay muchas copias involucradas. Como una optimización del rendimiento (que es prácticamente relevante), una función de clasificación está diseñada para ser destructiva en bastantes lenguajes distintos de C ++. Destructivo significa que la estructura de datos objetivo se modifica para lograr el objetivo de clasificación.
En C ++, el usuario puede elegir llamar a una de las dos implementaciones: una destructiva con mejor rendimiento, o una normal que no modifica la entrada. (La plantilla se omite por brevedad).
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
Además de la clasificación, esta elegancia también es útil en la implementación del algoritmo de búsqueda mediana destructiva en una matriz (inicialmente sin clasificar), mediante particiones recursivas.
Sin embargo, tenga en cuenta que, la mayoría de los idiomas aplicarían un enfoque de árbol de búsqueda binario equilibrado a la ordenación, en lugar de aplicar un algoritmo de ordenación destructivo a las matrices. Por lo tanto, la relevancia práctica de esta técnica no es tan alta como parece.
Cómo la optimización del compilador afecta esta respuesta
Cuando se aplica la alineación (y también la optimización de todo el programa / optimización del tiempo de enlace) en varios niveles de llamadas a funciones, el compilador puede ver (a veces exhaustivamente) el flujo de datos. Cuando esto sucede, el compilador puede aplicar muchas optimizaciones, algunas de las cuales pueden eliminar la creación de objetos completos en la memoria. Por lo general, cuando se aplica esta situación, no importa si los parámetros se pasan por valor o por referencia constante, porque el compilador puede analizar exhaustivamente.
Sin embargo, si la función de nivel inferior llama a algo que está más allá del análisis (por ejemplo, algo en una biblioteca diferente fuera de la compilación, o un gráfico de llamadas que es simplemente demasiado complicado), entonces el compilador debe optimizar a la defensiva.
Los objetos mayores que un valor de registro de máquina pueden copiarse mediante instrucciones explícitas de carga / almacenamiento de memoria, o mediante una llamada a la memcpy
función venerable . En algunas plataformas, el compilador genera instrucciones SIMD para moverse entre dos ubicaciones de memoria, cada instrucción mueve decenas de bytes (16 o 32).
Discusión sobre el tema de la verbosidad o el desorden visual.
Los programadores de C ++ están acostumbrados a esto, es decir, mientras un programador no odie a C ++, la sobrecarga de escribir o leer referencias constantes en el código fuente no es horrible.
Los análisis de costo-beneficio podrían haberse realizado muchas veces antes. No sé si hay algunos científicos que deberían citarse. Supongo que la mayoría de los análisis serían no científicos o no reproducibles.
Esto es lo que imagino (sin pruebas o referencias creíbles) ...
- Sí, afecta el rendimiento del software escrito en este idioma.
- Si los compiladores pueden entender el propósito del código, podría ser lo suficientemente inteligente como para automatizar eso
- Desafortunadamente, en lenguajes que favorecen la mutabilidad (en oposición a la pureza funcional), el compilador clasificaría la mayoría de las cosas como mutadas, por lo tanto, la deducción automática de la constidad rechazaría la mayoría de las cosas como no constantes.
- La sobrecarga mental depende de las personas; las personas que consideran que esto es una gran sobrecarga mental habrían rechazado C ++ como un lenguaje de programación viable.