Por que a base para todos os objetos é desencorajada em C ++

76

Stroustrup diz "Não invente imediatamente uma base exclusiva para todas as suas classes (uma classe Object). Normalmente, você pode fazer melhor sem ela na maioria das classes". (A linguagem de programação C ++, quarta edição, seção 1.3.4)

Por que uma classe base para tudo geralmente é uma má idéia e quando faz sentido criar uma?

Matthew James Briggs
fonte
16
porque C ++ não é Java ... E você não deve tentar forçá-lo a ser.
AK_ 16/02/2015
10
Perguntado no Stack Overflow: Por que não há classe base em C ++?
26
Além disso, eu discordo dos votos próximos de "principalmente com base em opiniões". Existem razões muito específicas que podem ser explicadas para isso, pois as respostas atestam tanto a questão quanto a questão SO vinculada.
2
É o princípio "você não vai precisar" de agilidade. A menos que você já tenha identificado uma necessidade específica, não a faça (até que você o faça).
Jool
3
@AK_: Há um comentário "tão estúpido quanto" no seu comentário.
DeadMG

Respostas:

75

Porque o que esse objeto teria para funcionalidade? Em java, toda a classe Base possui um toString, um hashCode & igualdade e uma variável monitor + condition.

  • ToString é útil apenas para depuração.

  • O hashCode é útil apenas se você desejar armazená-lo em uma coleção baseada em hash (a preferência em C ++ é passar uma função de hash para o contêiner como parâmetro do modelo ou evitar std::unordered_*completamente e, em vez disso, use std::vectorlistas simples e não ordenadas).

  • a igualdade sem um objeto base pode ser ajudada em tempo de compilação; se eles não tiverem o mesmo tipo, não poderão ser iguais. Em C ++, esse é um erro de tempo de compilação.

  • a variável de monitor e condição é melhor incluída explicitamente caso a caso.

No entanto, quando há mais coisas a fazer, há um caso de uso.

Por exemplo, no QT, há a QObjectclasse raiz que forma a base da afinidade do encadeamento, hierarquia de propriedade pai-filho e mecanismo de slots de sinal. Também força o uso do ponteiro para QObjects. No entanto, muitas classes no Qt não herdam o QObject porque não precisam do slot de sinal (particularmente os tipos de valor de alguma descrição).

catraca arrepiante
fonte
7
Você esqueceu de mencionar provavelmente o principal motivo pelo qual o Java tem uma classe base: antes dos genéricos, as classes de coleção precisavam da classe base para funcionar. Tudo (armazenamento interno, parâmetros, valores de retorno) foi digitado Object.
Aleksandr Dubinsky
1
@AleksandrDubinsky: E os genéricos apenas adicionavam açúcar sintático, não mudando nada, mas o polonês.
Deduplicator
4
Eu diria que código de hash, igualdade e suporte ao monitor também são erros de design em Java. Quem pensou que era uma boa idéia fazer com que todos os objetos fossem bloqueados ?!
usr
1
Sim, mas ninguém quer. Quando foi a última vez que você precisou bloquear um objeto e não pôde instanciar um objeto de bloqueio separado para fazer isso. É muito raro e sobrecarrega tudo. Os caras do Java tinham um entendimento ruim da segurança do encadeamento na época, como evidência de todos os objetos serem um bloqueio e das coleções seguras do encadeamento que agora estão obsoletas. A segurança do encadeamento é uma propriedade global, não por objeto.
usr
2
" HashCode só é útil se você quiser armazená-lo em uma coleção com base em hash (a preferência em C ++ é para std :: vector e listas simples não ordenadas). " O verdadeiro contra-argumento para _hashCodenão é 'usar um recipiente diferente', mas sim apontar O C ++ std::unordered_mapfaz hash usando um argumento de modelo, em vez de exigir que a própria classe de elemento forneça a implementação. Ou seja, como todos os outros bons contêineres e gerenciadores de recursos em C ++, não é intrusivo; não polui todos os objetos com funções ou dados , caso alguém precise deles em algum contexto posteriormente.
underscore_d
100

