priority - Consecuencias de solo usar stack en C++
std:: stack int (13)
El único argumento importante que se me ocurre es que la pila podría desbordarse, pero supongo que es improbable que esto ocurra. ¿Hay otros argumentos en contra de usar solo el valor de apilar / pasar en lugar de pasar por referencia?
Bueno, Dios mío, por dónde empezar ...
Como mencionas, "hay muchas copias innecesarias que ocurren en el código". Digamos que tienes un bucle donde llamas a una función en estos objetos. El uso de un puntero en lugar de duplicar los objetos puede acelerar la ejecución en uno o más órdenes de magnitud.
No puede pasar estructuras de datos, matrices, etc. de tamaño variable alrededor de la pila. Debe asignarlo dinámicamente y pasar un puntero o una referencia al principio. Si tu amigo no se ha encontrado con esto, entonces sí, es "nuevo en C ++".
Como mencionó, el programa en cuestión es simple y en su mayoría utiliza objetos bastante pequeños como gráficos 3-tuplas, que si los elementos son dobles serían 24 bytes cada uno. Pero en gráficos, es común tratar con arreglos 4x4, que manejan tanto la rotación como la traducción. Esos serían de 128 bytes cada uno, por lo que si un programa que tuviera que lidiar con ellos fuera cinco veces más lento por cada llamada de función con el paso por valor debido al aumento en la copia. Con el paso por referencia, pasar una matriz de 3 tuplas o 4x4 en un ejecutable de 32 bits solo implicaría duplicar un solo puntero de 4 bytes.
En las arquitecturas de CPU ricas en registros, como ARM, PowerPC, x86 de 64 bits, 680x0, pero no x86 de 32 bits, los punteros (y las referencias, que son secretos que usan ropa sintética de lujo) se pasan o devuelven en un registro, que es realmente muy rápido en comparación con el acceso a la memoria involucrado en una operación de pila.
Usted menciona la improbabilidad de quedarse sin espacio en la pila. Y sí, eso es así en un pequeño programa que uno podría escribir para una tarea de clase. Pero hace un par de meses, estaba depurando un código comercial que probablemente era 80 llamadas de función por debajo de main (). Si hubieran usado el paso por valor en lugar del paso por referencia, la pila habría sido descomunal. Y para que su amigo no piense que se trata de un "diseño roto", en realidad fue un navegador basado en WebKit implementado en Linux usando GTK +, todo lo cual es muy moderno, y la función de profundidad de llamada es normal para el código profesional .
Algunas arquitecturas ejecutables limitan el tamaño de un marco de pila individual, por lo que aunque no se quede sin espacio de pila en sí, podría superar eso y terminar con un código C ++ perfectamente válido que no se basaría en dicha plataforma.
Podría seguir y seguir.
Si su amigo está interesado en los gráficos, debería echar un vistazo a algunas de las API comunes que se usan en los gráficos: OpenGL y XWindows en Linux, Quartz en Mac OS X, Direct X en Windows. Y debe mirar las partes internas de los grandes sistemas C / C ++ como los motores de representación HTML WebKit o Gecko, o cualquiera de los navegadores Mozilla, o los kits de herramientas GTK + o Qt GUI. Todos pasan por algo mucho más grande que un entero entero o flotante por referencia, y a menudo completan los resultados por referencia en lugar de como un valor de retorno de función.
Nadie con ninguna chuleta seria de C / C ++ del mundo real, y me refiero a nadie , pasa las estructuras de datos por valor. Hay una razón para esto: es simplemente volverse ineficiente y propenso a los problemas.
Digamos que conozco a un chico que es nuevo en C ++. No pasa los punteros (con razón) pero se niega a pasar por referencia. Él usa pasar por valor siempre. La razón es que él siente que "pasar objetos por referencia es un signo de un diseño roto".
El programa es un pequeño programa de gráficos y la mayoría de los pases en cuestión son objetos vectoriales matemáticos (3-tuplas). Hay algunos objetos controladores grandes, pero nada más complicado que eso.
Me resulta difícil encontrar un argumento asesino en contra de solo usar la pila.
Yo diría que pasar por valor está bien para objetos pequeños como vectores, pero incluso entonces hay muchas copias innecesarias que ocurren en el código. Pasar objetos grandes por valor es obviamente un desperdicio y lo más probable es que no sea lo que quiere funcionalmente.
En el lado profesional, creo que la pila es más rápida en la asignación / desasignación de memoria y tiene un tiempo de asignación constante.
El único argumento importante que se me ocurre es que la pila podría desbordarse, pero supongo que es improbable que esto ocurra. ¿Hay otros argumentos en contra de usar solo el valor de apilar / pasar en lugar de pasar por referencia?
La razón es que él siente que "pasar objetos por referencia es un signo de un diseño roto".
Si bien esto es incorrecto en C ++ por razones puramente técnicas, usar el paso por valor es una aproximación lo suficientemente buena para los principiantes: es ciertamente mucho mejor que pasarlo todo por punteros (o tal vez incluso pasarlo todo por referencia). Hará que un código sea ineficiente pero, hey! Mientras esto no moleste a tu amigo, no te preocupes demasiado por esta práctica. Solo recuérdele que algún día podría querer reconsiderarlo.
Por otro lado, esto:
Hay algunos objetos controladores grandes, pero nada más complicado que eso.
es un problema ¿Tu amigo está hablando de un diseño roto, y luego todos los usos del código son algunos vectores 3D y grandes estructuras de control? Ese es un diseño roto. Buen código logra modularidad mediante el uso de estructuras de datos. No parece que este fuera el caso.
... Y una vez que usa tales estructuras de datos, el código sin paso por referencia puede volverse bastante ineficiente.
Yo diría que pasar por valor está bien para objetos pequeños como vectores, pero incluso entonces hay muchas copias innecesarias que ocurren en el código. Pasar objetos grandes por valor es obviamente un desperdicio y lo más probable es que no sea lo que quiere funcionalmente.
No es tan obvio como podría pensar. Los compiladores de C ++ realizan la elision de copia de forma muy agresiva, por lo que a menudo puede pasar por valor sin incurrir en el costo de una operación de copia. Y en algunos casos, pasar por valor puede ser incluso más rápido .
Antes de condenar el problema por razones de rendimiento, debe al menos producir los puntos de referencia para respaldarlo. Y pueden ser difíciles de crear porque el compilador generalmente elimina la diferencia de rendimiento.
Así que el verdadero problema debería ser uno de la semántica. ¿Cómo quieres que se comporte tu código? A veces, lo que usted quiere es la semántica de referencia, y luego debe pasar por referencia. Si específicamente desea / necesita semántica de valor, entonces pasa por valor.
Hay un punto a favor de pasar por valor. Es útil para lograr un estilo de código más funcional, con menos efectos secundarios y donde la inmutabilidad es el valor predeterminado. Eso hace que sea mucho más fácil razonar un montón de código, y también puede facilitar el paralelismo del código.
Pero en verdad, ambos tienen su lugar. Y nunca usar el paso por referencia es definitivamente una gran señal de advertencia.
Durante los últimos 6 meses más o menos, he estado experimentando con hacer que el valor predeterminado de paso por valor. Si no necesito explícitamente la semántica de referencia, entonces trato de asumir que el compilador realizará la elision de copia para mí, por lo que puedo pasar por valor sin perder ninguna eficiencia.
Hasta ahora, el compilador no me ha defraudado. Estoy seguro de que me encontraré con casos en los que tengo que volver atrás y cambiar algunas llamadas a pasar por referencia, pero lo haré cuando lo sepa.
- el rendimiento es un problema, y
- el compilador no pudo aplicar elision de copia
Como ya se mencionó, la gran diferencia entre una referencia y un puntero es que un puntero puede ser nulo. Si una clase requiere datos, una declaración de referencia lo requerirá. Agregar const hará que sea ''solo lectura'' si eso es lo que desea la persona que llama.
El "defecto" de paso por valor mencionado simplemente no es cierto. Pasar todo por valor cambiará completamente el rendimiento de una aplicación. No es tan malo cuando los tipos primitivos (es decir, int, double, etc.) se pasan por valor, pero cuando se pasa una instancia de clase por valor, se crean objetos temporales que requieren que los constructores y luego los destructores sean llamados en la clase y en todos de la variable miembro en la clase. Esto es exasperado cuando se usan jerarquías de clases grandes porque también se debe llamar a los constructores / destructores de clases primarias.
Además, solo porque el vector se pasa por valor no significa que solo use memoria de pila. El montón se puede usar para cada elemento tal como se crea en el vector temporal que se pasa al método / función. El propio vector también puede tener que reasignarse a través del montón si alcanza su capacidad.
Si pasa por valor, para que los valores de las personas que llaman no se modifiquen, simplemente use una referencia constante.
Compre a su amigo un buen libro de c ++. Pasar objetos no triviales por referencia es una buena práctica y le ahorra muchas llamadas innecesarias de constructor / destructor. Esto tampoco tiene nada que ver con la asignación en la tienda libre frente al uso de la pila. Puede (o debe) pasar objetos asignados en la pila de programas por referencia sin ningún uso gratuito de la tienda. También puedes ignorar completamente la tienda gratuita, pero eso te devuelve a los viejos tiempos de Fortran que tu amigo probablemente no tenía en mente; de lo contrario, elegiría un antiguo compilador f77 para tu proyecto, ¿no ...?
Creo que hay un gran malentendido en la pregunta en sí.
No existe una relación entre la pila o el montón de objetos asignados por un lado y pasar por valor o referencia o puntero por el otro.
Asignación de pila vs pila
Siempre que prefiera apilar cuando sea posible, la vida útil del objeto se administra para usted, lo cual es mucho más fácil de manejar.
Aunque podría no ser posible en un par de situaciones:
- Construcción virtual (pensar en una fábrica)
- Propiedad compartida (aunque siempre debes tratar de evitarla)
Y es posible que pierda algunos, pero en este caso debería usar SBRM (Scope Bound Resources Management) para aprovechar las capacidades de administración de la vida útil de la pila, por ejemplo, mediante el uso de punteros inteligentes.
Pase por: valor, referencia, puntero
En primer lugar, hay una diferencia de semántica :
- valor, referencia constante: el objeto pasado no será modificado por el método
- referencia: el objeto pasado puede ser modificado por el método
- puntero / const puntero: igual que la referencia (para el comportamiento), pero puede ser nulo
Tenga en cuenta que algunos idiomas (el tipo funcional como Haskell) no ofrecen referencia / puntero de forma predeterminada. Los valores son inmutables una vez creados. Aparte de algunas soluciones para tratar con el entorno exterior, no están tan restringidos por este uso y de alguna manera hacen que la depuración sea más fácil.
Su amigo debería saber que no hay absolutamente nada de malo en el paso por referencia o en el puntero: por ejemplo, swap
, no se puede implementar con el paso por valor.
Finalmente, el polimorfismo no permite la semántica paso por valor.
Ahora, hablemos de actuaciones.
Generalmente se acepta que las incorporaciones deben pasarse por valor (para evitar una indirección) y las grandes clases definidas por el usuario deben pasarse por referencia / puntero (para evitar la copia). De hecho, grande significa que el Constructor de Copias no es trivial.
Sin embargo, hay una pregunta abierta con respecto a las clases pequeñas definidas por el usuario. Algunos artículos publicados recientemente sugieren que, en algunos casos, el paso por valor podría permitir una mejor optimización del compilador, por ejemplo, en este caso:
Object foo(Object d) { d.bar(); return d; }
int main(int argc, char* argv[])
{
Object o;
o = foo(o);
return 0;
}
¡Aquí un compilador inteligente es capaz de determinar que o
puede modificarse en su lugar sin ningún tipo de copia! (Es necesario que la definición de la función sea visible, creo, no sé si Link-Time Optimization lo resolvería)
Por lo tanto, solo hay una posibilidad para el problema de rendimiento, como siempre: medir .
El problema de tu amigo no es tanto su idea como su religión. Dada cualquier función, siempre considere las ventajas y desventajas de pasar por valor, referencia, referencia constante, puntero o puntero inteligente. Entonces decide.
El único signo de diseño roto que veo aquí es la religión ciega de tu amigo.
Dicho esto, hay algunas firmas que no aportan mucho a la mesa. Tomar una constante por valor puede ser una tontería, porque si prometes no cambiar el objeto, entonces no deberías hacer tu propia copia. Por supuesto, a menos que sea una primitiva, en cuyo caso el compilador puede ser lo suficientemente inteligente como para tomar una referencia. O, a veces es torpe llevar un puntero a un puntero como argumento. Esto agrega complejidad; en su lugar, es posible que pueda salirse con la suya tomando una referencia a un puntero y obtenga el mismo efecto.
Pero no tome estas pautas como se establece en piedra; siempre considere sus opciones porque no hay pruebas formales que eliminen la utilidad de cualquier alternativa.
- Si necesita cambiar el argumento para sus propias necesidades, pero no desea afectar al cliente, tome el argumento por valor.
- Si desea proporcionar un servicio al cliente, y el cliente no está estrechamente relacionado con el servicio, entonces considere tomar un argumento por referencia.
- Si el cliente está estrechamente relacionado con el servicio, considere no tomar argumentos, pero escriba una función miembro.
- Si desea escribir una función de servicio para una familia de clientes que están estrechamente relacionados con el servicio pero que son muy distintos entre sí, considere tomar un argumento de referencia y quizás haga de la función un amigo de los clientes que necesitan esta amistad.
- Si no necesita cambiar el cliente, considere tomar una referencia constante.
Hay todo tipo de cosas que no se pueden hacer sin usar referencias, comenzando con un constructor de copias. Las referencias (o punteros) son fundamentales y, le guste o no, está usando referencias. (Una ventaja, o tal vez una desventaja, de las referencias es que no tiene que alterar el código, en general, para pasar una (const) referencia). Y no hay razón para no usar referencias la mayor parte del tiempo.
Y sí, pasar por valor está bien para objetos pequeños sin requisitos para la asignación dinámica, pero aún es tonto cojearse diciendo "sin referencias" sin mediciones concretas de que la llamada sobrecarga es (a) perceptible y (b) significativa . "La optimización prematura es la raíz de todo mal" 1 .
1 Varias atribuciones, incluyendo CA Hoare (aunque aparentemente lo niega).
Las respuestas que he visto hasta ahora se han centrado en el rendimiento: casos en los que el paso por referencia es más rápido que el paso por valor. Puede tener más éxito en su argumento si se enfoca en casos que son imposibles con el paso por valor.
Las tuplas o vectores pequeños son un tipo muy simple de estructura de datos. Las estructuras de datos más complejas comparten información, y ese intercambio no se puede representar directamente como valores. O bien necesitas usar referencias / punteros o algo que los simule, como matrices e índices.
Muchos problemas se reducen a datos que forman un gráfico o un gráfico dirigido. En ambos casos, tiene una mezcla de bordes y nodos que deben almacenarse dentro de la estructura de datos. Ahora tiene el problema de que los mismos datos deben estar en varios lugares. Si evita las referencias, primero debe duplicar los datos, y luego cada cambio debe replicarse cuidadosamente en cada una de las otras copias.
El argumento de su amigo se reduce a decir: abordar cualquier problema lo suficientemente complejo como para ser representado por un gráfico es un mal diseño ...
Lo primero es que la pila rara vez se desborda fuera de este sitio web, excepto en el caso de recursión.
Sobre su razonamiento, creo que puede estar equivocado porque está demasiado generalizado, pero lo que ha hecho puede ser correcto ... ¿o no?
Por ejemplo, la biblioteca de Windows Forms usa una estructura Rectangle
que tiene 4 miembros, el QuartzCore de Apple también tiene la estructura CGRect
, y esas estructuras siempre pasan por valor . Creo que podemos comparar eso con Vector con 3 variables de punto flotante.
Sin embargo, como no veo el código, siento que no debería juzgar lo que ha hecho, aunque tengo la sensación de que podría haber hecho lo correcto a pesar de su idea generalizada.
Subtipo-polimorfismo es un caso en el que pasar por valor no funcionaría porque cortaría la clase derivada a su clase base. Tal vez para algunos, el uso de subtipo-polimorfismo es un mal diseño?
Wow, ya hay 13 respuestas ... No leí todo en detalle, pero creo que esto es muy diferente de los demás ...
Él tiene un punto. La ventaja de pasar por valor como regla es que las subrutinas no pueden modificar sutilmente sus argumentos. Pasar referencias no constantes indicaría que cada función tiene efectos secundarios feos, lo que indica un diseño deficiente.
Simplemente explíquele la diferencia entre vector3 &
y vector3 const&
y demuestre cómo este último puede inicializarse mediante una constante como en vec_function( vector3(1,2,3) );
, pero no lo primero. Paso por referencia constante es una simple optimización de paso por valor.
Yo diría que no usar punteros en C es un signo de un programador novato.
Parece que tu amigo tiene miedo a los punteros.
Recuerde, los punteros de C ++ en realidad se heredaron del lenguaje C, y C se desarrolló cuando las computadoras eran mucho menos poderosas. Sin embargo, la velocidad y la eficiencia siguen siendo vitales hasta el día de hoy.
Entonces, ¿por qué usar punteros? ¡Le permiten al desarrollador optimizar un programa para que se ejecute más rápido o usar menos memoria que de otra manera! Hacer referencia a la ubicación de la memoria de un dato es mucho más eficiente que copiar todos los datos.
Los punteros generalmente son un concepto que es difícil de entender para aquellos que comienzan a programar, porque todos los experimentos realizados involucran pequeños arreglos, tal vez algunas estructuras, pero básicamente consisten en trabajar con un par de megabytes (si tiene suerte) cuando Tiene 1GB de memoria por toda la casa. En esta escena, un par de MB no son nada y por lo general es muy poco para tener un impacto significativo en el rendimiento de su programa.
Así que vamos a exagerar un poco. Piense en una matriz de caracteres con 2147483648 elementos (2 GB de datos) que necesita pasar a una función que escriba todos los datos en el disco. Ahora, ¿qué técnica crees que va a ser más eficiente / más rápida?
- Pase por valor, que tendrá que volver a copiar esos 2 GB de datos a otra ubicación en la memoria antes de que el programa pueda escribir los datos en el disco, o
- Pase por referencia, que solo se referirá a esa ubicación de memoria.
¿Qué pasa cuando no tienes 4GB de RAM? ¿Gastará $ y comprará chips de RAM solo porque tiene miedo de usar punteros?
Volver a copiar los datos en la memoria suena un poco redundante cuando no tiene que hacerlo, y es un desperdicio de recursos informáticos.
De todos modos, se paciente con tu amigo. Si quisiera convertirse en un programador serio / profesional en algún momento de su vida, eventualmente tendrá que tomarse el tiempo para entender los punteros.
Buena suerte.