Interfaces explícitas vs explícitas

5

Acho que entendo 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 de tempo de execução, isto é, funções virtuais e ponteiros / referências) e interfaces implícitas (polimorfismo no tempo de compilação, ou seja, templates) .

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

Alguma opinião sobre isso?

E se dois objetos oferecem a mesma interface implícita, quais razões (além do benefício técnico de não precisar de despacho dinâmico com uma tabela de pesquisa de função virtual, etc) estão lá para não ter esses objetos herdados de um objeto base que declara que interface, tornando assim uma interface explícita ? Outra maneira de dizer isso: 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 torne essa interface explícita? p>

Algumas postagens relacionadas:

Veja um exemplo para tornar essa questão 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 sem 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 recebe 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 transmitido para useCoolClass() seja filho de CoolClass (e implemente worthless() ). Os casos 2 e 3, por outro lado, terão qualquer classe que tenha uma função doSomethingCool() .

Se os usuários do código estivessem sempre usando a subclasse CoolClass , o Caso 1 faria sentido intuitivo, pois o CoolClassUser sempre estaria esperando uma implementação de 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 CoolClass ou criar sua própria classe que tenha uma função doSomethingCool() .

    
por Chris Morris 10.02.2012 / 19:03
fonte

1 resposta

4

Você já definiu o ponto importante - o tempo de execução e o outro tempo de compilação . A informação real que você precisa é das ramificações desta escolha.

Compiletime:

  • Pro: As interfaces de tempo de compilação são muito mais granulares do que as de tempo de execução. Com isso, o que quero dizer é que você pode usar apenas os requisitos de uma única função, ou um conjunto de funções, como você as 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 uma coisa dessas 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, classes POD podem usar interfaces implícitas . Não há necessidade de virtual herança ou outros truques 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 fazer um tipo de 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 beneficia da digitação de valores ganha grandes benefícios com os modelos.
  • Con: Modelos são uma puta para compilar e usar, e eles podem ser portais fiddly entre compiladores
  • Con: Modelos não podem ser carregados em tempo de execução (obviamente), então eles têm limites em expressar estruturas de dados dinâmicas, 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 muito mais fáceis, se os modelos puderem fazer isso. Além disso, você pode exportar tipos polimórficos em tempo de execução através de limites C, por exemplo, COM.
  • Pro: É muito mais fácil especificar e implementar a herança em tempo de execução e você não obterá nenhum comportamento específico do compilador.
  • Con: A herança em tempo de execução pode ser mais lenta que o inheritane em tempo de compilação.
  • Con: a herança em tempo de execução perde informações de tipo.
  • Con: a herança em tempo de execução é muito menos flexível.
  • Con: Herança múltipla é uma cadela.

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

Edit: Vale a pena notar que em C ++ particularmente há usos para herança outros do que o polimorfismo em tempo de execução. Por exemplo, você pode herdar typedefs, ou usá-lo para tagging de tipo, ou usar o CRTP. Em última análise, porém, essas técnicas (e outras) realmente se enquadram em "Compile-time", mesmo que sejam implementadas usando class X : public Y .

    
por 10.02.2012 / 19:36
fonte