Porque não há funções compartilhadas por todos os objetos. Não há nada para colocar nessa interface que faça sentido para todas as classes.

DeadMG
fonte
10
+1 pela simplicidade da resposta, esse é realmente o único motivo.
BWG 15/02
7
Na grande estrutura com a qual tenho experiência, a classe base comum fornece a infra-estrutura de serialização e reflexão desejada no <whatever> contexto. Meh. Isso resultou em pessoas serializando um monte de lixo junto com os dados e metadados e tornou o formato de dados muito grande e complexo para ser eficiente.
dmckee
19
@dmckee: Eu também argumentaria que serialização e reflexão não são necessidades universalmente úteis.
23415 DeadMG
16
@DeadMG: "MAS E SE VOCÊ PRECISAR SALVAR TUDO?"
deworde
8
Não sei, você coloca aspas, usa letras maiúsculas e as pessoas não conseguem ver a piada. @ MSalters: Bem, isso deve ser fácil, tem uma quantidade mínima de estado, basta especificar que está lá. Eu posso escrever meu nome em uma lista sem inserir um loop recursivo.
deworde 16/02
25

Sempre que você cria hierarquias altas de objetos de herança, você tende a encontrar o problema da Classe Base Frágil (Wikipedia.) .

Ter muitas pequenas hierarquias de herança separadas (distintas, isoladas) reduz as chances de encontrar esse problema.

Tornar todos os seus objetos parte de uma única hierarquia de herança enorme garante praticamente que você encontrará esse problema.

Mike Nakis
fonte
6
Quando a classe base (em Java "java.lang.Object") não contém métodos que chamam outros métodos, o problema da Classe Base Frágil não pode ocorrer.
Martin Rosenau
3
Uma classe base poderosa e útil que seria!
Mike Nakis
9
@MartinRosenau ... como você pode fazer em C ++ sem precisar de uma classe básica!
Gbjbaanb
5
@ DavorŽdralo Então, o C ++ tem um nome estúpido para uma função básica ("operator <<" em vez de algo sensível como "DebugPrint"), enquanto o Java tem uma aberração de uma classe base para absolutamente todas as classes que você escreve, sem exceções. Eu acho que gosto mais da verruga do C ++.
Sebastian Redl
4
@ DavorŽdralo: O nome da função é irrelevante. Imagem de uma sintaxe cout.print(x).print(0.5).print("Bye\n")- não depende operator<<.
MSalters
24

Porque:

  1. Você não deve pagar pelo que não usa.
  2. Essas funções fazem menos sentido em um sistema de tipos com base em valor do que em um sistema de tipos com base em referência.

A implementação de qualquer tipo de virtualfunção introduz uma tabela virtual, que requer sobrecarga de espaço por objeto que não é necessária nem desejada em muitas situações (a maioria?).

A implementação toStringnão virtual seria muito inútil, porque a única coisa que ele poderia retornar é o endereço do objeto, que é muito hostil ao usuário e ao qual o chamador já tem acesso, ao contrário do Java.
Da mesma forma, um não virtual equalsou hashCodepode usar apenas endereços para comparar objetos, o que é novamente bastante inútil e muitas vezes totalmente errado - ao contrário do Java, os objetos são copiados com frequência em C ++ e, portanto, distinguir a "identidade" de um objeto não é sempre significativo ou útil. (por exemplo, um intrealmente não deve ter uma identidade diferente de seu valor ... dois números inteiros do mesmo valor devem ser iguais.)

