Interfaces implícitas vs explícitas

9

Acho que compreendo as limitações reais do polimorfismo em tempo de compilação e do polimorfismo em tempo de execução. Mas quais são as diferenças conceituais entre interfaces explícitas (polimorfismo em tempo de execução. Isto é, funções virtuais e ponteiros / referências) e interfaces implícitas (polimorfismo em tempo de compilação. Ie. Modelos) .

Penso que dois objetos que oferecem a mesma interface explícita devem ter o mesmo tipo de objeto (ou ter um ancestral comum), enquanto dois objetos que oferecem a mesma interface implícita não precisam ter o mesmo tipo de objeto e, excluindo o implícito interface que ambos oferecem, pode ter uma funcionalidade bastante diferente.

Alguma idéia sobre isso?

E se dois objetos oferecem a mesma interface implícita, por que razões (além do benefício técnico de não precisar de expedição dinâmica com uma tabela de pesquisa de funções virtuais etc.), existem para não ter esses objetos herdados de um objeto base que declara essa interface, portanto tornando-o uma interface explícita ? Outra maneira de dizer: você pode me dar um caso em que dois objetos que oferecem a mesma interface implícita (e, portanto, podem ser usados ​​como tipos para a classe de modelo de exemplo) não devem herdar de uma classe base que torna explícita a interface?

Algumas postagens relacionadas:


Aqui está um exemplo para tornar essa pergunta mais concreta:

Interface implícita:

class Class1
{
public:
  void interfaceFunc();
  void otherFunc1();
};

class Class2
{
public:
  void interfaceFunc();
  void otherFunc2();
};

template <typename T>
class UseClass
{
public:
  void run(T & obj)
  {
    obj.interfaceFunc();
  }
};

Interface explícita:

class InterfaceClass
{
public:
  virtual void interfaceFunc() = 0;
};

class Class1 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc1();
};

class Class2 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc2();
};

class UseClass
{
public:
  void run(InterfaceClass & obj)
  {
    obj.interfaceFunc();
  }
};

Um exemplo concreto ainda mais profundo:

Alguns problemas de C ++ podem ser resolvidos com:

  1. uma classe de modelo cujo tipo de modelo fornece uma interface implícita
  2. uma classe não modelo que usa um ponteiro de classe base que fornece uma interface explícita

Código que não muda:

class CoolClass
{
public:
  virtual void doSomethingCool() = 0;
  virtual void worthless() = 0;
};

class CoolA : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that an A would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

class CoolB : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that a B would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

Caso 1 . Uma classe sem modelo que usa um ponteiro de classe base que fornece uma interface explícita:

