Meilleur moyen de déclarer une interface en C ++ 11

Comme nous le soaps tous, certaines langues ont la notion d’interfaces. Ceci 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 séparée (laisser l’en-tête suffisant). C’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 voudrais. Comment l’améliorer? Peut-être y a-t-il une classe de base quelque part dans l’espace de noms std faite 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 possibilité de copier des classes enfants. Tout ce qui est dit, c’est que l’enfant doit implémenter le test (ce qui est exactement ce que vous voulez pour une interface). Vous ne pouvez pas instancier cette classe pour ne pas avoir à vous soucier des constructeurs implicites car ils ne peuvent jamais être appelés directement en tant que type d’interface parent.

    Si vous souhaitez imposer que les classes enfants implémentent un destructeur, vous pouvez le rendre aussi 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 protéger votre destructeur non virtuel à la place.

    Pour le polymorphism dynamic (à l’exécution), je vous recommande d’utiliser l’idiome NVI ( Non-Virtual-Interface ). Ce modèle maintient l’interface non virtuelle et publique, le destructeur virtuel et public, et l’implémentation 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 */ } }; 

    La bonne chose à propos du polymorphism dynamic est que vous pouvez -pour l’exécution, passer une 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 redissortingbuera automatiquement this pointeur de son type de base statique vers son type dérivé dynamic et appellera l’implémentation correspondante (ce qui se produit généralement via des tables avec des pointeurs vers des fonctions virtuelles).

    Pour le statique (polymorphism à la compilation), je recommande d’utiliser le modèle de modèle curieusement récurrent (CRTP). Ceci est considérablement plus complexe car la conversion automatique de base vers un polymporphisme dynamic doit être effectuée avec static_cast . Ce casting statique peut être défini dans une classe auxiliaire dérivée de chaque interface statique

     template class enable_down_cast { private: typedef enable_down_cast Base; public: Derived const* self() const { // casting "down" the inheritance hierarchy return static_cast(this); } Derived* self() { return static_cast(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 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 lui-même en tant que 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 quelle forme? Les deux formulaires vous permettront de réutiliser une interface commune et d’injecter des tests de pré / post-condition dans la classe d’interface. L’avantage du polymorphism dynamic est que vous avez 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és d’inline). Le polymporhisme statique est le miroir de ce phénomène: aucun 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 à la compilation. Fondamentalement, un compromis efficacité / flexibilité.

    REMARQUE: pour le polymporhisme à la compilation, vous pouvez également utiliser les parameters du modèle. La différence entre les interfaces statiques via l’identificateur CRTP et les parameters de modèle ordinaires est que l’interface de type CRTP est explicite (basée sur les fonctions membres) et que l’interface de modèle est implicite (basée sur des expressions valides)

    Selon Scott Meyers (Effective Modern C ++): Lorsque vous typeid une interface (ou une classe de base polymorphe), vous avez besoin d’un destructeur virtuel pour obtenir des résultats tels que delete ou typeid sur un object de classe accessible via une référence.

     virtual ~Testable() = default; 

    Cependant, un destructeur déclaré par l’utilisateur supprime la génération des opérations de déplacement. Par conséquent, pour prendre en charge les opérations de déplacement, vous devez append:

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

    En remplaçant la class mot 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 pas instancier une classe avec des méthodes virtuelles pures. Cela vaut également pour le constructeur de la copie. Le constructeur par défaut généré par le compilateur sera vide car vous ne possédez aucun membre de données et est tout à fait suffisant pour vos classes dérivées.

    Vous avez raison de vous soucier de l’opérateur = car celui généré par le compilateur va certainement faire la mauvaise chose. En pratique, personne ne s’en préoccupe car la copie d’un object d’interface vers 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 public et virtuel dans ce cas.

    Le résultat final est une seule ligne de plus 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 descripteurs et / ou si tous les membres de la classe ont leurs propres destructeurs qui géreront tout nettoyage. De même, dans le cas d’une classe de base virtuelle, comme la classe de base ne peut jamais être instanciée directement, il n’est pas nécessaire de déclarer un constructeur si vous ne voulez que définir une interface sans membres de données … le compilateur les défauts sont très bien. Le seul élément à 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(); }