Mehrdad
fonte
Em relação a esse problema e ao frágil problema de classe base observado por Mike Nakis, observe uma interessante pesquisa / proposta de corrigi-lo em Java basicamente tornando todos os métodos internamente (ou seja, quando chamados da mesma classe) não virtuais, mas mantendo seu comportamento virtual quando chamado externamente; para obter um comportamento antigo / padrão (ou seja, virtual em qualquer lugar), a proposta introduziu uma nova openpalavra-chave. Eu não acho que foi além de alguns papéis.
Fizz
Um pouco mais de discussão sobre esse artigo pode ser encontrado em lambda-the-ultimate.org/classic/message12271.html
Fizz
Ter um classe base comum tornaria possível testar qualquer shared_ptr<Foo> para ver se ele também é um shared_ptr<Bar>(ou mesmo com outros tipos de ponteiro), mesmo se Fooe Barsão classes não relacionadas, que não sabem nada sobre o outro. Exigir que tal coisa funcione com "ponteiros brutos", dado o histórico de como essas coisas são usadas, seria caro, mas para coisas que serão armazenadas em heap de qualquer maneira, o custo adicional seria mínimo.
Supercat
Embora possa não ser útil ter uma classe base comum para tudo, acho que existem algumas categorias de objetos bastante grandes para as quais as classes base comuns seriam úteis. Por exemplo, muitas classes (uma pluralidade substancial, se não a maioria) em Java podem ser usadas de duas maneiras: como um detentor não compartilhado de dados mutáveis ​​ou como um detentor compartilhável de dados que ninguém pode modificar. Nos dois padrões de uso, um ponteiro gerenciado (referência) é usado como proxy para os dados subjacentes. Ser capaz de ter um tipo de ponteiro gerenciado comum para todos esses dados é útil.
Supercat
16

Ter um objeto raiz limita o que você pode fazer e o que o compilador pode fazer, sem muito retorno.

Uma classe raiz comum torna possível criar contêineres de qualquer coisa e extrair o que eles são com a dynamic_cast, mas se você precisar de contêineres de qualquer coisa, algo semelhante boost::anypode ser feito sem uma classe raiz comum. E boost::anytambém suporta primitivos - ele pode até suportar a otimização de pequenos buffers e deixá-los quase "sem caixa" na linguagem Java.

O C ++ suporta e prospera em tipos de valor. Literais e tipos de valores escritos pelo programador. Os contêineres C ++ armazenam, classificam, hash, consomem e produzem tipos de valor com eficiência.

A herança, especialmente o tipo de herança base do estilo Java de herança monolítica, requer tipos de "ponteiro" ou "referência" baseados em armazenamento livre. Seu identificador / ponteiro / referência aos dados contém um ponteiro para a interface da classe e, polimorficamente, pode representar outra coisa.

Embora isso seja útil em algumas situações, depois de se casar com o padrão com uma "classe base comum", você bloqueia toda a sua base de códigos no custo e na bagagem desse padrão, mesmo quando não é útil.

Quase sempre, você sabe mais sobre um tipo do que "ele é um objeto" no site de chamada ou no código que o utiliza.

Se a função for simples, escrever a função como modelo fornece um polimorfismo baseado em tempo de compilação do tipo pato, onde as informações no site de chamada não são descartadas. Se a função for mais complexa, o apagamento do tipo pode ser feito pelo qual as operações uniformes do tipo que você deseja executar (por exemplo, serialização e desserialização) podem ser construídas e armazenadas (em tempo de compilação) para serem consumidas (em tempo de execução) pelo código em uma unidade de tradução diferente.

Suponha que você tenha alguma biblioteca na qual deseja que tudo seja serializado. Uma abordagem é ter uma classe base:

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

Agora todo código que você escreve pode ser serialization_friendly.

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

Exceto não um std::vector, agora você precisa escrever todos os contêineres. E não os números inteiros que você obteve dessa biblioteca bignum. E não o tipo que você escreveu que achava que não precisava ser serializado. E não um tuple, ou um intou um double, ou um std::ptrdiff_t.

Adotamos outra abordagem:

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 em, aparentemente, não fazer nada. Exceto agora, podemos estender write_tosubstituindo write_tocomo uma função livre no espaço para nome de um tipo ou método no tipo.

Podemos até escrever um pouco de código de apagamento 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) );
) {}

e agora podemos pegar um tipo arbitrário e encaixotá-lo automaticamente em uma can_serializeinterface que permite invocar serializeposteriormente através de uma interface virtual.

Assim:

void writer_thingy( can_serialize s );

é uma função que aceita tudo o que pode ser serializado, em vez de

void writer_thingy( serialization_friendly const* s );

