Por que os construtores não são herdados?

33

Estou confuso sobre quais poderiam ser os problemas se um construtor fosse herdado de uma classe base. O Cpp Primer Plus diz:

Os construtores são diferentes de outros métodos de classe, pois criam novos objetos, enquanto outros são invocados por objetos existentes . Esse é um dos motivos pelos quais os construtores não são herdados . Herança significa que um objeto derivado pode usar um método de classe base, mas, no caso de construtores, o objeto não existe até que o construtor tenha feito seu trabalho.

Entendo que o construtor é chamado antes da construção do objeto.

Como isso pode causar problemas se uma classe filho herda ( ao herdar, a classe filho é capaz de substituir o método da classe pai, etc. Não apenas acessando o método da classe pai ) o construtor pai?

Entendo que não há necessidade de chamar explicitamente um construtor a partir de um código [não que eu saiba ainda.], Exceto ao criar objetos. Mesmo assim, você pode fazer isso usando algum mecanismo para chamar o construtor pai [No cpp, usando ::ou usando member initialiser list, No java usando super]. Em Java, há uma imposição para chamá-lo na 1ª linha. Entendo que é para garantir que o objeto pai seja criado primeiro e, em seguida, a construção do objeto filho prossiga.

Pode substituí- lo. Mas, não posso apresentar situações em que isso possa representar um problema. Se a criança herdar o construtor pai, o que pode dar errado?

Então, isso é apenas para evitar herdar funções desnecessárias. Ou há algo mais?

Suvarna Pattayil
fonte
Não estou muito atualizado com o C ++ 11, mas acho que há alguma forma de herança de construtor nele.
Yannis 13/05
3
Lembre-se de que o que você faz nos construtores, você deve cuidar dos destruidores.
Manoj R
2
Eu acho que você precisa definir o que significa "herdar um construtor". Todas as respostas parecem ter variações sobre o que elas acham que "herdar um construtor" significa. Acho que nenhum deles corresponde à minha interpretação inicial. A descrição "na caixa cinza" acima "Herança significa que um objeto derivado pode usar um método de classe base" implica que os construtores são herdados. Um objeto derivado certamente pode e usa métodos construtores de classe base, SEMPRE.
Dunk 13/05
1
Obrigado pelo esclarecimento de herdar um construtor, agora você pode obter algumas respostas.
Dunk

Respostas:

41

Não pode haver nenhuma herança adequada de construtores em C ++, porque o construtor de uma classe derivada precisa executar ações adicionais que um construtor de classe base não precisa fazer e não conhece. Essas ações adicionais são a inicialização dos membros de dados da classe derivada (e em uma implementação típica, também configurando o vpointer para se referir à tabela de classes derivadas).

Quando uma classe é construída, há várias coisas que sempre precisam acontecer: Os construtores da classe base (se houver) e os membros diretos precisam ser chamados e se houver alguma função virtual, o vpointer deve ser configurado corretamente . Se você não fornecer um construtor para a sua classe, em seguida, o compilador vai criar um que executa as ações e nada mais necessários. Se você fazer fornecer um construtor, mas perder algumas das ações necessárias (por exemplo, a inicialização de alguns membros), em seguida, o compilador irá adicionar automaticamente as ações que faltam para o seu construtor. Dessa forma, o compilador garante que cada classe tenha pelo menos um construtor e que cada construtor inicialize totalmente os objetos que cria.

No C ++ 11, uma forma de 'herança de construtor' foi introduzida, na qual você pode instruir o compilador a gerar um conjunto de construtores para você que usa os mesmos argumentos que os construtores da classe base e que apenas encaminham esses argumentos para o classe base.
Embora seja oficialmente chamado de herança, não é verdade, porque ainda existe uma função específica de classe derivada. Agora ele é gerado pelo compilador, em vez de ser explicitamente escrito por você.

Esse recurso funciona assim:

struct Base {
    Base(int a) : i(a) {}
    int i;
};

struct Derived : Base {
    Derived(int a, std::string s) : Base(a), m(s) {}

    using Base::Base; // Inherit Base's constructors.
    // Equivalent to:
    //Derived(int a) : Base(a), m() {}

