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?
c++
object-oriented
object-oriented-design
Matthew James Briggs
fonte
fonte
Respostas:
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, usestd::vector
listas 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
QObject
classe 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).fonte
Object
._hashCode
não é 'usar um recipiente diferente', mas sim apontar O C ++std::unordered_map
faz 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.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.
fonte
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.
fonte
cout.print(x).print(0.5).print("Bye\n")
- não dependeoperator<<
.Porque:
A implementação de qualquer tipo de
virtual
funçã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
toString
nã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
equals
ouhashCode
pode 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, umint
realmente não deve ter uma identidade diferente de seu valor ... dois números inteiros do mesmo valor devem ser iguais.)fonte
open
palavra-chave. Eu não acho que foi além de alguns papéis.shared_ptr<Foo>
para ver se ele também é umshared_ptr<Bar>
(ou mesmo com outros tipos de ponteiro), mesmo seFoo
eBar
sã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.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 semelhanteboost::any
pode ser feito sem uma classe raiz comum. Eboost::any
també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:
Agora todo código que você escreve pode ser
serialization_friendly
.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 umtuple
, ou umint
ou umdouble
, ou umstd::ptrdiff_t
.Adotamos outra abordagem:
que consiste em, aparentemente, não fazer nada. Exceto agora, podemos estender
write_to
substituindowrite_to
como 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:
e agora podemos pegar um tipo arbitrário e encaixotá-lo automaticamente em uma
can_serialize
interface que permite invocarserialize
posteriormente através de uma interface virtual.Assim:
é uma função que aceita tudo o que pode ser serializado, em vez de
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 substituindowrite_to( my_buffer*, std::vector<T> const& )
- com essa sobrecarga, ele pode ser passado para umcan_serialize
e a serialização das informaçõesstd::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.
fonte
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.
fonte
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. ComoCObject
era a classe base de quase todas as classes (infelizmente nãoCString
- equivalente astring
nos 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.
fonte
TObject
antes 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.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::vector
aqui porque ele não implementaIObject
- é melhor criar uma nova derivadaIVectorObject
que 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
Integer
eint
confusã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.)fonte
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ê podestatic_cast
voltar para a classe original.No entanto o que você não pode fazer com
void
que você pode fazer comObject
é 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.fonte
[[no_unique_address]]
, que pode ser usado pelos compiladores para dar largura zero aos subobjetos dos membros.[[no_unique_address]]
permitirá que o compilador faça o EBO de variáveis-membro.Java adota a filosofia de design de que o comportamento indefinido não deve existir . Código como:
testará se
felix
possui um subtipoCat
dessa interface de implementosWoofer
; se o fizer, executará o elenco e a invocaçãowoof()
e, se não, lançará uma exceção. O comportamento do código é totalmente definido,felix
implementandoWoofer
ou 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
*Cat
em 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.fonte
Woofer
é uma interface eCat
é herdável, o elenco seria legítimo porque poderia existir (se não agora, possivelmente no futuro) umWoofingCat
que herdaCat
e implementaWoofer
. Observe que, no modelo de compilação / vinculação Java, a criação de aWoofingCat
não exigiria acesso ao código fonte paraCat
norWoofer
.Cat
para umWoofer
e 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.dynamic_cast
terá 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 ...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.
fonte