¿Por qué se desalienta la base para todos los objetos en C ++?

76

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?

Matthew James Briggs
fuente
16
porque C ++ no es Java ... Y no deberías intentar forzarlo a serlo.
AK_
10
Preguntado sobre el desbordamiento de pila: ¿por qué no hay una clase base en C ++?
26
Además, no estoy de acuerdo con los votos cerrados para "principalmente basado en la opinión". Hay razones muy específicas que se pueden explicar para esto, ya que las respuestas dan fe tanto de esta pregunta como de la pregunta SO vinculada.
2
Es el principio de "no lo vas a necesitar" de ágil. A menos que ya haya identificado una necesidad particular, no lo haga (hasta que lo haga).
Jool
3
@AK_: Hay un "tan estúpido como" que falta en tu comentario.
DeadMG

Respuestas:

75

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, usar std::vectorlistas 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 QObjectclase 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).

monstruo de trinquete
fuente
77
Olvidó mencionar probablemente la razón principal por la que Java tiene una clase base: antes de los genéricos, las clases de colección necesitaban la clase base para funcionar. Se tipeó todo (almacenamiento interno, parámetros, valores de retorno) Object.
Aleksandr Dubinsky
1
@AleksandrDubinsky: Y los genéricos solo agregaron azúcar sintáctico, sin cambiar realmente nada más que el esmalte.
Deduplicador
44
Yo diría que el código hash, la igualdad y el soporte de monitor también son errores de diseño en Java. ¿Quién pensó que era una buena idea hacer que todos los objetos fueran un candado?
usr
1
Sí, pero nadie lo quiere. Cuándo fue la última vez que necesitó bloquear un objeto y no pudo crear una instancia de un objeto de bloqueo separado para hacerlo. Es muy raro y pone una carga sobre todo. Los chicos de Java tenían una mala comprensión de la seguridad de los hilos en ese entonces como evidencia de que todos los objetos son un bloqueo y de las colecciones de seguridad de hilos que ahora están en desuso. La seguridad de subprocesos es una propiedad global, no una por objeto.
Usr
2
" hashCode solo es útil si desea almacenarlo en una colección basada en hash (la preferencia en C ++ es para std :: vector y listas sin ordenar). " El argumento en contra real _hashCodeno es 'usar un contenedor diferente' sino señalar que C ++ std::unordered_maphace 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.
underscore_d
100

Porque no hay funciones compartidas por todos los objetos. No hay nada que poner en esta interfaz que tenga sentido para todas las clases.

DeadMG
fuente
10
+1 por simplicidad de la respuesta, esta es realmente la única razón.
BWG
77
En el gran marco con el que tengo experiencia, la clase base común proporciona la infraestructura de serialización y reflexión que se desea en el contexto <whatever>. Meh Simplemente resultó en que las personas serializaran un montón de datos cruzados junto con los datos y metadatos e hizo que el formato de datos fuera demasiado grande y complejo para ser eficiente.
dmckee
19
@dmckee: También diría que la serialización y la reflexión no son necesidades universalmente útiles.
DeadMG
16
@DeadMG: "PERO ¿QUÉ PASA SI NECESITA AHORRAR TODO?"
deworde
8
No sé, lo pones entre comillas, usas mayúsculas y la gente no puede ver el chiste. @MSalters: Bueno, eso debería ser fácil, tiene una cantidad mínima de estado, solo especifica que está allí. Puedo escribir mi nombre en una lista sin ingresar un ciclo recursivo.
deworde
25

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.

Mike Nakis
fuente
66
Cuando la clase base (en Java "java.lang.Object") no contiene ningún método que llame a otros métodos, el problema de la clase base frágil no puede ocurrir.
Martin Rosenau
3
¡Una clase base poderosa y útil que sería!
Mike Nakis
99
@MartinRosenau ... ¡como puedes hacer en C ++ sin necesidad de una clase base maestra!
gbjbaanb
55
@ Davor®dralo Entonces C ++ tiene un nombre estúpido para una función básica ("operador <<" en lugar de algo sensato como "DebugPrint"), mientras que Java tiene un fenómeno de una clase base para absolutamente todas las clases que escribes, sin excepciones. Creo que me gusta más la verruga de C ++.
Sebastian Redl
44
@ Davor®dralo: El nombre de la función es irrelevante. Imagen de una sintaxis cout.print(x).print(0.5).print("Bye\n"), no depende operator<<.
MSalters
24

