web-dev-qa-db-fra.com

Comment déclarez-vous une interface en C++?

Comment configurer une classe qui représente une interface? Est-ce juste une classe de base abstraite?

757
Aaron Fischer

Pour compléter la réponse par bradtgmurray , vous pouvez créer une exception à la liste des méthodes virtuelles pures de votre interface en ajoutant un destructeur virtuel. Cela vous permet de transmettre la propriété du pointeur à une autre partie sans exposer la classe dérivée concrète. Le destructeur n'a rien à faire, car l'interface n'a pas de membre concret. Cela peut sembler contradictoire de définir une fonction à la fois virtuelle et en ligne, mais croyez-moi, ce n'est pas le cas.

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Parent
{
    public:
        virtual ~Parent();
};

class Child : public Parent, public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

Vous n'avez pas besoin d'inclure un corps pour le destructeur virtuel - il s'avère que certains compilateurs ont du mal à optimiser un destructeur vide et qu'il vaut mieux utiliser le paramètre par défaut. 

652
Mark Ransom

Faites une classe avec des méthodes virtuelles pures. Utilisez l'interface en créant une autre classe qui remplace ces méthodes virtuelles.

Une méthode virtuelle pure est une méthode de classe définie comme virtuelle et affectée à 0.

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};
230
bradtgmurray

La raison pour laquelle vous avez une catégorie de type d'interface spéciale en plus des classes de base abstraites dans C # / Java est que C #/Java ne prend pas en charge l'héritage multiple. 

C++ prenant en charge l'héritage multiple, un type spécial n'est pas nécessaire. Une classe de base abstraite sans méthodes non abstraites (virtuelles pures) équivaut fonctionnellement à une interface C #/Java.

140
Joel Coehoorn

Il n'y a pas de concept "d'interface" en soi en C++. D'après les informations dont je dispose, les interfaces ont d'abord été introduites en Java pour pallier le manque d'héritage multiple. Ce concept s'est révélé très utile et le même effet peut être obtenu en C++ en utilisant une classe de base abstraite.

Une classe de base abstraite est une classe dans laquelle au moins une fonction membre (méthode en langage java) est une fonction virtuelle pure déclarée à l'aide de la syntaxe suivante:

class A
{
  virtual void foo() = 0;
};

Une classe de base abstraite ne peut pas être instanciée, i. e. vous ne pouvez pas déclarer un objet de classe A. Vous pouvez uniquement dériver des classes de A, mais toute classe dérivée qui ne fournit pas d'implémentation de foo() sera également abstraite. Pour cesser d'être abstraite, une classe dérivée doit fournir des implémentations pour toutes les fonctions virtuelles pures qu'elle hérite.

Notez qu'une classe de base abstraite peut être plus qu'une interface, car elle peut contenir des membres de données et des fonctions membres qui ne sont pas virtuelles. Un équivalent d'une interface serait une classe de base abstraite sans aucune donnée avec uniquement des fonctions virtuelles pures.

Et, comme l'a souligné Mark Ransom, une classe de base abstraite devrait fournir un destructeur virtuel, comme toute classe de base, à cet égard.

46
Dima

Pour autant que je puisse tester, il est très important d'ajouter le destructeur virtuel. J'utilise des objets créés avec new et détruits avec delete.

Si vous n'ajoutez pas le destructeur virtuel dans l'interface, le destructeur de la classe héritée n'est pas appelé.

class IBase {
public:
    virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
    virtual void Describe() = 0; // pure virtual method
};

class Tester : public IBase {
public:
    Tester(std::string name);
    virtual ~Tester();
    virtual void Describe();
private:
    std::string privatename;
};

Tester::Tester(std::string name) {
    std::cout << "Tester constructor" << std::endl;
    this->privatename = name;
}

Tester::~Tester() {
    std::cout << "Tester destructor" << std::endl;
}

void Tester::Describe() {
    std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}


void descriptor(IBase * obj) {
    obj->Describe();
}

int main(int argc, char** argv) {

    std::cout << std::endl << "Tester Testing..." << std::endl;
    Tester * obj1 = new Tester("Declared with Tester");
    descriptor(obj1);
    delete obj1;

    std::cout << std::endl << "IBase Testing..." << std::endl;
    IBase * obj2 = new Tester("Declared with IBase");
    descriptor(obj2);
    delete obj2;

    // this is a bad usage of the object since it is created with "new" but there are no "delete"
    std::cout << std::endl << "Tester not defined..." << std::endl;
    descriptor(new Tester("Not defined"));


    return 0;
}

Si vous exécutez le code précédent sans virtual ~IBase() {};, vous verrez que le destructeur Tester::~Tester() n'est jamais appelé.

42
Carlos C Soto

Ma réponse est fondamentalement la même que les autres mais je pense qu'il y a deux autres choses importantes à faire:

  1. Déclarez un destructeur virtuel dans votre interface ou créez un destructeur non virtuel protégé pour éviter les comportements indéfinis si quelqu'un tente de supprimer un objet de type IDemo.

  2. Utilisez l'héritage virtuel pour éviter les problèmes d'héritage multiple. (Il y a plus souvent un héritage multiple lorsque nous utilisons des interfaces.) 

Et comme d'autres réponses:

  • Faites une classe avec des méthodes virtuelles pures.
  • Utilisez l'interface en créant une autre classe qui remplace ces méthodes virtuelles.

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
            virtual ~IDemo() {}
    }
    

    Ou

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
        protected:
            ~IDemo() {}
    }
    

    Et 

    class Child : virtual public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    }
    
31
Rexxar

En C++ 11, vous pouvez facilement éviter l'héritage:

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

