¿Por qué no hay una clase base en C ++?

84

Pregunta rápida: desde el punto de vista del diseño, ¿por qué, en C ++, no hay una clase base madre de todos, lo que suele ocurrir objecten otros lenguajes?

slezica
fuente
27
En realidad, se trata más de una cuestión filosófica que de una cuestión de diseño. La respuesta corta es "porque aparentemente Bjarne no quería hacer eso".
Jerry Coffin
4
Tampoco hay nada que le impida hacer uno.
GWW
22
Personalmente, creo que es un defecto de diseño tener todo derivado de la misma clase base. Promueve un estilo de programación que causa más problemas de los que vale (ya que tiende a tener contenedores genéricos de objeto de los que luego necesita para usar el objeto real (esto frunzo el ceño como un mal diseño en mi humilde opinión)). ¿Realmente quiero un contenedor que pueda albergar tanto coches como objetos de automatización de mando?
Martin York
3
@Martin, pero míralo de esta manera: por ejemplo, hasta C ++ 0x auto, tenías que usar definiciones de tipo de una milla de largo para iteradores o s de una sola vez typedef. Con una jerarquía de clases más genérica, podría usar objecto iterator.
slezica
9
@Santiago: El uso de un sistema de tipos unificado casi siempre significa que terminas con una gran cantidad de código que se basa en polimorfismo, despacho dinámico y RTTI, todos los cuales son relativamente costosos y todos inhiben las optimizaciones que son posibles cuando no se utilizan.
James McNellis

Respuestas:

116

La decisión definitiva se encuentra en las preguntas frecuentes de Stroustrup . En resumen, no transmite ningún significado semántico. Tendrá un costo. Las plantillas son más útiles para los contenedores.

¿Por qué C ++ no tiene un objeto de clase universal?

  • No necesitamos uno: la programación genérica proporciona alternativas seguras de tipo estático en la mayoría de los casos. Otros casos se manejan usando herencia múltiple.

  • No existe una clase universal útil: un universal verdaderamente no tiene semántica propia.

  • Una clase "universal" fomenta el pensamiento descuidado sobre tipos e interfaces y conduce a una comprobación excesiva del tiempo de ejecución.

  • El uso de una clase base universal implica un costo: los objetos deben asignarse en montón para que sean polimórficos; eso implica costo de memoria y acceso. Los objetos de montón no admiten naturalmente la semántica de copia. Los objetos de montón no admiten el comportamiento de ámbito simple (lo que complica la gestión de recursos). Una clase base universal fomenta el uso de dynamic_cast y otras comprobaciones en tiempo de ejecución.

Capitán Jirafa
fuente
83
No necesitamos ningún / objeto de clase base / no necesitamos ningún / control de pensamiento ...;)
Piskvor dejó el edificio
3
@Piskvor - LOL ¡Quiero ver el video musical!
Byron Whitlock
10
Objects must be heap-allocated to be polymorphic- No creo que esta afirmación sea correcta en general. Definitivamente puede crear una instancia de una clase polimórfica en la pila y pasarla como un puntero a una de sus clases base, lo que produce un comportamiento polimórfico.
Byron
5
En Java, al intentar convertir una referencia a Fooen una referencia a Barcuando Barno se hereda Foo, se lanza de forma determinista una excepción. En C ++, intentar lanzar un puntero a Foo en un puntero a Bar podría enviar un robot atrás en el tiempo para matar a Sarah Connor.
supercat
3
@supercat Es por eso que usamos dynamic_cast <>. Para evitar SkyNet y los sofás Chesterfield que aparecen misteriosamente.
Captain Giraffe
44

Primero pensemos en por qué querrías tener una clase base en primer lugar. Puedo pensar en algunas razones diferentes:

  1. Para soportar operaciones o colecciones genéricas que funcionarán en objetos de cualquier tipo.
  2. Incluir varios procedimientos que son comunes a todos los objetos (como la gestión de la memoria).
  3. Todo es un objeto (¡sin primitivas!). Algunos lenguajes (como Objective-C) no tienen esto, lo que complica bastante las cosas.