eo primeiro, ao contrário do segundo, ele pode manipular int, std::vector<std::vector<Bob>>automaticamente.

Não demorou muito para escrevê-lo, especialmente porque esse tipo de coisa é algo que você raramente quer fazer, mas adquirimos a capacidade de tratar qualquer coisa como serializável sem exigir um tipo de base.

Além disso, agora podemos tornar o std::vector<T>serializável como cidadão de primeira classe simplesmente substituindo write_to( my_buffer*, std::vector<T> const& )- com essa sobrecarga, ele pode ser passado para um can_serializee a serialização das informações std::vectoré armazenada em uma tabela e acessada por .write_to.

Em suma, o C ++ é poderoso o suficiente para que você possa implementar as vantagens de uma única classe base rapidamente quando necessário, sem ter que pagar o preço de uma hierarquia de herança forçada quando não for necessário. E os horários em que a base única (falsificada ou não) é necessária são razoavelmente raros.

Quando os tipos são na verdade sua identidade e você sabe o que são, as oportunidades de otimização são muitas. Os dados são armazenados localmente e de forma contígua (o que é altamente importante para a facilidade de cache nos processadores modernos), os compiladores podem entender facilmente o que uma determinada operação faz (em vez de ter um ponteiro de método virtual opaco que ele deve pular, levando a códigos desconhecidos no outro lado), que permite que as instruções sejam reordenadas de maneira ideal, e menos pinos redondos são martelados em orifícios redondos.

Yakk
fonte
8

Há muitas boas respostas acima, e o fato claro de que qualquer coisa que você faria com uma classe base de todos os objetos pode ser melhor realizada de outras maneiras, como mostra a resposta de @ ratchetfreak e os comentários sobre ela, é muito importante, mas existe outra razão, que é evitar criar diamantes de herançaquando herança múltipla é usada. Se você tivesse alguma funcionalidade em uma classe base universal, assim que começasse a usar herança múltipla, teria que começar a especificar qual variante dela você gostaria de acessar, porque poderia ser sobrecarregada de maneira diferente em caminhos diferentes da cadeia de herança. E a base não pode ser virtual, porque isso seria muito ineficiente (exigindo que todos os objetos tenham uma tabela virtual a um custo potencialmente enorme no uso de memória e localidade). Isso se tornaria um pesadelo logístico muito rapidamente.

Jules
fonte
1
Uma solução para o problema do diamante é ter todos os tipos que não derivam virtualmente um tipo de base por meio de vários caminhos substituindo todos os membros virtuais desse tipo de base; se um tipo de base comum tivesse sido incorporado à linguagem desde o início, um compilador poderia gerar automaticamente implementações padrão legítimas (embora não necessariamente impressionantes).
Supercat
5

De fato, os primeiros compiladores e bibliotecas C ++ da Microsofts (eu conheço o Visual C ++, 16 bits) tinham essa classe chamada CObject.

No entanto, você deve saber que naquela época os "modelos" não eram suportados por esse compilador C ++ simples, portanto, classes como std::vector<class T>não eram possíveis. Em vez disso, uma implementação de "vetor" poderia lidar apenas com um tipo de classe, portanto havia uma classe comparável à std::vector<CObject>atual. Como CObjectera a classe base de quase todas as classes (infelizmente não CString- equivalente a stringnos compiladores modernos), você poderia usar essa classe para armazenar quase todos os tipos de objetos.

Como os compiladores modernos suportam modelos, esse caso de uso de uma "classe base genérica" ​​não é mais fornecido.

Você deve pensar no fato de que o uso de uma classe base tão genérica custará (um pouco) memória e tempo de execução - por exemplo, na chamada para o construtor. Portanto, existem desvantagens ao usar essa classe, mas pelo menos ao usar os compiladores C ++ modernos, quase não há caso de uso para essa classe.

