Interfaces implicites et explicites

9

Je pense que je comprends les limites réelles du polymorphisme à la compilation et du polymorphisme à l'exécution. Mais quelles sont les différences conceptuelles entre les interfaces explicites (polymorphisme d'exécution. Ie les fonctions virtuelles et les pointeurs / références) et les interfaces implicites (polymorphisme de compilation. Ie les modèles) .

Mes pensées sont que deux objets qui offrent la même interface explicite doivent être du même type d'objet (ou avoir un ancêtre commun), tandis que deux objets qui offrent la même interface implicite n'ont pas besoin d'être du même type d'objet et, à l'exclusion de l'implicite l'interface qu'ils offrent tous les deux, peut avoir des fonctionnalités très différentes.

Des réflexions à ce sujet?

Et si deux objets offrent la même interface implicite, quelles raisons (outre l'avantage technique de ne pas avoir besoin de répartition dynamique avec une table de recherche de fonction virtuelle, etc.) sont là pour ne pas faire hériter ces objets d'un objet de base qui déclare cette interface, donc ce qui en fait une interface explicite ? Une autre façon de le dire: pouvez-vous me donner un cas où deux objets qui offrent la même interface implicite (et peuvent donc être utilisés comme types pour l'exemple de classe de modèle) ne devraient pas hériter d'une classe de base qui rend cette interface explicite?

Quelques articles liés:


Voici un exemple pour rendre cette question plus concrète:

Interface implicite:

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 explicite:

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();
  }
};

Un exemple concret encore plus approfondi:

Certains problèmes C ++ peuvent être résolus avec:

  1. une classe basée sur un modèle dont le type de modèle fournit une interface implicite
  2. une classe non basée sur un modèle qui prend un pointeur de classe de base qui fournit une interface explicite

Code qui ne change pas:

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 */ }
};

Cas 1 . Une classe non basée sur un modèle qui prend un pointeur de classe de base qui fournit une interface explicite:

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;
}

Cas 2 . Une classe basée sur un modèle dont le type de modèle fournit une interface implicite:

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;
}

Cas 3 . Une classe basée sur un modèle dont le type de modèle fournit une interface implicite (cette fois, ne dérivant pas 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;
}

Le cas 1 requiert que l'objet transmis useCoolClass()soit un enfant de CoolClass(et implémente worthless()). Les cas 2 et 3, en revanche, prendront n'importe quelle classe qui a une doSomethingCool()fonction.

Si les utilisateurs du code étaient toujours bien classés CoolClass, alors le cas 1 a un sens intuitif, car CoolClassUserils s'attendraient toujours à une implémentation de a CoolClass. Mais supposons que ce code fasse partie d'un framework API, donc je ne peux pas prédire si les utilisateurs voudront sous CoolClass-classer ou rouler leur propre classe qui a une doSomethingCool()fonction.

Chris Morris
la source
Peut-être que je manque quelque chose, mais la différence importante n'est-elle pas déjà énoncée succinctement dans votre premier paragraphe, à savoir que les interfaces explicites sont du polymorphisme d'exécution, tandis que les interfaces implicites sont du polymorphisme de compilation?
Robert Harvey
2
Il y a certains problèmes qui peuvent être résolus soit en ayant une classe ou une fonction qui prend un pointeur vers une classe abstraite (qui fournit une interface explicite), soit en ayant une classe ou une fonction basée sur un modèle qui utilise un objet qui fournit une interface implicite. Les deux solutions fonctionnent. Quand souhaiteriez-vous utiliser la première solution? La deuxième?
Chris Morris
Je pense que la plupart de ces considérations s'effondrent lorsque vous ouvrez un peu plus les concepts. par exemple, où situeriez-vous le polymorphisme statique sans héritage?
Javier

Réponses:

8

Vous avez déjà défini le point important: l'un est au moment de l' exécution et l'autre au moment de la compilation . Les vraies informations dont vous avez besoin sont les ramifications de ce choix.