Estas son las dos buenas razones por las que los lenguajes de las marcas Smalltalk, Ruby y Objective-C tienen clases base (técnicamente, Objective-C no tiene realmente una clase base, pero para todos los efectos, la tiene).

Para el n. ° 1, la necesidad de una clase base que unifique todos los objetos bajo una sola interfaz se evita con la inclusión de plantillas en C ++. Por ejemplo:

void somethingGeneric(Base);

Derived object;
somethingGeneric(object);

es innecesario, cuando se puede mantener la integridad del tipo por completo mediante polimorfismo paramétrico.

template <class T>
void somethingGeneric(T);

Derived object;
somethingGeneric(object);

Para el n. ° 2, mientras que en Objective-C, los procedimientos de administración de memoria son parte de la implementación de una clase y se heredan de la clase base, la administración de memoria en C ++ se realiza utilizando composición en lugar de herencia. Por ejemplo, puede definir un contenedor de puntero inteligente que realizará el recuento de referencias en objetos de cualquier tipo:

template <class T>
struct refcounted
{
  refcounted(T* object) : _object(object), _count(0) {}

  T* operator->() { return _object; }
  operator T*() { return _object; }

  void retain() { ++_count; }

  void release()
  {
    if (--_count == 0) { delete _object; }
  }

  private:
    T* _object;
    int _count;
};

Luego, en lugar de llamar a métodos en el objeto en sí, estaría llamando a métodos en su contenedor. Esto no solo permite una programación más genérica: también le permite separar preocupaciones (ya que, idealmente, su objeto debería estar más preocupado por lo que debería hacer que por cómo debería gestionarse su memoria en diferentes situaciones).

Por último, en un lenguaje que tiene tanto primitivas como objetos reales como C ++, se pierden los beneficios de tener una clase base (una interfaz consistente para cada valor), ya que entonces tienes ciertos valores que no pueden ajustarse a esa interfaz. Para usar primitivas en ese tipo de situación, debe convertirlas en objetos (si su compilador no lo hace automáticamente). Esto crea muchas complicaciones.

Entonces, la respuesta corta a su pregunta: C ++ no tiene una clase base porque, al tener polimorfismo paramétrico a través de plantillas, no es necesario.

Jonathan Sterling
fuente
¡Está bien! Me alegro de que le haya resultado útil :)
Jonathan Sterling
2
Para el punto 3, controlo con C #. C # tiene primitivas que se pueden encajar en object( System.Object), pero no es necesario. Para el compilador, inty System.Int32son alias y pueden usarse indistintamente; el tiempo de ejecución maneja el boxeo cuando es necesario.
Cole Johnson
En C ++, no hay necesidad de boxear. Con las plantillas, el compilador genera el código correcto en tiempo de compilación.
Rob K
2
Tenga en cuenta que si bien esta respuesta (antigua) demuestra un puntero inteligente contado de referencia, ahora C ++ 11 tiene el std::shared_ptrque debería usarse en su lugar.
15

El paradigma dominante para las variables de C ++ es pasar por valor, no pasar por referencia. Forzar que todo se derive de una raíz Objectharía que pasarlos por valor fuera un error ipse facto.

(Porque aceptar un Objeto por valor como parámetro, por definición lo cortaría y quitaría su alma).

Esto no es bienvenido. C ++ te hace pensar si querías valor o semántica de referencia, dándote la opción. Esto es muy importante en la informática de rendimiento.

sehe
fuente
1
No sería un error por sí solo. Las jerarquías de tipos ya existen y utilizan el paso por valor bastante bien cuando es apropiado. Sin embargo, alentaría más estrategias basadas en montones donde la transferencia por referencia es más apropiada.
Dennis Zickefoose
En mi humilde opinión, un buen lenguaje debe tener distintos tipos de herencia para las situaciones en las que una clase derivada puede usarse legítimamente como un objeto de clase base usando segmento por referencia, aquellos en los que puede convertirse en uno usando segmento por valor, y aquellos donde ninguno es legítimo. El valor de tener un tipo de base común para las cosas que se pueden dividir sería limitado, pero muchas cosas no se pueden dividir y deben almacenarse indirectamente; el costo adicional de que tales cosas se deriven de un común Objectsería relativamente pequeño.
supercat
6