Porque:

  1. No debe pagar por lo que no usa.
  2. Estas funciones tienen menos sentido en un sistema de tipos basado en valores que en un sistema de tipos basado en referencias.

La implementación de cualquier tipo de virtualfunció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 toStringno 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 equalso hashCodesolo 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, un intrealmente no debería tener una identidad distinta de su valor ... dos enteros del mismo valor deberían ser iguales)

Mehrdad
fuente
En relación con este problema y el problema de la clase base frágil señalado por Mike Nakis, tenga en cuenta la interesante investigación / propuesta de solucionarlo en Java básicamente haciendo que todos los métodos internos (es decir, cuando se los llama desde la misma clase) no sean virtuales, pero manteniendo su comportamiento virtual cuando llamado externamente; Para obtener un comportamiento antiguo / estándar (es decir, virtual en todas partes), la propuesta introdujo una nueva openpalabra clave. Sin embargo, no creo que haya ido más allá de unos pocos documentos.
Fizz
Puede encontrar un poco más de discusión sobre ese documento en lambda-the-ultimate.org/classic/message12271.html
Fizz
Tener una clase base común haría posible probar cualquiera shared_ptr<Foo> para ver si también es un shared_ptr<Bar>(o lo mismo con otros tipos de puntero), incluso si Fooy Barson 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.
supercat
Si bien puede no ser útil tener una clase base común para todo, creo que hay algunas categorías de objetos bastante grandes para las que las clases base comunes serían útiles. Por ejemplo, muchas clases (una pluralidad sustancial, si no una mayoría) en Java se pueden usar de dos maneras: como un titular no compartido de datos mutables, o como un titular de datos compartible que nadie puede modificar. Con ambos patrones de uso, se utiliza un puntero administrado (referencia) como proxy para los datos subyacentes. Es útil poder tener un tipo de puntero administrado común para todos esos datos.
supercat
16

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 similar boost::anypuede hacerlo sin una clase raíz común. Y boost::anytambié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:

struct serialization_friendly {
  virtual void write_to( my_buffer* ) const = 0;
  virtual void read_from( my_buffer const* ) = 0;
  virtual ~serialization_friendly() {}
};

Ahora cada bit de código que escribes puede ser serialization_friendly.

void serialize( my_buffer* b, serialization_friendly const* x ) {
  if (x) x->write_to(b);
}

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 a tuple, o a into a double, o a std::ptrdiff_t.

Tomamos otro enfoque:

