Question 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?


728
2017-11-25 16:48


origine


Réponses:


Pour développer la réponse par bradtgmurray, vous pouvez faire une exception à la liste de méthodes virtuelles pure de votre interface en ajoutant un destructeur virtuel. Cela vous permet de passer 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 aucun membre concret. Il peut sembler contradictoire de définir une fonction à la fois virtuelle et inline, 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 de la difficulté à optimiser un destructeur vide et qu'il vaut mieux utiliser la valeur par défaut.


625
2017-11-25 17:11



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

226
2017-11-25 16:53



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

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


130
2017-11-25 17:01



Il n'y a pas de concept d'interface en soi en C ++. AFAIK, les interfaces ont d'abord été introduites en Java pour contourner le manque d'héritage multiple. Ce concept s'est avéré 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 Java jargon) est une fonction virtuelle pure déclarée en utilisant 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 à partir de A, mais toute classe dérivée ne fournissant pas une implémentation de foo() sera aussi abstrait. Afin d'arrêter d'être abstrait, 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 purement virtuelles. Un équivalent d'une interface serait une classe de base abstraite sans aucune donnée avec seulement des fonctions virtuelles pures.

Et, comme Mark Ransom l'a souligné, une classe de base abstraite devrait fournir un destructeur virtuel, tout comme n'importe quelle classe de base, d'ailleurs.


43
2017-11-25 17:27



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étruit 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é.


38
2018-03-05 17:53



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éclarer un destructeur virtuel dans votre interface ou en faire un non protégé virtuel 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 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
2017-11-25 18:48



Toutes les bonnes réponses ci-dessus. Une chose supplémentaire que vous devez garder à l'esprit - vous pouvez également avoir un destructeur virtuel pur. La seule différence est que vous devez toujours l'implémenter.

Confus?


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

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

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

La raison principale pour laquelle vous voudriez faire ceci est si vous voulez fournir des méthodes d'interface, comme je l'ai fait, mais en les rendant facultatives.

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

Réimplémenter un destructeur dans la classe dérivée n'est pas du tout important - je réimplémente toujours un destructeur, virtuel ou non, dans mes classes dérivées.


9
2017-11-25 22:02



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 La sémantique des valeurs de Sean Parent et le polymorphisme basé sur les concepts (fortement recommandé, de meilleures versions de cette technique y sont expliquées) le cas suivant est étudié:

Dire que j'ai une application dans laquelle je traite avec mes formes polymorphiquement en utilisant le MyShape interface:

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 en utilisant YourShape interface:

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

Maintenant, dites que vous voulez 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 étendre mes formes comme suit:

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

D'abord, modifier mes formes pourrait ne pas être possible du tout. En outre, l'héritage multiple mène à la route du code spaghetti (imaginez un troisième projet qui utilise le TheirShape interface ... ce qui se passe si elles appellent également leur fonction de tirage my_draw?).

Mise à jour: Il y a quelques nouvelles références sur le polymorphisme basé sur l'héritage:


8
2018-06-25 13:51



Si vous utilisez le compilateur C ++ de Microsoft, vous pouvez effectuer les opérations suivantes:

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

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

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


7
2017-10-13 19:53



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

D'abord, assurez-vous que votre destructeur est également virtuel

Deuxièmement, vous pouvez vouloir hériter virtuellement (plutôt que normalement) lorsque vous implémentez, juste pour de bonnes mesures.


4
2017-11-25 17:35



Vous pouvez également considérer les classes de contrat implémentées avec le NVI (Non Virtual Interface Pattern). 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
2017-11-25 17:49