¡El problema es que existe un tipo de este tipo en C ++! Lo es void. :-) Cualquier puntero se puede convertir implícitamente de forma segura void *, incluidos los punteros a tipos básicos, clases sin tabla virtual y clases con tabla virtual.

Dado que debería ser compatible con todas esas categorías de objetos, por voidsí mismo no puede contener métodos virtuales. Sin funciones virtuales y RTTI, no se puede obtener información útil sobre el tipo void(coincide con CADA tipo, por lo que solo puede decir cosas que son verdaderas para CADA tipo), pero las funciones virtuales y RTTI harían que los tipos simples fueran muy ineficaces y evitarían que C ++ se un lenguaje adecuado para programación de bajo nivel con acceso directo a memoria, etc.

Entonces, existe ese tipo. Simplemente proporciona una interfaz muy minimalista (de hecho, vacía) debido a la naturaleza de bajo nivel del lenguaje. :-)

Ellioh
fuente
Los tipos de puntero y de objeto no son los mismos. No puede tener un objeto de tipo void.
Kevin Panko
1
De hecho, así es como suele funcionar la herencia en C ++. Una de las cosas más importantes que hace es la compatibilidad de los tipos de puntero y referencia: cuando se requiere un puntero a una clase, se puede dar un puntero a una clase derivada. Y la capacidad de lanzar punteros estáticos entre dos tipos es una señal de que están conectados en una jerarquía. Desde este punto de vista, void definitivamente es una clase base universal en C ++.
Ellioh
Si declara una variable de tipo void, no se compilará y obtendrá este error . En Java, podría tener una variable de tipo Objecty funcionaría. Esa es la diferencia entre voidun tipo "real". Quizás sea cierto que voides el tipo base para todo, pero como no proporciona ningún constructor, método o campo, no hay forma de saber si está ahí o no. Esta afirmación no se puede probar ni refutar.
Kevin Panko
1
En Java, cada variable es una referencia. Esa es la diferencia. :-) (Por cierto, en C ++ también hay clases abstractas que no puede usar para declarar una variable). Y en mi respuesta expliqué por qué no es posible obtener RTTI del vacío. Entonces, teóricamente vacío todavía es una clase base para todo. static_cast es una prueba, ya que realiza conversiones solo entre tipos relacionados y se puede usar para void. Pero tiene razón, la ausencia de acceso RTTI en vacío realmente lo hace muy diferente de los tipos base en los idiomas de nivel superior.
Ellioh
1
@Ellioh Después de consultar el estándar C ++ 11 §3.9.1p9, reconozco que voides un tipo (y descubrí un uso? : algo inteligente de ), pero todavía está muy lejos de ser una clase base universal. Por un lado, es "un tipo incompleto que no se puede completar", y por otro, no hay void&tipo.
bcrist
-2

C ++ es un lenguaje fuertemente tipado. Sin embargo, es sorprendente que no tenga un tipo de objeto universal en el contexto de la especialización de plantillas.

Tomemos, por ejemplo, el patrón

