java - pure - ¿Por qué no tener todas las funciones como virtual en C++?
virtual class c++ (11)
Pero supongo que con la velocidad arquitectónica moderna es casi despreciable.
Esta suposición es errónea y, supongo, la razón principal de esta decisión.
Consideremos el caso del alineamiento. La función de sort
C ++ ''se desempeña mucho más rápido que el qsort
de C, por lo demás similar, en algunos escenarios porque puede alinear su argumento comparador, mientras que C no puede (debido al uso de punteros de función). En casos extremos, esto puede significar diferencias de rendimiento de hasta 700% (Scott Meyers, Effective STL).
Lo mismo sería cierto para las funciones virtuales. Hemos tenido discusiones similares antes; por ejemplo, ¿hay alguna razón para usar C ++ en lugar de C, Perl, Python, etc.?
Sé que las funciones virtuales tienen una sobrecarga de desreferenciación para llamar a un método. Pero supongo que con la velocidad arquitectónica moderna es casi despreciable.
- ¿Hay alguna razón en particular por la que todas las funciones en C ++ no sean virtuales como en Java?
- Según mi conocimiento, definir una función virtual en una clase base es suficiente / necesario. Ahora, cuando escribo una clase para padres, es posible que no sepa qué métodos se anularían. Entonces, ¿eso significa que al escribir una clase secundaria alguien tendría que editar la clase principal? ¿Esto suena como inconveniente y en ocasiones no es posible?
Actualizar:
Resumiendo de la respuesta de Jon Skeet a continuación:
Es una compensación entre hacer que alguien se dé cuenta explícitamente de que está heredando la funcionalidad [que tiene riesgos potenciales en sí mismos [(verifique la respuesta de Jon)] [y las posibles pequeñas ganancias de rendimiento] con una compensación por menos flexibilidad, más cambios de código y una curva de aprendizaje más pronunciada.
Otras razones de diferentes respuestas:
Las funciones virtuales no se pueden alinear porque la alineación tiene que ocurrir en tiempo de ejecución. Esto tiene un impacto en el rendimiento cuando espera que las funciones se beneficien de la incorporación.
Podría haber otras razones, y me encantaría conocerlas y resumirlas.
Uno de los principios principales de C ++ es: usted solo paga por lo que usa ("principio de sobrecarga cero"). Si no necesita el mecanismo de envío dinámico, no debe pagar sus gastos generales.
Como autor de la clase base, debe decidir qué métodos deben permitirse anularse. Si estás escribiendo ambos, continúa y refactoriza lo que necesitas. Pero funciona de esta manera, porque tiene que haber una manera para que el autor de la clase base controle su uso.
El problema es que si bien Java se compila a un código que se ejecuta en una máquina virtual, no se puede hacer esa misma garantía para C ++. Es común usar C ++ como un reemplazo más organizado para C, y C tiene una traducción de 1: 1 al ensamblaje.
Si considera que 9 de cada 10 microprocesadores del mundo no están en una computadora personal o en un teléfono inteligente, verá el problema cuando considere que hay muchos procesadores que necesitan este acceso de bajo nivel.
C ++ fue diseñado para evitar esa deferencia oculta si no la necesitaba, manteniendo así la naturaleza 1: 1. Parte del primer código C ++ en realidad tuvo un paso intermedio para ser traducido a C antes de ejecutar un compilador de C a ensamblador.
Hay buenas razones para controlar qué métodos son virtuales más allá del rendimiento. Aunque en realidad no finalizo la mayoría de mis métodos en Java, probablemente debería ... a menos que un método esté diseñado para ser anulado, probablemente no debería ser IMO virtual.
El diseño para la herencia puede ser complicado, en particular, significa que necesita documentar mucho más sobre lo que podría llamarlo y lo que podría llamar. Imagínese si tiene dos métodos virtuales y uno llama al otro; eso debe documentarse; de lo contrario, alguien podría anular el método "llamado" con una implementación que llame al método "llamada", creando involuntariamente un desbordamiento de pila (o un bucle infinito si hay optimización de llamadas de cola). En ese momento, tendrá menos flexibilidad en su implementación; no podrá cambiarla más adelante.
Tenga en cuenta que C # es un lenguaje similar a Java de varias maneras, pero optó por hacer que los métodos no sean virtuales de forma predeterminada. Algunas otras personas no están interesadas en esto, pero ciertamente lo acojo con satisfacción, y en realidad prefiero que las clases también sean inevitables por defecto.
Básicamente, todo se reduce a este consejo de Josh Bloch: diseñar para heredar o prohibirlo.
La mayoría de las respuestas tienen que ver con la sobrecarga de las funciones virtuales, pero hay otras razones para no realizar ninguna función en una clase virtual, como el hecho de que cambiará la clase de diseño estándar a, bueno, diseño no estándar , y eso Puede ser un problema si necesita serializar datos binarios. Eso se resuelve de manera diferente en C #, por ejemplo, al tener la struct
una familia de tipos diferente a la de las class
.
Desde el punto de vista del diseño, cada función pública establece un contrato entre su tipo y los usuarios del tipo, y cada función virtual (pública o no) establece un contrato diferente con las clases que amplían su tipo. Cuanto mayor sea el número de contratos que firme, menor será el margen para los cambios que tiene. De hecho, hay bastantes personas, incluidos algunos escritores bien conocidos, que defienden que la interfaz pública nunca debe contener funciones virtuales, ya que su compromiso con sus clientes puede ser diferente de los compromisos que requiere de sus extensiones. Es decir, las interfaces públicas muestran lo que usted hace por sus clientes, mientras que la interfaz virtual muestra cómo otros pueden ayudarlo a hacerlo.
Otro efecto de las funciones virtuales es que siempre se envían al overrider final (a menos que califique la llamada explícitamente), y eso significa que cualquier función que sea necesaria para mantener sus invariantes (piense en el estado de las variables privadas) no debe ser virtual : si una clase lo extiende, tendrá que hacer una llamada calificada explícita al padre o romper los invariantes en su nivel.
Esto es similar al ejemplo del desbordamiento de bucle / pila infinito que mencionó @Jon Skeet, solo de una manera diferente: tiene que documentar en cada función si accede a algún atributo privado para que las extensiones aseguren que la función se llame al tiempo correcto. Y eso, a su vez, significa que está rompiendo la encapsulación y tiene una abstracción con fugas : sus detalles internos ahora son parte de la interfaz (documentación + requisitos en sus extensiones), y no puede modificarlos como desee.
Luego está el rendimiento ... habrá un impacto en el rendimiento, pero en la mayoría de los casos está sobrevalorado, y se podría argumentar que solo en los pocos casos en los que el rendimiento es crítico, se retiraría y declararía las funciones como no virtuales. . Por otra parte, eso podría no ser simple en un producto construido, ya que las dos interfaces (extensiones públicas +) ya están vinculadas.
Las llamadas a los métodos Java son mucho más eficientes que C ++ debido a la optimización del tiempo de ejecución.
Lo que necesitamos es compilar C ++ en bytecode y ejecutarlo en JVM.
Llegué bastante tarde a la fiesta aquí, así que agregaré una cosa que no he notado y que resumiré en otras respuestas, y resumiré rápidamente ...
Usabilidad en la memoria compartida : una implementación típica de envío virtual tiene un puntero a una tabla de envío virtual específica de clase en cada objeto. Las direcciones en estos punteros son específicas del proceso de creación, lo que significa que los sistemas de procesos múltiples que acceden a objetos en la memoria compartida no pueden enviarse utilizando el objeto de otro proceso. Esa es una limitación inaceptable dada la importancia de la memoria compartida en los sistemas multiproceso de alto rendimiento.
Encapsulación : la capacidad de un diseñador de clases para controlar los miembros a los que se accede mediante el código del cliente, lo que garantiza que se mantengan la semántica y las invariantes de la clase. Por ejemplo, si deriva de
std::string
(puedo obtener algunos comentarios por atreverse a sugerir que ;-P), entonces puede usar todas las operaciones normales de insertar / borrar / agregar y asegurarse de que, siempre que no haga cualquier cosa que siempre tenga un comportamiento indefinido parastd::string
como pasar valores de posición erróneos a las funciones: los datos destd::string
serán sólidos. Alguien que verifique o mantenga su código no tiene que verificar si ha cambiado el significado de esas operaciones. Para una clase, la encapsulación garantiza la libertad de modificar posteriormente la implementación sin romper el código del cliente. Otra perspectiva sobre la misma declaración: el código del cliente puede usar la clase de la forma que quiera sin ser sensible a los detalles de la implementación. Si se puede cambiar cualquier función en una clase derivada, todo el mecanismo de encapsulación simplemente se elimina.- Dependencias ocultas : cuando no sabes qué otras funciones dependen de la que estás anulando, ni que la función fue diseñada para ser anulada, no puedes razonar sobre el impacto de tu cambio. Por ejemplo, piensas que "siempre quise esto", y cambias
std::string::operator[]()
yat()
para considerar que los valores negativos (después de una conversión de tipo a firmados) se compensan al revés de la final de la cadena. Pero, tal vez alguna otra función estaba usandoat()
como una especie de aseveración de que un índice era válido (sabiendo que se lanzaría de otra manera) antes de intentar una inserción o eliminación ... ese código podría pasar de lanzarse de una manera especificada por el Estándar a tener un comportamiento indefinido (pero probablemente letal). - Documentación : al hacer que una función sea
virtual
, usted está documentando que es un punto de personalización y parte de la API para el código del cliente.
- Dependencias ocultas : cuando no sabes qué otras funciones dependen de la que estás anulando, ni que la función fue diseñada para ser anulada, no puedes razonar sobre el impacto de tu cambio. Por ejemplo, piensas que "siempre quise esto", y cambias
Inlining - lado del código y uso de la CPU: el despacho virtual complica el trabajo del compilador de resolver cuándo llamar a las funciones en línea, y por lo tanto podría proporcionar un código peor en términos de espacio / aumento de volumen y uso de CPU.
Indirección durante las llamadas : incluso si una llamada fuera de línea se realiza de cualquier manera, hay un pequeño costo de rendimiento para el envío virtual que puede ser significativo al llamar repetidamente a funciones trivialmente simples en sistemas de rendimiento crítico. (Debe leer el puntero por objeto a la tabla de envío virtual, luego la entrada de la tabla de envío virtual en sí misma - significa que las páginas VDT también están consumiendo caché).
Uso de la memoria : los punteros por objeto a las tablas de distribución virtuales pueden representar una pérdida significativa de memoria, especialmente para matrices de objetos pequeños. Esto significa que menos objetos caben en el caché y puede tener un impacto significativo en el rendimiento.
Diseño de memoria : es esencial para el rendimiento y altamente conveniente para la interoperabilidad, ya que C ++ puede definir clases con el diseño de memoria exacto de los datos de miembros especificados por la red o estándares de datos de varias bibliotecas y protocolos. Estos datos a menudo provienen de fuera de su programa C ++ y pueden generarse en otro idioma. Dichos protocolos de comunicaciones y almacenamiento no tendrán "espacios vacíos" para los punteros a las tablas de envío virtuales, y como se mencionó anteriormente, incluso si lo tuvieran, y el compilador de alguna manera le permite inyectar los punteros correctos para su proceso sobre los datos entrantes, lo que frustraría Acceso multiproceso a los datos. El código de serialización / deserialización / comunicación basado en el tamaño / puntero, pero práctico, también se haría más complicado y potencialmente más lento.
Parece que esta pregunta podría tener algunas respuestas. Las funciones virtuales no deben usarse excesivamente. ¿Por qué? . En mi opinión, lo único que se destaca es que solo agrega más complejidad en términos de saber qué se puede hacer con herencia.
Sí, es debido a la sobrecarga de rendimiento. Los métodos virtuales son llamados usando tablas virtuales e indirectas.
En Java todos los métodos son virtuales y la sobrecarga también está presente. Pero, a diferencia de C ++, el compilador JIT perfila el código durante el tiempo de ejecución y puede alinear aquellos métodos que no usan esta propiedad. Entonces, JVM sabe dónde es realmente necesario y dónde no lo libera de tomar la decisión por su cuenta.
Te olvidas de una cosa. La sobrecarga también está en la memoria, es decir, se agrega una tabla virtual y un puntero a esa tabla para cada objeto. Ahora bien, si tiene un objeto que tiene un número significativo de instancias esperadas, entonces no es despreciable. ejemplo, millones de instancias equivalen a 4 Mega bytes. Estoy de acuerdo en que para una aplicación simple esto no es mucho, pero para los dispositivos en tiempo real, como los enrutadores, esto cuenta.
Pago por uso (en palabras Bjarne Stroustrup).