Stroustrup dice "No inventes de inmediato una base única para todas tus clases (una clase de Objeto). Por lo general, puedes hacerlo mejor sin ella para la mayoría de las clases". (El lenguaje de programación C ++ cuarta edición, sección 1.3.4)
¿Por qué una clase base para todo generalmente es una mala idea, y cuándo tiene sentido crear una?
c++
object-oriented
object-oriented-design
Matthew James Briggs
fuente
fuente
Respuestas:
Porque, ¿qué tendría ese objeto para la funcionalidad? En java, toda la clase Base tiene un toString, un hashCode e igualdad y una variable monitor + condición.
ToString solo es útil para la depuración.
hashCode solo es útil si desea almacenarlo en una colección basada en hash (la preferencia en C ++ es pasar una función de hash al contenedor como parámetro de plantilla o evitar por
std::unordered_*
completo y, en su lugar, usarstd::vector
listas simples y sin ordenar).La igualdad sin un objeto base se puede ayudar en el momento de la compilación, si no tienen el mismo tipo, entonces no pueden ser iguales. En C ++ este es un error de tiempo de compilación.
la variable monitor y condición se incluye mejor explícitamente caso por caso.
Sin embargo, cuando hay más de lo que necesita hacer, entonces hay un caso de uso.
Por ejemplo, en QT existe la
QObject
clase raíz que forma la base de la afinidad de hilos, la jerarquía de propiedad padre-hijo y el mecanismo de ranuras de señal. También fuerza el uso por puntero para QObjects. Sin embargo, muchas clases en Qt no heredan QObject porque no necesitan la ranura de señal (particularmente los tipos de valor de alguna descripción).fuente
Object
._hashCode
no es 'usar un contenedor diferente' sino señalar que C ++std::unordered_map
hace hashing usando un argumento de plantilla, en lugar de requerir que la clase de elemento misma proporcione la implementación. Es decir, como todos los otros buenos contenedores y administradores de recursos en C ++, no es intrusivo; no contamina todos los objetos con funciones o datos en caso de que alguien los necesite en algún contexto más adelante.Porque no hay funciones compartidas por todos los objetos. No hay nada que poner en esta interfaz que tenga sentido para todas las clases.
fuente
Cada vez que construye jerarquías de herencia altas de objetos, tiende a encontrarse con el problema de la Clase Base Frágil (Wikipedia) .
Tener muchas pequeñas jerarquías de herencia separadas (distintas, aisladas) reduce las posibilidades de encontrarse con este problema.
Hacer que todos sus objetos formen parte de una única jerarquía de herencia enorme garantiza prácticamente que se encontrará con este problema.
fuente
cout.print(x).print(0.5).print("Bye\n")
, no dependeoperator<<
.Porque:
La implementación de cualquier tipo de
virtual
función introduce una tabla virtual, que requiere una sobrecarga de espacio por objeto que no es necesaria ni deseada en muchas (¿la mayoría?) Situaciones.Implementar de forma
toString
no virtual sería bastante inútil, porque lo único que podría devolver es la dirección del objeto, que es muy amigable para el usuario y al que la persona que llama ya tiene acceso, a diferencia de Java.Del mismo modo, una versión no virtual
equals
ohashCode
solo podría usar direcciones para comparar objetos, lo que de nuevo es bastante inútil y, a menudo, incluso completamente incorrecto: a diferencia de Java, los objetos se copian con frecuencia en C ++ y, por lo tanto, distinguir la "identidad" de un objeto no es ni siquiera Siempre significativo o útil. (por ejemplo, unint
realmente no debería tener una identidad distinta de su valor ... dos enteros del mismo valor deberían ser iguales)fuente
open
palabra clave. Sin embargo, no creo que haya ido más allá de unos pocos documentos.shared_ptr<Foo>
para ver si también es unshared_ptr<Bar>
(o lo mismo con otros tipos de puntero), incluso siFoo
yBar
son clases no relacionadas, que no saben nada el uno del otro. Exigir que tal cosa funcione con "punteros en bruto", dada la historia de cómo se usan tales cosas, sería costoso, pero para cosas que de todos modos se almacenarán en un montón, el costo adicional sería mínimo.Tener un objeto raíz limita lo que puede hacer y lo que puede hacer el compilador, sin mucha recompensa.
Una clase raíz común hace posible crear contenedores de cualquier cosa y extraer lo que son con un
dynamic_cast
, pero si necesita contenedores de cualquier cosa, algo similarboost::any
puede hacerlo sin una clase raíz común. Yboost::any
también admite primitivas: incluso puede admitir la pequeña optimización del búfer y dejarlos casi "sin caja" en lenguaje Java.C ++ admite y prospera en los tipos de valor. Ambos literales y tipos de valores escritos del programador. Los contenedores C ++ almacenan, clasifican, combinan, consumen y producen de manera eficiente tipos de valor.
La herencia, especialmente el tipo de herencia monolítica que implican las clases base de estilo Java, requiere tipos de "puntero" o "referencia" basados en la tienda libre. Su identificador / puntero / referencia a datos contiene un puntero a la interfaz de la clase, y polimórficamente podría representar algo más.
Si bien esto es útil en algunas situaciones, una vez que se ha casado con el patrón con una "clase base común", ha bloqueado toda su base de código en el costo y el equipaje de este patrón, incluso cuando no es útil.
Casi siempre sabe más sobre un tipo que "es un objeto" en el sitio de llamada o en el código que lo utiliza.
Si la función es simple, escribir la función como una plantilla le proporciona un polimorfismo basado en el tiempo de compilación tipo pato donde la información en el sitio de llamada no se desecha. Si la función es más compleja, se puede borrar el tipo mediante el cual las operaciones uniformes en el tipo que desea realizar (digamos, serialización y deserialización) se pueden construir y almacenar (en tiempo de compilación) para que el consumidor las consuma (en tiempo de ejecución) código en una unidad de traducción diferente.
Supongamos que tiene una biblioteca donde desea que todo sea serializable. Un enfoque es tener una clase base:
Ahora cada bit de código que escribes puede ser
serialization_friendly
.Excepto no un
std::vector
, así que ahora necesitas escribir cada contenedor. Y no esos enteros que obtuviste de esa biblioteca bignum. Y no ese tipo que escribió que no creía que fuera necesario serializar. Y no atuple
, o aint
o adouble
, o astd::ptrdiff_t
.Tomamos otro enfoque:
que consiste en, bueno, no hacer nada, aparentemente. Excepto que ahora podemos extender
write_to
anulandowrite_to
como una función libre en el espacio de nombres de un tipo o un método en el tipo.Incluso podemos escribir un poco de código de borrado de tipo:
y ahora podemos tomar un tipo arbitrario y colocarlo automáticamente en una
can_serialize
interfaz que le permite invocarserialize
en un punto posterior a través de una interfaz virtual.Entonces:
es una función que toma cualquier cosa que pueda serializar, en lugar de
y la primera, a diferencia de la segunda, que puede manejar
int
,std::vector<std::vector<Bob>>
de forma automática.No tardó mucho en escribirlo, especialmente porque este tipo de cosas es algo que rara vez quieres hacer, pero obtuvimos la capacidad de tratar cualquier cosa como serializable sin requerir un tipo base.
Además, ahora podemos hacer que se pueda
std::vector<T>
serializar como un ciudadano de primera clase simplemente anulandowrite_to( my_buffer*, std::vector<T> const& )
: con esa sobrecarga, se puede pasar acan_serialize
ay la serialización de losstd::vector
archivos se almacena en una tabla virtual y se accede a ella.write_to
.En resumen, C ++ es lo suficientemente potente como para implementar las ventajas de una sola clase base sobre la marcha cuando sea necesario, sin tener que pagar el precio de una jerarquía de herencia forzada cuando no es necesario. Y los tiempos en que se requiere la base única (falsa o no) es razonablemente rara.
Cuando los tipos son en realidad su identidad, y usted sabe cuáles son, abundan las oportunidades de optimización. Los datos se almacenan localmente y de forma contigua (lo cual es muy importante para la compatibilidad de la memoria caché en los procesadores modernos), los compiladores pueden comprender fácilmente lo que hace una operación determinada (en lugar de tener un puntero de método virtual opaco que debe saltar, lo que lleva a un código desconocido en el otro lado) que permite reordenar las instrucciones de manera óptima, y se clavan menos clavijas redondas en agujeros redondos.
fuente
Hay muchas buenas respuestas anteriores, y el hecho claro de que todo lo que haría con una clase de base de todos los objetos se puede hacer mejor de otras maneras, como se muestra en la respuesta de @ ratchetfreak y los comentarios al respecto es muy importante, pero hay otra razón, que es evitar crear diamantes de herenciacuando se usa herencia múltiple. Si tuviera alguna funcionalidad en una clase base universal, tan pronto como comenzara a usar la herencia múltiple, tendría que comenzar a especificar a qué variante desea acceder, ya que podría sobrecargarse de manera diferente en las diferentes rutas de la cadena de herencia. Y la base no puede ser virtual, porque esto sería muy ineficiente (requiriendo que todos los objetos tengan una tabla virtual a un costo potencialmente enorme en el uso de la memoria y la localidad). Esto se convertiría en una pesadilla logística muy rápidamente.
fuente
De hecho, los primeros compiladores y bibliotecas de C ++ de Microsofts (sé sobre Visual C ++, 16 bits) tenían una clase de este tipo
CObject
.Sin embargo, debe saber que en ese momento las "plantillas" no eran compatibles con este simple compilador de C ++, por lo que las clases como
std::vector<class T>
no eran posibles. En cambio, una implementación "vectorial" solo podía manejar un tipo de clase, por lo que había una clase que es comparable a lastd::vector<CObject>
actual. Debido a queCObject
era la clase base de casi todas las clases (desafortunadamente no delCString
equivalente destring
los compiladores modernos), podría usar esta clase para almacenar casi todo tipo de objetos.Debido a que los compiladores modernos admiten plantillas, este caso de uso de una "clase base genérica" ya no se da.
Debe pensar en el hecho de que usar una clase base genérica de este tipo costará (un poco) memoria y tiempo de ejecución, por ejemplo, en la llamada al constructor. Por lo tanto, existen inconvenientes cuando se usa una clase de este tipo, pero al menos cuando se usan los compiladores modernos de C ++ casi no hay caso de uso para dicha clase.
fuente
TObject
incluso antes de que existiera MFC. No culpe a Microsoft por esa parte del diseño, parecía una buena idea para casi todos en ese momento.Voy a sugerir otra razón que proviene de Java.
Porque no puedes crear una clase base para todo, al menos no sin un montón de placa de caldera.
Es posible que pueda salirse con la suya en sus propias clases, pero probablemente encontrará que termina duplicando mucho código. Por ejemplo, "No puedo usar
std::vector
aquí ya que no se implementaIObject
; será mejor que cree un nuevo derivadoIVectorObject
que haga lo correcto ...".Este será el caso cuando se trate de clases de biblioteca incorporadas o estándar o de otras bibliotecas.
Ahora bien, si se construye en el idioma que acabaría con cosas como el
Integer
yint
confusión que está en java, o un gran cambio en la sintaxis del lenguaje. (Eso sí, creo que algunos otros idiomas han hecho un buen trabajo al incorporarlo a todos los tipos; ruby parece un mejor ejemplo).También tenga en cuenta que si su clase base no es polimórfica en tiempo de ejecución (es decir, usando funciones virtuales), podría obtener el mismo beneficio al usar rasgos como el marco.
por ejemplo, en lugar de
.toString()
usted podría tener lo siguiente: (NOTA: Sé que puede hacer esto mejor usando bibliotecas existentes, etc., es solo un ejemplo ilustrativo).fuente
Podría decirse que "vacío" cumple muchos de los roles de una clase base universal. Puede lanzar cualquier puntero a un
void*
. Luego puede comparar esos punteros. Puedesstatic_cast
volver a la clase original.Sin embargo, lo que no puede hacer con
void
lo que puede hacerObject
es usar RTTI para averiguar qué tipo de objeto realmente tiene. En última instancia, esto se debe a que no todos los objetos en C ++ tienen RTTI, y de hecho es posible tener objetos de ancho cero.fuente
[[no_unique_address]]
, que los compiladores pueden usar para dar ancho cero a los subobjetos de los miembros.[[no_unique_address]]
permitirá que el compilador a las variables miembro de EBO.Java toma la filosofía de diseño de que el comportamiento indefinido no debería existir . Código como:
probará si
felix
tiene un subtipo deCat
esa interfaz de implementosWoofer
; si lo hace, realizará el lanzamiento y la invocaciónwoof()
y, si no lo hace, arrojará una excepción. El comportamiento del código está completamente definido si sefelix
implementaWoofer
o no .C ++ toma la filosofía de que si un programa no debe intentar alguna operación, no debería importar lo que haría el código generado si se intentara esa operación, y la computadora no debería perder el tiempo tratando de restringir el comportamiento en casos que "deberían" nunca te levantes. En C ++, agregando los operadores de indirección apropiados para convertir a
*Cat
a a*Woofer
, el código produciría un comportamiento definido cuando el reparto es legítimo, pero un comportamiento indefinido cuando no lo es .Tener un tipo base común para las cosas hace posible validar los lanzamientos entre derivados de ese tipo base, y también realizar operaciones de lanzamiento de prueba, pero validar los lanzamientos es más costoso que simplemente asumir que son legítimos y esperar que no pase nada malo. La filosofía de C ++ sería que dicha validación requiere "pagar por algo que [generalmente] no necesita".
Otro problema relacionado con C ++, pero que no sería un problema para un nuevo lenguaje, es que si varios programadores crean una base común, derivan sus propias clases a partir de eso y escriben código para trabajar con cosas de esa clase base común, dicho código no podrá funcionar con objetos desarrollados por programadores que usaron una clase base diferente. Si un nuevo idioma requiere que todos los objetos de montón tengan un formato de encabezado común y nunca haya permitido objetos de montón que no lo tengan, entonces un método que requiera una referencia a un objeto de montón con dicho encabezado aceptará una referencia a cualquier objeto de montón cualquiera podría alguna vez crear.
Personalmente, creo que tener un medio común para preguntarle a un objeto "¿eres convertible a tipo X" es una característica muy importante en un lenguaje / marco, pero si esa característica no está integrada en un idioma desde el principio, es difícil agréguelo más tarde. Personalmente, creo que dicha clase base debería agregarse a una biblioteca estándar a la primera oportunidad, con una fuerte recomendación de que todos los objetos que se usarán polimórficamente deberían heredar de esa base. Hacer que los programadores implementen sus propios "tipos base" dificultaría el paso de objetos entre el código de diferentes personas, pero tener un tipo base común del que muchos programadores heredaron facilitaría la tarea.
APÉNDICE
Usando plantillas, es posible definir un "titular de objeto arbitrario" y preguntarle sobre el tipo de objeto contenido en él; El paquete Boost contiene algo llamado
any
. Por lo tanto, a pesar de que C ++ no tiene un tipo estándar de "referencia de tipo comprobable a nada", es posible crear uno. Esto no resuelve el problema mencionado al no tener algo en el estándar del lenguaje, es decir, incompatibilidad entre implementaciones de diferentes programadores, pero explica cómo funciona C ++ sin tener un tipo base del que todo se deriva: haciendo posible crear algo que actúa como unofuente
Woofer
es una interfaz yCat
es heredable, el elenco sería legítimo porque podría existir (si no ahora, posiblemente en el futuro) unaWoofingCat
que heredeCat
e implementeWoofer
. Tenga en cuenta que, según el modelo de compilación / vinculación de Java, la creación de aWoofingCat
no requeriría acceso al código fuente paraCat
niWoofer
.Cat
a ayWoofer
responderá la pregunta "¿es convertible a tipo X". C ++ te permitirá forzar un lanzamiento, porque bueno, tal vez realmente sepas lo que estás haciendo, pero también te ayudará si eso no es lo que realmente quieres hacer.dynamic_cast
tendrá un comportamiento definido si apunta a un objeto polimórfico, y un comportamiento indefinido si no lo hace, así que desde una perspectiva semántica ...Symbian C ++ tenía, de hecho, una clase base universal, CBase, para todos los objetos que se comportaban de una manera particular (principalmente si se asignaban al montón). Proporcionó un destructor virtual, puso a cero la memoria de la clase en la construcción y ocultó el constructor de la copia.
La razón subyacente era que era un lenguaje para sistemas embebidos y que los compiladores y especificaciones de C ++ eran realmente una mierda hace 10 años.
No todas las clases heredan de esto, solo algunas.
fuente