template <class T> class Hook;
template <class ReturnType, class ... ArgTypes>
class Hook<ReturnType (ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

que se puede instanciar como

Hook<decltype(some_function)> ...;

Ahora supongamos que queremos lo mismo para una función en particular. Me gusta

template <auto fallback> class Hook;
template <auto fallback, class ReturnType, class ... ArgTypes>
class Hook<ReturnType fallback(ArgTypes...)>
{
   ...
   ReturnType operator () (ArgTypes... args) { ... }
};

con la instanciación especializada

Hook<some_function> ...

Pero, por desgracia, aunque la clase T puede representar cualquier tipo (clase o no) antes de la especialización, no hay equivalente auto fallback(estoy usando esa sintaxis como el no tipo genérico más obvio en este contexto) que pueda reemplazar a cualquier argumento de plantilla sin tipo antes de la especialización.

Entonces, en general, este patrón no se transfiere de argumentos de plantilla de tipo a argumentos de plantilla que no son de tipo.

Al igual que con muchos rincones en el lenguaje C ++, la respuesta probablemente sea "ningún miembro del comité pensó en eso".

usuario7339377
fuente
Incluso si (algo como) tu ReturnType fallback(ArgTypes...)funciona, sería un mal diseño. template <class T, auto fallback> class Hook; template <class ReturnType, class ... ArgTypes> class Hook<ReturnType (ArgTypes...), ReturnType(*fallback)(ArgTypes...)> ...hace lo que quiere y significa que los parámetros de la plantilla Hookson de tipos
Caleth
-4

C ++ se llamó inicialmente "C con clases". Es una progresión del lenguaje C, a diferencia de otras cosas más modernas como C #. Y no se puede ver C ++ como un lenguaje, sino como una base de lenguajes (Sí, me acuerdo del libro de Scott Meyers Effective C ++).

C en sí mismo es una mezcla de lenguajes, el lenguaje de programación C y su preprocesador.

C ++ agrega otra combinación:

  • el enfoque de clase / objetos

  • plantillas

  • el STL

Personalmente, no me gustan algunas cosas que vienen directamente de C a C ++. Un ejemplo es la función de enumeración. La forma en que C # permite que el desarrollador lo use es mucho mejor: limita la enumeración en su propio alcance, tiene una propiedad Count y es fácilmente iterable.

Como C ++ quería ser retrocompatible con C, el diseñador fue muy permisivo al permitir que el lenguaje C ingresara en su totalidad a C ++ (hay algunas diferencias sutiles, pero no recuerdo nada que pudieras hacer usando un compilador de C que no podría hacer usando un compilador de C ++).

sergiol
fuente
"No puedo ver C ++ como un lenguaje, sino como la base de los lenguajes" ... ¿te importaría explicar e ilustrar a qué se dirige esa extraña declaración? Múltiples paradigmas es distinto de "múltiples lenguajes", aunque es justo decir que el preprocesador no está unido al resto de los pasos del compilador - buen punto. Re enums: se pueden incluir trivialmente en un ámbito de clase si se desea, y todo el lenguaje carece de introspección, un problema importante, aunque en este caso se puede aproximar, consulte el código benum de Boost Vault. ¿Su punto es solo Q: C ++ no comenzó con una base universal?
Tony Delroy
@Tony: Curiosamente, el libro al que me referí, lo único que ni siquiera mencionan como otro idioma es el preprocesador, que es el único que consideras por separado. Entiendo su punto de vista, ya que el preprocesador analiza primero su propia entrada. Pero tener un mecanismo de creación de plantillas es como tener otro idioma que hace la generalización por usted. Aplica una sintaxis de plantilla y tendrá el compilador que genera funciones para cada tipo que tenga. Cuando puede tener un programa escrito en C y dar su entrada a un compilador de C ++, se trata de dos lenguajes en uno: C y C con objetos => C ++
sergiol
STL, puede verse como un complemento, pero tiene clases y contenedores muy prácticos que a través de plantillas amplían NATIVAMENTE la potencia de C ++. Y las enumeraciones que estaba diciendo son las enumeraciones NATIVAS. Cuando habla de Boost, confía en un código de terceros que no es NATIVO para el idioma. En C #, no tengo que incluir nada externo para tener una enumeración iterable y de ámbito propio. En C ++, un truco que uso a menudo es llamar al último elemento de una enumeración, sin valores asignados, para que se inicien automáticamente desde cero y se incrementen, algo como NUM_ITEMS, y esto me da el límite superior.
sergiol
2
gracias por su explicación adicional - al menos puedo ver de dónde viene; He escuchado a algunas personas quejarse de que aprender a usar plantillas se siente desarticulado de otra programación de C ++, aunque ciertamente esa no es mi experiencia. Dejaré que otros lectores hagan lo que quieran con las otras perspectivas que presenta. Salud.
Tony Delroy
1
Creo que el mayor problema de tener un tipo de base universal es que ese tipo sería inútil si no tuviera ningún método virtual; C ++ no quería agregar ninguna sobrecarga para los tipos de estructuras que no tienen ningún método virtual, pero cada objeto de cualquier tipo que tenga algún método virtual debe tener, como mínimo, un puntero a una tabla de envío. Si las clases y estructuras hubieran habitado universos diferentes, podría haber sido posible tener un objeto de clase universal, pero las clases y estructuras son básicamente lo mismo en C ++.
supercat