Compiletime:

  • Pro: Les interfaces au moment de la compilation sont beaucoup plus granulaires que celles au moment de l'exécution. Je veux dire par là que vous ne pouvez utiliser que les exigences d'une seule fonction ou d'un ensemble de fonctions, comme vous les appelez. Vous n'êtes pas obligé de toujours faire toute l'interface. Les exigences sont uniquement et exactement ce dont vous avez besoin.
  • Pro: Des techniques comme CRTP signifient que vous pouvez utiliser des interfaces implicites pour les implémentations par défaut de choses comme les opérateurs. Vous ne pourriez jamais faire une telle chose avec l'héritage d'exécution.
  • Pro: Les interfaces implicites sont beaucoup plus faciles à composer et à multiplier "héritent" que les interfaces d'exécution, et n'imposent aucun type de restrictions binaires - par exemple, les classes POD peuvent utiliser des interfaces implicites. Il n'y a pas besoin d' virtualhéritage ou d'autres manigances avec des interfaces implicites - un gros avantage.
  • Pro: Le compilateur peut faire bien plus d'optimisations pour les interfaces au moment de la compilation. De plus, le type de sécurité supplémentaire rend le code plus sûr.
  • Pro: Il est impossible de taper des valeurs pour les interfaces d'exécution, car vous ne connaissez pas la taille ou l'alignement de l'objet final. Cela signifie que tout cas qui a besoin / bénéficie de la saisie de valeur tire de grands avantages des modèles.
  • Con: Les modèles sont une chienne à compiler et à utiliser, et ils peuvent être un portage fastidieux entre les compilateurs
  • Con: les modèles ne peuvent pas être chargés au moment de l'exécution (évidemment), ils ont donc des limites dans l'expression des structures de données dynamiques, par exemple.

Durée:

  • Pro: Le type final ne doit pas être décidé avant l'exécution. Cela signifie que l'héritage d'exécution peut exprimer certaines structures de données beaucoup plus facilement, si les modèles peuvent le faire. Vous pouvez également exporter des types polymorphes d'exécution à travers les frontières C, par exemple COM.
  • Pro: Il est beaucoup plus facile de spécifier et d'implémenter l'héritage au moment de l'exécution, et vous n'obtiendrez pas vraiment de comportement spécifique au compilateur.
  • Con: l'héritage au moment de l'exécution peut être plus lent que l'héritage au moment de la compilation.
  • Con: l'héritage d'exécution perd les informations de type.
  • Inconvénient: l'héritage d'exécution est beaucoup moins flexible.
  • Con: l'héritage multiple est une chienne.

Étant donné la liste relative, si vous n'avez pas besoin d'un avantage spécifique de l'héritage d'exécution, ne l'utilisez pas. Il est plus lent, moins flexible et moins sûr que les modèles.

Edit: Il convient de noter qu'en C ++ en particulier, il existe des utilisations de l'héritage autres que le polymorphisme d'exécution. Par exemple, vous pouvez hériter de typedefs, ou l'utiliser pour le balisage de type, ou utiliser le CRTP. En fin de compte, cependant, ces techniques (et d'autres) relèvent vraiment du «temps de compilation», même si elles sont mises en œuvre à l'aide class X : public Y.

DeadMG
la source
Concernant votre premier pro pour le temps de compilation, cela est lié à l'une de mes principales questions. Souhaitez-vous jamais préciser que vous ne souhaitez travailler qu'avec une interface explicite. C'est à dire. «Je me fiche que vous ayez toutes les fonctions dont j'ai besoin, si vous n'héritez pas de la classe Z, je ne veux rien avoir à faire avec vous». De plus, l'héritage d'exécution ne perd pas les informations de type lors de l'utilisation de pointeurs / références, n'est-ce pas?
Chris Morris
@ChrisMorris: Non. Si cela fonctionne, cela fonctionne, c'est tout ce dont vous devez vous soucier. Pourquoi obliger quelqu'un à écrire le même code exact ailleurs?
jmoreno
1
@ChrisMorris: Non, je ne le ferais pas. Si je n'ai besoin que de X, alors c'est l'un des principes fondamentaux de l'encapsulation que je ne devrais demander et ne me soucier que de X. En outre, il perd les informations de type. Vous ne pouvez pas, par exemple, empiler allouer un objet d'un tel type. Vous ne pouvez pas instancier un modèle avec son vrai type. Vous ne pouvez pas y appeler de fonctions membres modèles.
DeadMG
Qu'en est-il d'une situation où vous avez une classe Q qui utilise une certaine classe. Q prend un paramètre de modèle, donc toute classe qui fournit l'interface implicite fera l'affaire, du moins le pensons-nous. Il s'avère que la classe Q attend également de sa classe interne (appelez-la H) qu'elle utilise l'interface de Q. Par exemple, lorsque l'objet H est détruit, il devrait appeler une fonction des Q. Cela ne peut pas être spécifié dans une interface implicite. Ainsi, les modèles échouent. Plus clairement, un ensemble de classes étroitement couplées qui nécessite plus que des interfaces implicites les unes des autres semble empêcher l'utilisation de modèles.
Chris Morris
Con compiletime: laid à déboguer, nécessité de mettre les définitions dans l'en-tête
JFFIGK