Martin Rosenau
fonte
3
Isso é MFC? [preenchimento de comentário]
user253751 15/02
3
É de fato MFC. Um farol brilhante do design de OO que mostrava ao mundo como as coisas deveriam ser feitas. Ah, espere ... #
1616 gbjbaanb
4
@gbjbaanb O Turbo Pascal e o Turbo C ++ possuíam o seu TObjectantes mesmo do MFC existir. Não culpe a Microsoft por essa parte do design, parecia uma boa idéia para praticamente todo mundo nessa época.
hvd
Mesmo antes dos modelos, tentar escrever Smalltalk em C ++ produzia resultados terríveis.
JDługosz 16/02
@hvd Ainda assim, o MFC foi um exemplo muito pior de design orientado a objetos do que qualquer coisa produzida pela Borland.
Jules
5

Vou sugerir outro motivo que vem do Java.

Porque você não pode criar uma classe base para tudo, pelo menos não sem um monte de placas de caldeira.

Você pode se dar bem com suas próprias classes - mas provavelmente descobrirá que acaba duplicando muito código. Por exemplo: "Não posso usar std::vectoraqui porque ele não implementa IObject- é melhor criar uma nova derivada IVectorObjectque faça a coisa certa ...".

Esse será o caso sempre que você estiver lidando com classes de biblioteca padrão ou internas ou de outras bibliotecas.

Agora, se ele foi construído para a língua que você iria acabar com coisas como o Integere intconfusão que está em java, ou uma grande mudança para a sintaxe da linguagem. (Lembre-se, acho que algumas outras línguas fizeram um bom trabalho ao incorporá-lo a todos os tipos - o ruby ​​parece ser um exemplo melhor.)

Observe também que, se sua classe base não for polimórfica em tempo de execução (ou seja, usar funções virtuais), você poderá obter o mesmo benefício do uso de características como framework.