void write_to( my_buffer* b, int x ) {
  b->write_integer(x);
}    
template<class T,
  class=std::enable_if_t< void_t<
    std::declval<T const*>()->write_to( std::declval<my_buffer*>()
  > >
>
void write_to( my_buffer* b, T const* x ) {
  if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
  write_to( b, t );
}

que consiste en, bueno, no hacer nada, aparentemente. Excepto que ahora podemos extender write_toanulando write_tocomo 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:

namespace details {
  struct can_serialize_pimpl {
    virtual void write_to( my_buffer* ) const = 0;
    virtual void read_from( my_buffer const* ) = 0;
    virtual ~can_serialize_pimpl() {}
  };
}
struct can_serialize {
  void write_to( my_buffer* b ) const { pImpl->write_to(b); }
  void read_from( my_buffer const* b ) { pImpl->read_from(b); }
  std::unique_ptr<details::can_serialize_pimpl> pImpl;
  template<class T> can_serialize(T&&);
};
namespace details { 
  template<class T>
  struct can_serialize : can_serialize_pimpl {
    std::decay_t<T>* t;
    void write_to( my_buffer*b ) const final override {
      serialize( b, std::forward<T>(*t) );
    }
    void read_from( my_buffer const* ) final override {
      deserialize( b, std::forward<T>(*t) );
    }
    can_serialize(T&& in):t(&in) {}
  };
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
  std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}

y ahora podemos tomar un tipo arbitrario y colocarlo automáticamente en una can_serializeinterfaz que le permite invocar serializeen un punto posterior a través de una interfaz virtual.

Entonces:

void writer_thingy( can_serialize s );

es una función que toma cualquier cosa que pueda serializar, en lugar de

void writer_thingy( serialization_friendly const* s );

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 anulando write_to( my_buffer*, std::vector<T> const& ): con esa sobrecarga, se puede pasar a can_serializeay la serialización de los std::vectorarchivos 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.

Yakk
fuente
8

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.

Jules
fuente
1
Una solución al problema del diamante es hacer que todos los tipos que no derivan virtualmente un tipo base a través de múltiples rutas anulen a todos los miembros virtuales de ese tipo base; Si un tipo base común se hubiera incorporado al lenguaje desde el principio, un compilador podría generar automáticamente implementaciones predeterminadas legítimas (aunque no necesariamente impresionantes).
supercat
5

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 la std::vector<CObject>actual. Debido a que CObjectera la clase base de casi todas las clases (desafortunadamente no del CStringequivalente de stringlos 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.

Martin Rosenau
fuente
3
¿Eso es MFC? [relleno de comentarios]
user253751
3
De hecho, es MFC. Una luz brillante del diseño OO que mostró al mundo cómo se deben hacer las cosas. Oh, espera ...
gbjbaanb
44
@gbjbaanb Turbo Pascal y Turbo C ++ tenían los suyos TObjectincluso 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.
hvd
Incluso antes de las plantillas, intentar escribir Smalltalk en C ++ produjo resultados terribles.
JDługosz
@hvd Aún así, MFC fue un ejemplo mucho peor de diseño orientado a objetos que cualquier cosa que Borland haya producido.
Jules
5

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::vectoraquí ya que no se implementa IObject; será mejor que cree un nuevo derivado IVectorObjectque 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 Integery intconfusió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).

template<typename T>
struct ToStringTrait;

template<typename T> 
std::string toString(const T & t) {
  return ToStringTrait<T>::toString(t);
}

template<>
struct ToStringTrait<int> {
  std::string toString(int v) {
    return itoa(v);
  }
}

template<typename T>
struct ToStringTrait<std::vector<T>> {
  std::string toString(const std::vector<T> &v) {
    std::stringstream ss;
    ss<<"{";
    for(int i=0; i<v.size(); ++i) {
      ss<<toString(v[i]);
    }
    ss<<"}";
    return ss.str();
  }
}
Michael Anderson
fuente
3

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. Puedes static_castvolver a la clase original.

Sin embargo, lo que no puede hacer con voidlo que puede hacer Objectes 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.

pjc50
fuente
1
Solo subobjetos de clase base de ancho cero, no los normales.
Deduplicador
@Deduplicator A modo de actualización, C ++ 17 agrega [[no_unique_address]], que los compiladores pueden usar para dar ancho cero a los subobjetos de los miembros.
underscore_d
1
@underscore_d Te refieres a planeado para C ++ 20, [[no_unique_address]]permitirá que el compilador a las variables miembro de EBO.
Deduplicador
@Dupuplicator Whoops, sí. Ya comencé a usar C ++ 17, ¡pero creo que sigo pensando que es más avanzado de lo que realmente es!
underscore_d
2

Java toma la filosofía de diseño de que el comportamiento indefinido no debería existir . Código como:

Cat felix = GetCat();
Woofer Rover = (Woofer)felix;
Rover.woof();

probará si felixtiene un subtipo de Catesa interfaz de implementos Woofer; si lo hace, realizará el lanzamiento y la invocación woof()y, si no lo hace, arrojará una excepción. El comportamiento del código está completamente definido si se feliximplementa Woofero 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 *Cata 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 uno

Super gato
fuente
Ese reparto falla en tiempo de compilación en C ++ , Java y C # .
milleniumbug
1
@milleniumbug: Si Wooferes una interfaz y Cates heredable, el elenco sería legítimo porque podría existir (si no ahora, posiblemente en el futuro) una WoofingCatque herede Cate implemente Woofer. Tenga en cuenta que, según el modelo de compilación / vinculación de Java, la creación de a WoofingCatno requeriría acceso al código fuente para Catni Woofer.
supercat
3
C ++ tiene dynamic_cast , que maneja adecuadamente el intento de convertir de a Cata 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.
Rob K
2
@RobK: Tienes razón sobre la sintaxis, por supuesto; mea culpa. He estado leyendo un poco más sobre dynamic_cast y parece que, en cierto sentido, C ++ moderno tiene todos los objetos polimórficos derivados de una clase base de "objeto polimórfico" base con cualquier campo (s) necesario para identificar el tipo de objeto (típicamente una tabla virtual puntero, aunque eso es un detalle de implementación). C ++ no describe las clases polimórficas de esa manera, pero pasar un puntero a dynamic_casttendrá 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 ...
supercat
2
... todos los objetos polimórficos almacenan cierta información con el mismo diseño, y todos admiten un comportamiento que no es compatible con objetos no polimórficos; En mi opinión, eso significa que se comportan como si derivaran de una base común, ya sea que la definición del lenguaje utilice dicha terminología o no.
supercat
1

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.

James
fuente