class CoolClassUser
{
public:  
  void useCoolClass(CoolClass * coolClass)
  { coolClass.doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Caso 2 . Uma classe de modelo cujo tipo de modelo fornece uma interface implícita:

template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser<CoolClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Caso 3 . Uma classe de modelo cujo tipo de modelo fornece uma interface implícita (desta vez, não derivada de CoolClass:

class RandomClass
{
public:
  void doSomethingCool()
  { /* Do cool stuff that a RandomClass would do */ }

  // I don't have to implement worthless()! Na na na na na!
}


template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  RandomClass * c1 = new RandomClass;
  RandomClass * c2 = new RandomClass;

  CoolClassUser<RandomClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

O caso 1 exige que o objeto que está sendo passado useCoolClass()seja filho de CoolClass(e implemente worthless()). Os casos 2 e 3, por outro lado, terão qualquer classe que tenha uma doSomethingCool()função.

Se os usuários do código sempre subclassificassem bem CoolClass, o Caso 1 CoolClassUserfaria sentido intuitivo, pois sempre estaria esperando uma implementação de a CoolClass. Mas suponha que esse código faça parte de uma estrutura de API, portanto, não posso prever se os usuários desejarão subclassificar CoolClassou rolar sua própria classe que possui uma doSomethingCool()função.

Chris Morris
fonte
Talvez esteja faltando alguma coisa, mas a diferença importante já não foi declarada de maneira sucinta no seu primeiro parágrafo, que é que interfaces explícitas são polimorfismo em tempo de execução, enquanto interfaces implícitas são polimorfismo em tempo de compilação?
Robert Harvey
2
Existem alguns problemas que podem ser resolvidos com uma classe ou função que leva um ponteiro para uma classe abstrata (que fornece uma interface explícita) ou com uma classe ou função modelada que usa um objeto que fornece uma interface implícita. Ambas as soluções funcionam. Quando você gostaria de usar a primeira solução? O segundo?
21412 Chris Morris
Eu acho que a maioria dessas considerações desmorona quando você abre um pouco mais os conceitos. por exemplo, onde você ajustaria o polimorfismo estático sem herança?
Javier

Respostas:

8

Você já definiu o ponto importante: um é o tempo de execução e o outro , o tempo de compilação . As informações reais de que você precisa são as ramificações dessa escolha.

Compiletime:

  • Pro: as interfaces em tempo de compilação são muito mais granulares que as em tempo de execução. Com isso, o que quero dizer é que você pode usar apenas os requisitos de uma única função ou de um conjunto de funções, como você os chama. Você não precisa sempre fazer toda a interface. Os requisitos são apenas e exatamente o que você precisa.
  • Pro: Técnicas como CRTP significam que você pode usar interfaces implícitas para implementações padrão de coisas como operadores. Você nunca poderia fazer isso com herança em tempo de execução.
  • Pro: interfaces implícitas são muito mais fáceis de compor e multiplicar "herdar" do que interfaces de tempo de execução e não impõem nenhum tipo de restrição binária - por exemplo, as classes POD podem usar interfaces implícitas. Não há necessidade de virtualherança ou outras travessuras com interfaces implícitas - uma grande vantagem.
  • Pro: o compilador pode fazer muito mais otimizações para interfaces em tempo de compilação. Além disso, o tipo extra de segurança contribui para um código mais seguro.
  • Pro: é impossível digitar valor para interfaces de tempo de execução, porque você não sabe o tamanho ou o alinhamento do objeto final. Isso significa que qualquer caso que precise / se beneficie da digitação de valor ganha grandes benefícios com os modelos.
  • Contras: Os modelos são uma droga para compilar e usar, e podem ser portáveis ​​de maneira complicada entre compiladores
  • Contras: Os modelos não podem ser carregados em tempo de execução (obviamente), portanto, eles têm limites para expressar estruturas dinâmicas de dados, por exemplo.

Tempo de execução:

  • Pro: O tipo final não precisa ser decidido até o tempo de execução. Isso significa que a herança em tempo de execução pode expressar algumas estruturas de dados com muito mais facilidade, se os modelos conseguirem fazê-lo. Além disso, você pode exportar tipos polimórficos em tempo de execução através dos limites C, por exemplo, COM.
  • Pro: é muito mais fácil especificar e implementar a herança em tempo de execução, e você realmente não terá nenhum comportamento específico do compilador.
  • Contras: A herança em tempo de execução pode ser mais lenta que a herança em tempo de compilação.
  • Con: herança em tempo de execução perde informações de tipo.
  • Contras: A herança em tempo de execução é muito menos flexível.
  • Contras: herança múltipla é uma vadia.

Dada a lista relativa, se você não precisar de uma vantagem específica da herança em tempo de execução, não a utilize. É mais lento, menos flexível e menos seguro que os modelos.

Edit: Vale a pena notar que em C ++ particularmente existem usos para herança além do polimorfismo em tempo de execução. Por exemplo, você pode herdar typedefs ou usá-lo para identificação de tipo ou usar o CRTP. Por fim, essas técnicas (e outras) realmente se enquadram no "tempo de compilação", mesmo que elas sejam implementadas usando class X : public Y.

DeadMG
fonte
Em relação ao seu primeiro profissional para compiletime, isso está relacionado a uma das minhas principais perguntas. Você gostaria de deixar claro que deseja trabalhar apenas com uma interface explícita. Ou seja. 'Eu não me importo se você tem todas as funções necessárias, se você não herda da classe Z, então eu não quero ter nada a ver com você'. Além disso, a herança em tempo de execução não perde as informações de tipo ao usar ponteiros / referências, correto?
21412 Chris Morris
@ ChrisMorris: Não. Se funciona, funciona, e é com isso que você deve se preocupar. Por que fazer alguém escrever exatamente o mesmo código em outro lugar?
jmoreno
11
@ ChrisMorris: Não, eu não faria. Se eu só precisar do X, é um dos princípios fundamentais básicos do encapsulamento que eu só deveria pedir e me preocupar com o X. Além disso, ele perde informações de tipo. Você não pode, por exemplo, empilhar alocar um objeto desse tipo. Você não pode instanciar um modelo com seu tipo verdadeiro. Você não pode chamar funções de membro modeladas neles.
DeadMG
Que tal uma situação em que você tem uma classe Q que usa alguma classe. Q usa um parâmetro de modelo, então qualquer classe que forneça a interface implícita funcionará, ou assim pensamos. Acontece que a classe Q também espera que sua classe interna (chame de H) use a interface de Q. Por exemplo, quando o objeto H é destruído, ele deve chamar alguma função de Q's. Isso não pode ser especificado em uma interface implícita. Assim, os modelos falham. Em termos mais claros, um conjunto de classes fortemente acoplado que requer mais do que interfaces implícitas um do outro parece impedir o uso de modelos.
Chris Morris
Con compiletime: feio para depurar, necessidade de colocar as definições no cabeçalho
JFFIGK