por exemplo, em vez de .toString()você poderia ter o seguinte: (NOTA: eu sei que você pode fazer isso mais limpo usando bibliotecas existentes, etc., é apenas um exemplo 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
fonte
3

Indiscutivelmente "vazio" cumpre muitos dos papéis de uma classe base universal. Você pode converter qualquer ponteiro para a void*. Você pode comparar esses ponteiros. Você pode static_castvoltar para a classe original.

No entanto o que você não pode fazer com voidque você pode fazer com Objecté usar RTTI para descobrir que tipo de objeto que você realmente tem. Em última análise, isso se deve a como nem todos os objetos em C ++ têm RTTI e, na verdade, é possível ter objetos de largura zero.

pjc50
fonte
1
Apenas subobjetos de classe-base de largura zero, não os normais.
Deduplicator
@Duplicador Por meio de atualização, o C ++ 17 adiciona [[no_unique_address]], que pode ser usado pelos compiladores para dar largura zero aos subobjetos dos membros.
underscore_d
1
@underscore_d Você quer dizer planejado para C ++ 20, [[no_unique_address]]permitirá que o compilador faça o EBO de variáveis-membro.
Deduplicator
@Duplicator Whoops, sim. Eu já comecei a usar o C ++ 17, mas acho que ainda acho que é mais avançado do que realmente é!
Underscore_d
2

Java adota a filosofia de design de que o comportamento indefinido não deve existir . Código como:

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

testará se felixpossui um subtipo Catdessa interface de implementos Woofer; se o fizer, executará o elenco e a invocação woof()e, se não, lançará uma exceção. O comportamento do código é totalmente definido, feliximplementando Wooferou não .

O C ++ adota a filosofia de que, se um programa não deve tentar alguma operação, não importa o que o código gerado faria se essa operação fosse tentada e o computador não deveria perder tempo tentando restringir o comportamento nos casos que "deveriam" nunca surja. Em C ++, adicionando os operadores de indireção apropriados para converter a *Catem a *Woofer, o código produziria um comportamento definido quando a conversão for legítima, mas um comportamento indefinido quando não for .

Ter um tipo de base comum para as coisas torna possível validar as conversões entre derivadas desse tipo de base e também realizar operações try-cast, mas validar as conversões é mais caro do que simplesmente assumir que elas são legítimas e esperar que nada de ruim aconteça. A filosofia do C ++ seria que essa validação requer "pagar por algo que você [geralmente] não precisa".

Outro problema relacionado ao C ++, mas não seria um problema para uma nova linguagem, é que, se vários programadores criam uma base comum, derivam suas próprias classes disso e escrevem código para trabalhar com coisas dessa classe base comum, esse código não poderá trabalhar com objetos desenvolvidos por programadores que usaram uma classe base diferente. Se um novo idioma exigir que todos os objetos de heap tenham um formato de cabeçalho comum e nunca permita objetos de heap que não existiam, um método que exija uma referência a um objeto de heap com esse cabeçalho aceitará uma referência a qualquer objeto de heap que alguém poderia criar.

Pessoalmente, acho que ter um meio comum de perguntar a um objeto "você pode ser convertido para o tipo X" é um recurso muito importante em uma linguagem / estrutura, mas se esse recurso não estiver incorporado em uma linguagem desde o início, será difícil adicione depois. Pessoalmente, acho que essa classe base deve ser adicionada a uma biblioteca padrão na primeira oportunidade, com uma forte recomendação de que todos os objetos que serão usados ​​polimorficamente herdem dessa base. Fazer com que os programadores implementem seus próprios "tipos de base" tornaria mais difícil a passagem de objetos entre o código de pessoas diferentes, mas ter um tipo de base comum do qual muitos programadores herdaram tornaria mais fácil.

TERMO ADITIVO

Usando modelos, é possível definir um "detentor arbitrário de objeto" e perguntar sobre o tipo de objeto nele contido; o pacote Boost contém uma coisa chamada any. Portanto, mesmo que o C ++ não tenha um tipo padrão de "referência verificável por tipo a qualquer coisa", é possível criar um. Isso não resolve o problema acima mencionado de não ter algo no padrão da linguagem, isto é, incompatibilidade entre implementações de diferentes programadores, mas explica como o C ++ se dá sem ter um tipo base do qual tudo é derivado: possibilitando a criação de algo que age como um.

supercat
fonte
Essa conversão falha no momento da compilação em C ++ , Java e C # .
milleniumbug
1
@ milleniumbug: Se Wooferé uma interface e Caté herdável, o elenco seria legítimo porque poderia existir (se não agora, possivelmente no futuro) um WoofingCatque herda Cate implementa Woofer. Observe que, no modelo de compilação / vinculação Java, a criação de a WoofingCatnão exigiria acesso ao código fonte para Catnor Woofer.
supercat
3
O C ++ possui dynamic_cast , que lida adequadamente com a tentativa de converter de um Catpara um Woofere responderá à pergunta "você é conversível no tipo X". O C ++ permitirá que você force um elenco, porque, ei, talvez você realmente saiba o que está fazendo, mas também ajudará se não for o que você realmente quer fazer.
Rob K
2
@ RobK: Você está certo sobre a sintaxe, é claro; mea culpa. Eu tenho lido um pouco mais sobre dynamic_cast e parece que, em certo sentido, o C ++ moderno tem todos os objetos polimórficos derivados de uma classe base de "objeto polimórfico" base com quaisquer campos necessários para identificar o tipo de objeto (normalmente uma tabela ponteiro, embora seja um detalhe de implementação). C ++ não descrevem as classes polimórficas dessa maneira, mas passando um ponteiro para dynamic_castterá definido comportamento se ele aponta para um objeto polimórfico e comportamento indefinido se isso não acontecer, então a partir de uma perspectiva semântica ...
supercat
2
... todos os objetos polimórficos armazenam algumas informações com o mesmo layout e todos suportam um comportamento que não é suportado por objetos não polimórficos; na minha opinião, isso significa que eles se comportam como se derivassem de uma base comum, quer a definição de linguagem use essa terminologia ou não.
Supercat
1

O Symbian C ++ de fato tinha uma classe base universal, CBase, para todos os objetos que se comportavam de uma maneira específica (principalmente se eles alocassem heap). Ele forneceu um destruidor virtual, zerou a memória da classe na construção e ocultou o construtor de cópias.

A lógica por trás disso era que era uma linguagem para sistemas embarcados e compiladores e especificações C ++ eram realmente uma merda há 10 anos.

Nem todas as classes herdadas disso, apenas algumas.

James
fonte