¿Por qué no tener todas las funciones como virtuales en C++?


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 insignificante.

  1. ¿Hay alguna razón en particular por la que todas las funciones en C++ no son virtuales como en Java?
  2. Desde mi conocimiento, definir una función virtual en una clase base es suficiente/necesario. Ahora, cuando escribo una clase de padre, es posible que no sepa qué métodos se anularían. Así que eso significa que al escribir una clase de niño alguien tendría que editar la clase padre. Esto suena como inconveniente y a veces no es posible?

Actualización:
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 funcionalidad [que tiene riesgos potenciales en sí mismos [(verifique la respuesta de Jon)] [y posibles pequeñas ganancias de rendimiento] con una compensación por menos flexibilidad, más cambios de código y un aprendizaje más pronunciado curva.

Otras razones de diferentes respuestas:

Las funciones virtuales no pueden estar alineadas porque tienen que ocurrir en tiempo de ejecución. Esto tiene impactos en el rendimiento cuando espera que sus funciones se beneficien de la inserción.

Podría haber otras razones potencialmente, y me encantaría conocerlas y resumirlas.

Author: Kara, 2011-07-07

11 answers

Hay buenas razones para controlar qué métodos son virtuales más allá del rendimiento. Si bien no en realidad hacer la mayoría de mis métodos finales en Java, probablemente debería... a menos que un método esté diseñado para ser sobrescrito, probablemente no debería ser IMO virtual.

Diseñar para la herencia puede ser complicado, en particular, significa que necesita documentar mucho más sobre cómo podría llamarlo y cómo podría llamarlo. Imagine si tiene dos métodos virtuales, y uno llama al otro - que debe estar documentado, de lo contrario alguien podría sobrescribir el método "called" con una implementación que llama al método "calling", creando inconscientemente un desbordamiento de pila (o bucle infinito si hay optimización de llamada de cola). En ese momento, tiene menos flexibilidad en su implementación, no puede cambiarla en una fecha posterior.

Tenga en cuenta que C# es un lenguaje similar a Java en varias formas, pero eligió hacer que los métodos no sean virtuales por defecto. Otras personas no estoy interesado en esto, pero ciertamente lo acojo con satisfacción, y en realidad preferiría que las clases no fueran hereditarias por defecto también.

Básicamente, todo se reduce a este consejo de Josh Bloch: diseñar para la herencia o prohibirlo.

 74
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-07 06:29:09
  1. Uno de los principios principales de C++ es: solo paga por lo que usa ("principio de sobrecarga cero"). Si no necesita el mecanismo de envío dinámico, no debe pagar por sus gastos generales.

  2. Como el autor de la clase base, usted debe decidir qué métodos se debe permitir que se anulen. Si estás escribiendo ambos, sigue adelante y refactoriza lo que necesites. Pero funciona de esta manera, porque tiene que haber una manera para que el autor de la clase base controle su utilizar.

 50
Author: eran,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-07 06:33:57

Pero supongo que con la velocidad arquitectónica moderna es casi insignificante.

Esta suposición es errónea, y, supongo, la razón principal de esta decisión.

Considere el caso de la inserción. La función sort de C++ realiza mucho más rápido que la qsort de C 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.?

 28
Author: Konrad Rudolph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-12 07:31:21

La mayoría de las respuestas tratan de la sobrecarga de funciones virtuales, pero hay otras razones para no hacer ninguna función en una clase virtual, como el hecho de que cambiará la clase de standard-layout a, bueno, non-standard-layout, y eso puede ser un problema si necesita serializar datos binarios. Esto se resuelve de manera diferente en C#, por ejemplo, al tener structs siendo una familia de tipos diferente a classes.

Desde el punto de vista del diseño, cada público function 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 extienden su tipo. Cuanto mayor sea el número de dichos contratos que firme, menos espacio tendrá para los cambios. De hecho, hay bastantes personas, incluidos algunos escritores conocidos, que defienden que la interfaz pública nunca debe contener funciones virtuales, ya que su compromiso con sus clientes podría ser diferente de los compromisos que necesita de sus extensiones. Es decir, las interfaces públicas muestran lo que haces por tus clientes, mientras que la interfaz virtual muestra cómo otros pueden ayudarte a hacerlo.

Otro efecto de las funciones virtuales es que siempre se envían al overrider final (a menos que califique explícitamente la llamada), y eso significa que cualquier función que se necesite para mantener sus invariantes (piense en el estado de las variables privadas) no debería ser virtual: si un la clase lo extiende, tendrá que hacer una llamada explícita calificada al padre o rompería las invariantes en su nivel.

Esto es similar al ejemplo del desbordamiento de bucle/pila infinito que @Jon Skeet mencionó, solo de una manera diferente: debe documentar en cada función si accede a algún atributo privado para que las extensiones aseguren que la función se llame en el momento adecuado. Y eso a su vez significa que estás rompiendo la encapsulación y tener una abstracción filtrada: Sus detalles internos ahora son parte de la interfaz (documentación + requisitos en sus extensiones), y no puede modificarlos como desee.

Entonces hay 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 donde el rendimiento es crítico, se retiraría y declararía las funciones no virtuales. Por otra parte, que podría no ser simple en un producto construido, dado que las dos interfaces (public + extensions) ya están enlazadas.

 13
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-07 07:47:52

Olvidas una cosa. La sobrecarga también está en memoria, es decir, agrega una tabla virtual y un puntero a esa tabla para cada objeto. Ahora, si tiene un objeto que tiene un número significativo de instancias esperadas, entonces no es insignificante. por ejemplo, million instance equivale a 4 Mega byte. Estoy de acuerdo en que para una aplicación simple esto no es mucho, pero para dispositivos en tiempo real, como los enrutadores, esto cuenta.

 7
Author: roni,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-07 11:37:29

Pay per use (en palabras de Bjarne Stroustrup).

 5
Author: iammilind,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-07 06:32:02

Llego bastante tarde a la fiesta aquí, así que agregaré una cosa que no he notado cubierta en otras respuestas, y resumiré rápidamente...

  • Usabilidad en memoria compartida: una implementación típica de virtual dispatch tiene un puntero a una tabla de virtual dispatch específica de clase en cada objeto. Las direcciones en estos punteros son específicas para el proceso que los crea, lo que significa que los sistemas multiproceso que acceden a objetos en memoria compartida no pueden despacharse usando otro objeto del proceso! Esa es una limitación inaceptable dada la importancia de la memoria compartida en sistemas multiproceso de alto rendimiento.

  • Encapsulation : la capacidad de un diseñador de clases para controlar los miembros a los que se accede por código cliente, asegurando que la semántica de las clases y los invariantes se mantengan. Por ejemplo, si se deriva de std::string (puede que reciba algunos comentarios por atreverse a sugerir eso; - P), entonces puede usar todas las operaciones normales de insertar / borrar / anexar y asegúrese de que - siempre que no haga nada que sea siempre un comportamiento indefinido para std::string como pasar valores de posición defectuosos a las funciones, los datos de std::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 instrucción: el código cliente puede usar la clase de la manera que desee sin ser sensible a la detalles de implementación. Si cualquier función puede ser cambiada en una clase derivada, todo ese mecanismo de encapsulación es simplemente volado.

    • Dependencias ocultas: cuando no sabe qué otras funciones dependen de la que está sobreescribiendo, ni que la función fue diseñada para ser sobreescrita, entonces no puede razonar sobre el impacto de su cambio. Por ejemplo, usted piensa "Siempre he querido esto", y cambiar std::string::operator[]() y at() para considerar los valores negativos (después de un type-cast to signed) para ser compensados hacia atrás desde el final de la cuerda. Pero, tal vez alguna otra función estaba usando at() como una especie de afirmación de que un índice era válido - sabiendo que lanzaría de lo contrario - antes de intentar una inserción o eliminación... ese código podría pasar de lanzar de una manera estándar a tener un comportamiento indefinido (pero probablemente letal).
    • Documentación : al hacer una función virtual, estás documentando que es un punto pretendido de personalización, y parte de la API para el código del cliente a utilizar.

  • Inlining - lado de código y uso de CPU: virtual dispatch complica el trabajo del compilador de calcular cuándo hacer llamadas a funciones en línea, y por lo tanto podría proporcionar un código peor en términos de espacio/hinchazón y uso de CPU.

  • Indirección durante las llamadas: incluso si se realiza una llamada fuera de línea de cualquier manera, hay un pequeño costo de rendimiento para el despacho virtual que puede ser importante cuando se llaman funciones trivialmente simples repetidamente en sistemas críticos de rendimiento. (Debe leer el puntero por objeto a la tabla de despacho virtual, luego la entrada de la tabla de despacho virtual en sí misma, lo que significa que las páginas de VDT también consumen caché.)

  • Uso de memoria: los punteros por objeto a las tablas de despacho virtual pueden representar un desperdicio significativo de memoria, especialmente para matrices de objetos pequeños. Esto significa que caben menos objetos en la caché, y puede tener un impacto significativo en el rendimiento.

  • Diseño de memoria: es esencial para el rendimiento, y muy conveniente para la interoperabilidad, que C++ pueda definir clases con el diseño de memoria exacto de datos de miembros especificados por estándares de red o datos de varias bibliotecas y protocolos. Esos datos a menudo provienen de fuera de su programa de C++ y pueden generarse en otro lenguaje. Tales comunicaciones y protocolos de almacenamiento no tendrán "huecos" para los punteros a virtual despachar tablas, y como se discutió anteriormente, incluso si lo hicieran, y el compilador de alguna manera le permite inyectar eficientemente los punteros correctos para su proceso sobre los datos entrantes, eso frustraría el acceso multiproceso a los datos. El código de comunicaciones basado en serialización/deserialización/tamaño, crudo pero práctico, también se haría más complicado y potencialmente más lento.

 5
Author: Tony Delroy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-08 02:35:48

Parece que esta pregunta podría tener algunas respuestas Las funciones virtuales no deben usarse en exceso - ¿Por qué ?. En mi opinión, lo único que destaca es que solo agrega más complejidad en términos de saber qué se puede hacer con la herencia.

 3
Author: Kevin Jalbert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 10:30:48

Sí, es debido a la sobrecarga de rendimiento. Los métodos virtuales se llaman usando tablas virtuales e indirección.

En Java todos los métodos son virtuales y la sobrecarga también está presente. Pero, contrariamente a 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. Por lo tanto, JVM sabe dónde es realmente necesario y dónde no lo libera de tomar la decisión por su cuenta.

 2
Author: Rekin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-07 06:32:39

El problema es que mientras Java compila código que se ejecuta en una máquina virtual, esa misma garantía no se puede hacer para C++. Es común usar C++ como un reemplazo más organizado para C, y C tiene una traducción 1: 1 al ensamblado.

Si considera que 9 de cada 10 microprocesadores en el mundo no están en una computadora personal o un teléfono inteligente, verá el problema cuando considere además que hay muchos procesadores que necesitan este acceso de bajo nivel.

C++ fue diseñado para evita ese aplazamiento oculto si no lo necesitabas, manteniendo así esa naturaleza 1: 1. Algunos de los primeros códigos de C++ en realidad tenían un paso intermedio de ser traducidos a C antes de correr a través de un compilador de C-to-assembly.

 1
Author: Ape-inago,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-14 19:09:21

Las llamadas a 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.

 -5
Author: irreputable,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-07 07:15:02