Dans ce cas, une interface a une sémantique de référence, c’est-à-dire que vous devez vous assurer que l’objet survit à l’interface (il est également possible de créer des interfaces avec une sémantique de valeur).

Ces types d'interfaces ont leurs avantages et leurs inconvénients:

Enfin, l'héritage est la racine de tout mal dans la conception de logiciels complexes. Dans Polymorphisme basé sur les concepts et la sémantique des valeurs de Sean Parent (fortement recommandé, de meilleures versions de cette technique y sont expliquées), le cas suivant est étudié:

Disons que j'ai une application dans laquelle je traite mes formes de manière polymorphe à l'aide de l'interface MyShape:

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

Dans votre application, vous faites la même chose avec différentes formes à l'aide de l'interface YourShape:

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

Supposons maintenant que vous souhaitiez utiliser certaines des formes que j'ai développées dans votre application. Conceptuellement, nos formes ont la même interface, mais pour que mes formes fonctionnent dans votre application, vous devez les étendre de la manière suivante:

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

Premièrement, modifier mes formes peut ne pas être possible du tout. De plus, l'héritage multiple mène au code spaghetti (imaginez un troisième projet utilisant l'interface TheirShape ... que se passera-t-il si elles appellent aussi leur fonction de dessin my_draw?).

Mise à jour: Il existe quelques nouvelles références sur le polymorphisme sans héritage:

10
gnzlbg

Toutes les bonnes réponses ci-dessus… .. Une chose supplémentaire que vous devriez garder à l'esprit: vous pouvez également avoir un destructeur virtuel pur. La seule différence est que vous devez toujours le mettre en œuvre. 

Confus? 


    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

La raison principale pour laquelle vous souhaitez procéder est si vous souhaitez fournir des méthodes d'interface, comme je l'ai fait, mais rendre leur remplacement facultatif. 

Pour faire de la classe une classe d'interface, il faut une méthode virtuelle pure, mais toutes vos méthodes virtuelles ont des implémentations par défaut. La seule méthode restante pour rendre virtuel pur est le destructeur.

Réimplémenter un destructeur dans la classe dérivée n’a rien de grave, je réimplémente toujours un destructeur, virtuel ou non, dans mes classes dérivées.

9
Rodyland

Si vous utilisez le compilateur C++ de Microsoft, vous pouvez procéder comme suit:

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

J'aime cette approche car elle permet d'obtenir un code d'interface beaucoup plus petit et que la taille du code généré peut être considérablement réduite. L'utilisation de novtable supprime toute référence au pointeur vtable dans cette classe, vous ne pouvez donc jamais l'instancier directement. Voir la documentation ici - novtable .

7
Mark Ingram

Un petit ajout à ce qui est écrit là-haut:

Tout d’abord, assurez-vous que votre destructeur est également virtuel 

Deuxièmement, vous souhaiterez peut-être hériter virtuellement (plutôt que normalement) lorsque vous implémenterez, juste pour de bonnes mesures. 

4
Uri

Vous pouvez également envisager des classes de contrat implémentées avec le modèle NVI (modèle d'interface non virtuelle). Par exemple:

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1();
    void f(Parameters p) {
        assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
        // + class invariants.
        do_f(p);
        // Check post-conditions + class invariants.
    }
private:
    virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
    virtual void do_f(Parameters p); // From contract 1.
    virtual void do_g(Parameters p); // From contract 2.
};
4
Luc Hermitte

S'il est vrai que virtual est le standard de facto pour définir une interface, n'oublions pas le modèle classique semblable à C, qui est fourni avec un constructeur en C++:

struct IButton
{
    void (*click)(); // might be std::function(void()) if you prefer

    IButton( void (*click_)() )
    : click(click_)
    {
    }
};

// call as:
// (button.*click)();

Cela présente l’avantage de pouvoir lier à nouveau les événements au moment de l’exécution sans avoir à reconstruire votre classe (car C++ n’a pas de syntaxe permettant de modifier les types polymorphes, il s’agit d’une solution de contournement pour les classes caméléons).

Conseils:

  • Vous pouvez en hériter en tant que classe de base (les types virtuels et non virtuels sont autorisés) et renseigner click dans le constructeur de votre descendant.
  • Vous pouvez avoir le pointeur de fonction en tant que membre protected et avoir une référence et/ou un getter public.
  • Comme mentionné ci-dessus, cela vous permet de passer de l'implémentation au runtime. Ainsi, c'est aussi un moyen de gérer l'état. En fonction du nombre de ifs par rapport aux changements d'état dans votre code, ceci pourrait être plus rapide que switch()es ou ifs (un retournement est prévu autour de 3-4 ifs, mais toujours en premier.
  • Si vous choisissez std::function<> sur les pointeurs de fonction, vous pourriez pouvez gérer toutes vos données d'objet dans IBase. À partir de ce moment, vous pouvez avoir des schémas de valeur pour IBase (par exemple, std::vector<IBase> fonctionnera). Notez que ceci pourrait être plus lent en fonction de votre compilateur et du code STL; De plus, les implémentations actuelles de std::function<> ont tendance à avoir une surcharge par rapport aux pointeurs de fonction ou même aux fonctions virtuelles (cela pourrait changer dans le futur).
0
lorro

Voici la définition de abstract class en c ++ standard

n4687

13.4.2

Une classe abstraite est une classe qui ne peut être utilisée que comme classe de base d'une autre classe. pas d'objets d'un résumé La classe peut être créée sauf en tant que sous-objets d'une classe qui en dérive. Une classe est abstraite si elle a au moins une fonction virtuelle pure. 

0
陳 力