    std::string m;
};

Derivedagora possui dois construtores (sem contar os construtores copiar / mover). Um que aceita um int e uma string e outro que aceita apenas um int.

Bart van Ingen Schenau
fonte
Portanto, isso pressupõe que a classe filho não terá seu próprio construtor? e o construtor herdado, obviamente, não tem conhecimento dos membros filhos e inicializações de ser feito
Suvarna Pattayil
C ++ é definido de tal maneira que é impossível para uma classe não ter seu próprio construtor (es). É por isso que não considero "herança de construtor" uma herança. É apenas uma maneira de evitar escrever clichês tediosos.
Bart van Ingen Schenau
2
Eu acho que a confusão decorre do fato de que mesmo um construtor vazio trabalha nos bastidores. Portanto, o construtor realmente tem duas partes: as obras internas e a parte que você escreve. Como eles não são construtores bem separados, não são herdados.
Sarien
1
Por favor, considere indicar de onde m()vem e como isso mudaria se seu tipo fosse, por exemplo int.
Deduplicator
É possível fazer using Base::Baseimplicitamente? Ele teria grandes consequências se eu esqueci essa linha em uma classe derivada e eu preciso para herdar o construtor em todas as classes derivadas
Pós Auto
7

Não está claro o que você quer dizer com "herdando o construtor pai". Você usa a palavra substituição , o que sugere que você pode estar pensando em construtores que se comportam como funções virtuais polimórficas . Eu deliberadamente não uso o termo "construtores virtuais" porque esse é um nome comum para um padrão de código em que você realmente exige uma instância de um objeto já existente para criar outro.

Há pouca utilidade para construtores polimórficos fora do padrão "construtor virtual", e é difícil criar um cenário concreto em que um construtor polimórfico real possa ser usado. Um exemplo muito elaborado que nem é remotamente válido em C ++ :

struct Base {
  virtual Base(unsigned p1, unsigned p2) {...}
};

struct Derived: public Base {
  Derived(unsigned p1, unsigned p2) : Base(p1, p2) override {...}
};

int main(void) {
  unsigned p1 = 0;
  unsigned p2 = 42;
  Derived *p_d1 = new Base(p1, p2); // This might call Derived(unsigned, unsigned).
  Derived *p_d2 = nullptr;
  p_d2 = new Base(p1, p2); // This might call Derived(unsigned, unsigned) too.
}

Nesse caso, o construtor chamado depende do tipo concreto da variável que está sendo construída ou atribuída. É complexo detectar durante a análise / geração de código e não tem utilidade: você conhece o tipo concreto que está construindo e escreveu um construtor específico para a classe derivada. O seguinte código C ++ válido faz exatamente o mesmo, é um pouco mais curto e é mais explícito no que faz:

struct Base {
  Base(unsigned p1, unsigned p2) {...}
};

struct Derived: public Base {
  Derived(unsigned p1, unsigned p2) : Base(p1, p2) {...}
};

int main(void) {
  unsigned p1 = 0;
  unsigned p2 = 42;
  Derived *p_d1 = new Derived(p1, p2); 
  Derived *p_d2 = nullptr;
  p_d2 = new Derived(p1, p2);
}


Uma segunda interpretação ou talvez uma pergunta adicional é - e se os construtores da classe Base estivessem automaticamente presentes em todas as classes derivadas, a menos que explicitamente ocultos.

Se a criança herdar o construtor pai, o que pode dar errado?

Você precisaria escrever código adicional para ocultar um construtor pai que é incorreto para usar na construção da classe derivada. Isso pode acontecer quando a classe derivada especializa a classe base de uma maneira que certos parâmetros se tornam irrelevantes.

O exemplo típico é retângulos e quadrados (observe que os quadrados e retângulos geralmente não são substituíveis por Liskov, portanto, não é um design muito bom, mas destaca o problema).

struct Rectangle {
  Rectangle(unsigned width, unsigned height) {...}
};

struct Square : public Rectangle {
  explicit Square(unsigned side) : Rectangle(side, side) {...}
};

