Comme nous le savons tous, certains langages ont la notion d'interfaces. C'est Java:
public interface Testable {
void test();
}
Comment puis-je y parvenir en C++ (ou C++ 11) de la manière la plus compacte et avec peu de bruit de code? J'apprécierais une solution qui n'aurait pas besoin d'une définition distincte (que l'en-tête soit suffisant). Ceci est une approche très simple que même je trouve buggy ;-)
class Testable {
public:
virtual void test() = 0;
protected:
Testable();
Testable(const Testable& that);
Testable& operator= (const Testable& that);
virtual ~Testable();
}
Ce n'est que le début ... et déjà plus longtemps que je le souhaiterais. Comment l'améliorer? Peut-être y a-t-il une classe de base quelque part dans l'espace de noms std conçue spécialement pour cela?
Qu'en est-il de:
class Testable
{
public:
virtual ~Testable() { }
virtual void test() = 0;
}
En C++, cela n'a aucune incidence sur la capacité de copie des classes enfants. Tout cela dit, c'est que l'enfant doit implémenter test
(c'est exactement ce que vous voulez pour une interface). Vous ne pouvez pas instancier cette classe, vous n'avez donc pas à vous soucier des constructeurs implicites car ils ne peuvent jamais être appelés directement en tant que type d'interface parent.
Si vous souhaitez appliquer que les classes enfants implémentent un destructeur, vous pouvez également le rendre pur (mais vous devez toujours l'implémenter dans l'interface).
Notez également que si vous n'avez pas besoin d'une destruction polymorphe, vous pouvez choisir de rendre votre destructeur protégé non virtuel à la place.
Pour le polymorphisme dynamique (à l'exécution), je recommanderais d'utiliser l'idiome Non-Virtual-Interface (NVI). Ce modèle conserve l'interface non virtuelle et publique, le destructeur virtuel et public, et la mise en œuvre pure virtuelle et privée
class DynamicInterface
{
public:
// non-virtual interface
void fun() { do_fun(); } // equivalent to "this->do_fun()"
// enable deletion of a Derived* through a Base*
virtual ~DynamicInterface() = default;
private:
// pure virtual implementation
virtual void do_fun() = 0;
};
class DynamicImplementation
:
public DynamicInterface
{
private:
virtual void do_fun() { /* implementation here */ }
};
Ce qui est bien avec le polymorphisme dynamique, c'est que vous pouvez, lors de l'exécution, passer n'importe quelle classe dérivée où un pointeur ou une référence à la classe de base de l'interface est attendu. Le système d'exécution abaissera automatiquement le pointeur this
de son type de base statique vers son type dérivé dynamique et appellera l'implémentation correspondante (se produit généralement via des tables avec des pointeurs vers des fonctions virtuelles).
Pour statique (polymorphisme au moment de la compilation), je recommanderais d'utiliser le Curieusement modèle récurrent modèle (CRTP). Ceci est beaucoup plus complexe car la conversion automatique de la base en dérivé du polymporphisme dynamique doit être effectuée avec static_cast
. Cette conversion statique peut être définie dans une classe d'assistance dont chaque interface statique dérive
template<typename Derived>
class enable_down_cast
{
private:
typedef enable_down_cast Base;
public:
Derived const* self() const
{
// casting "down" the inheritance hierarchy
return static_cast<Derived const*>(this);
}
Derived* self()
{
return static_cast<Derived*>(this);
}
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~enable_down_cast() = default; // C++11 only, use ~enable_down_cast() {} in C++98
};
Ensuite, vous définissez une interface statique comme celle-ci:
template<typename Impl>
class StaticInterface
:
// enable static polymorphism
public enable_down_cast< Impl >
{
private:
// dependent name now in scope
using enable_down_cast< Impl >::self;
public:
// interface
void fun() { self()->do_fun(); }
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~StaticInterface() = default; // C++11 only, use ~IFooInterface() {} in C++98/03
};
et enfin vous faites une implémentation qui dérive de l'interface avec elle-même comme paramètre
class StaticImplementation
:
public StaticInterface< StaticImplementation >
{
private:
// implementation
friend class StaticInterface< StaticImplementation > ;
void do_fun() { /* your implementation here */ }
};
Cela vous permet toujours d'avoir plusieurs implémentations de la même interface, mais vous devez savoir au moment de la compilation quelle implémentation vous appelez.
Alors, quand utiliser quel formulaire? Les deux formulaires vous permettront de réutiliser une interface commune et d'injecter des tests de conditions pré/post dans la classe d'interface. L'avantage du polymorphisme dynamique est que vous disposez d'une flexibilité d'exécution, mais vous payez pour cela dans les appels de fonction virtuels (généralement un appel via un pointeur de fonction, avec peu de possibilité d'inline). Le polymporhisme statique en est le miroir: pas de surcharge d'appel de fonction virtuelle, mais l'inconvénient est que vous avez besoin de plus de code passe-partout et que vous devez savoir ce que vous appelez au moment de la compilation. Fondamentalement, un compromis efficacité/flexibilité.
REMARQUE: pour le polymporhisme à la compilation, vous pouvez également utiliser des paramètres de modèle. La différence entre l'interface statique via l'idiome CRTP et les paramètres de modèle ordinaires est que l'interface de type CRTP est explicite (basée sur les fonctions membres) et l'interface de modèle est implicite (basée sur des expressions valides)
Selon Scott Meyers (Effective Modern C++): Lorsque vous déclarez une interface (ou une classe de base polymorphe), vous avez besoin d'un destructeur virtuel, pour des résultats corrects d'opérations comme delete
ou typeid
sur un objet de classe dérivée accessible via un pointeur ou une référence de classe de base.
virtual ~Testable() = default;
Cependant, un destructeur déclaré par l'utilisateur supprime la génération des opérations de déplacement, donc pour prendre en charge les opérations de déplacement, vous devez ajouter:
Testable(Testable&&) = default;
Testable& operator=(Testable&&) = default;
La déclaration des opérations de déplacement désactive les opérations de copie et vous devez également:
Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;
Et le résultat final est:
class Testable
{
public:
virtual ~Testable() = default; // make dtor virtual
Testable(Testable&&) = default; // support moving
Testable& operator=(Testable&&) = default;
Testable(const Testable&) = default; // support copying
Testable& operator=(const Testable&) = default;
virtual void test() = 0;
};
Un autre article intéressant ici: La règle de zéro en C++
En remplaçant Word class
par struct
, toutes les méthodes seront publiques par défaut et vous pourrez enregistrer une ligne.
Il n'est pas nécessaire de protéger le constructeur, car vous ne pouvez de toute façon pas instancier une classe avec des méthodes virtuelles pures. Cela vaut également pour le constructeur de copie. Le constructeur par défaut généré par le compilateur sera vide car vous n'avez aucun membre de données et est complètement suffisant pour vos classes dérivées.
Vous avez raison de vous inquiéter de la =
puisque l'opérateur généré par le compilateur fera certainement la mauvaise chose. Dans la pratique, personne ne s'en soucie jamais car copier un objet d'interface dans un autre n'a jamais de sens; ce n'est pas une erreur qui arrive souvent.
Les destructeurs d'une classe héritable doivent toujours être publics et virtuels, ou protégés et non virtuels. Je préfère le public et le virtuel dans ce cas.
Le résultat final n'est qu'une ligne plus longue que l'équivalent Java:
struct Testable {
virtual void test() = 0;
virtual ~Testable();
};
Gardez à l'esprit que la "règle des trois" n'est pas nécessaire si vous ne gérez pas les pointeurs, les poignées et/ou tous les membres de données de la classe ont leurs propres destructeurs qui gèreront tout nettoyage. Toujours dans le cas d'une classe de base virtuelle, car la classe de base ne peut jamais être directement instanciée, il n'est pas nécessaire de déclarer un constructeur si tout ce que vous voulez faire est de définir une interface qui n'a pas de membres de données ... le compilateur les défauts sont très bien. Le seul élément que vous devez conserver est le destructeur virtuel si vous prévoyez d'appeler delete
sur un pointeur du type d'interface. Donc, en réalité, votre interface peut être aussi simple que:
class Testable
{
public:
virtual void test() = 0;
virtual ~Testable();
}