Se Square herdou o construtor de dois valores de Retângulo, você poderia construir quadrados com altura e largura diferentes ... Isso é logicamente errado, então você deseja ocultar esse construtor.

Joris Timmermans
fonte
3

Por que os construtores não são herdados: a resposta é surpreendentemente simples: o construtor da classe base "constrói" a classe base e o construtor da classe herdada "constrói" a classe herdada. Se a classe herdada herdasse o construtor, o construtor tentaria criar um objeto do tipo classe base e você não conseguiria "construir" um objeto do tipo classe herdada.

Que tipo de derrota o propósito de herdar uma classe.

Pieter B
fonte
3

O problema mais óbvio em permitir que a classe derivada substitua o construtor da classe base é que o desenvolvedor da classe derivada agora é responsável por saber como construir sua (s) classe (s) base (s). O que acontece quando a classe derivada não constrói a classe base corretamente?

Além disso, o princípio Liskov-Substitution não se aplicaria mais, pois você não poderá mais contar com sua coleção de objetos da classe base para serem compatíveis entre si, porque não há garantia de que a classe base foi construída de forma adequada ou compatível com os outros tipos derivados.

É ainda mais complicado quando mais de 1 nível de herança é adicionado. Agora sua classe derivada precisa saber como construir todas as classes base na cadeia.

Então, o que acontece se você adicionar uma nova classe base ao topo da hierarquia de herança? Você precisaria atualizar todos os construtores de classe derivada.

Dunk
fonte
2

Os construtores são fundamentalmente diferentes de outros métodos:

  1. Eles são gerados se você não os escrever.
  2. Todos os construtores da classe base são chamados implicitamente, mesmo que você não faça isso manualmente
  3. Você não os chama explicitamente, mas criando objetos.

Então, por que eles não são herdados? Resposta simples: porque sempre há uma substituição, gerada ou gravada manualmente.

Por que toda classe precisa de um construtor? Essa é uma pergunta complicada e acredito que a resposta depende do compilador. Existe um construtor "trivial", para o qual o compilador não exige que seja chamado como parece. Eu acho que é a coisa mais próxima do que você quer dizer com herança, mas pelas três razões expostas acima, acho que comparar construtores com métodos normais não é realmente útil. :)

Sarien
fonte
1

Toda classe precisa de um construtor, mesmo o padrão.
O C ++ criará construtores padrão para você, exceto se você criar um construtor especializado.
Caso sua classe base use um construtor especializado, você precisará escrever o construtor especializado na classe derivada, mesmo que os dois sejam iguais e encadeá-los de volta.
O C ++ 11 permite evitar a duplicação de código em construtores usando :

Class A : public B {
using B:B;
....
  1. Um conjunto de construtores herdados é composto por

    • Todos os construtores não modelo da classe base (depois de omitir os parâmetros de reticências, se houver) (desde C ++ 14)
    • Para cada construtor com argumentos padrão ou o parâmetro de reticências, todas as assinaturas de construtor que são formadas removendo as reticências e omitindo argumentos padrão das extremidades do argumento listam uma por uma
    • Todos os modelos de construtores da classe base (depois de omitir os parâmetros de reticências, se houver) (desde C ++ 14)
    • Para cada modelo de construtor com argumentos padrão ou reticências, todas as assinaturas de construtor que são formadas removendo as reticências e omitindo argumentos padrão das extremidades do argumento listam uma por uma
  2. Todos os construtores herdados que não são o construtor padrão ou o construtor de copiar / mover e cujas assinaturas não correspondem aos construtores definidos pelo usuário na classe derivada, são declarados implicitamente na classe derivada. Os parâmetros padrão não são herdados

MeduZa
fonte
0

Você pode usar:

MyClass() : Base()

Você está perguntando por que você tem que fazer isso?

A subclasse pode ter propriedades adicionais que talvez precisem ser inicializadas no construtor ou pode inicializar as variáveis ​​da classe base de uma maneira diferente.

De que outra forma você criaria o objeto subtipo?

Tom Tom
fonte
Obrigado. Na verdade, estou tentando descobrir por que um construtor não é herdado na classe filho, como outros métodos da classe pai.
Suvarna Pattayil