Domanda

Come posso impostare una classe che rappresenta un'interfaccia? È solo una classe base astratta?

È stato utile?

Soluzione

Per espandere la risposta di bradtgmurray , potresti voler fare un'eccezione all'elenco dei metodi virtuali puri della tua interfaccia aggiungendo un distruttore virtuale. Ciò consente di passare la proprietà del puntatore a un'altra parte senza esporre la classe derivata concreta. Il distruttore non deve fare nulla, perché l'interfaccia non ha membri concreti. Potrebbe sembrare contraddittorio definire una funzione come virtuale e in linea, ma fidati di me - non lo è.

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

Non è necessario includere un corpo per il distruttore virtuale - si scopre che alcuni compilatori hanno problemi a ottimizzare un distruttore vuoto e si sta meglio usando il valore predefinito.

Altri suggerimenti

Crea una classe con metodi virtuali puri. Utilizzare l'interfaccia creando un'altra classe che sovrascrive quei metodi virtuali.

Un metodo virtuale puro è un metodo di classe definito virtuale e assegnato a 0.

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

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

L'intero motivo per cui hai una speciale categoria di tipi di interfaccia oltre alle classi di base astratte in C # / Java è perché C # / Java non supportano l'ereditarietà multipla.

C ++ supporta l'ereditarietà multipla, quindi non è necessario un tipo speciale. Una classe base astratta senza metodi non astratti (puro virtuale) è funzionalmente equivalente a un'interfaccia C # / Java.

Non esiste un concetto di " interfaccia " di per sé in C ++. AFAIK, le interfacce sono state introdotte per la prima volta in Java per ovviare alla mancanza di ereditarietà multipla. Questo concetto si è rivelato abbastanza utile e lo stesso effetto può essere ottenuto in C ++ usando una classe base astratta.

Una classe base astratta è una classe in cui almeno una funzione membro (metodo nel linguaggio Java) è una funzione virtuale pura dichiarata utilizzando la sintassi seguente:

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

Non è possibile creare un'istanza di una classe base astratta, i. e. non puoi dichiarare un oggetto di classe A. Puoi derivare solo le classi da A, ma qualsiasi classe derivata che non fornisce un'implementazione di foo() sarà anche astratta. Per smettere di essere astratto, una classe derivata deve fornire implementazioni per tutte le funzioni virtuali pure che eredita.

Notare che una classe base astratta può essere più di un'interfaccia, perché può contenere membri di dati e funzioni membro che non sono virtuali virtuali. Un equivalente di un'interfaccia sarebbe una classe base astratta senza dati con solo funzioni virtuali pure.

E, come ha sottolineato Mark Ransom, una classe base astratta dovrebbe fornire un distruttore virtuale, proprio come qualsiasi classe base, per quella materia.

Per quanto ho potuto testare, è molto importante aggiungere il distruttore virtuale. Sto usando oggetti creati con new e distrutti con delete.

Se non si aggiunge il distruttore virtuale nell'interfaccia, il distruttore della classe ereditata non viene chiamato.

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

Se esegui il codice precedente senza virtual ~IBase() {};, vedrai che il distruttore Tester::~Tester() non viene mai chiamato.

La mia risposta è sostanzialmente la stessa delle altre ma penso che ci siano altre due cose importanti da fare:

  1. Dichiara un distruttore virtuale nella tua interfaccia o creane uno non virtuale protetto per evitare comportamenti indefiniti se qualcuno tenta di eliminare un oggetto di tipo IDemo.

  2. Usa l'ereditarietà virtuale per evitare problemi con l'ereditarietà multipla. (Esistono più spesso ereditarietà multiple quando utilizziamo le interfacce.)

E come altre risposte:

  • Crea una classe con metodi virtuali puri.
  • Usa l'interfaccia creando un'altra classe che sovrascrive quei metodi virtuali.

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

    o

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

    E

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

In C ++ 11 puoi facilmente evitare del tutto l'ereditarietà:

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

In questo caso, un'interfaccia ha una semantica di riferimento, ovvero è necessario assicurarsi che l'oggetto sopravviva all'interfaccia (è anche possibile creare interfacce con la semantica del valore).

Questo tipo di interfacce ha i suoi pro e contro:

Infine, l'eredità è la radice di tutti i mali nella progettazione di software complessi. In Sean Parent's Value Semantics e Concepts based Polymorphism (altamente raccomandato, versioni migliori di questo qui viene spiegata la tecnica) viene studiato il seguente caso:

Supponi di avere un'applicazione in cui gestisco polimorficamente le mie forme usando l'interfaccia MyShape:

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

Nella tua applicazione, fai lo stesso con forme diverse usando l'interfaccia YourShape:

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

Ora dì che vuoi usare alcune delle forme che ho sviluppato nella tua applicazione. Concettualmente, le nostre forme hanno la stessa interfaccia, ma per far funzionare le mie forme nella tua applicazione dovrai estenderle come segue:

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

Innanzitutto, modificare le mie forme potrebbe non essere affatto possibile. Inoltre, l'eredità multipla conduce la strada verso il codice spaghetti (immagina che arrivi un terzo progetto che utilizza l'interfaccia TheirShape ... cosa succede se chiamano anche la loro funzione di disegno my_draw?).

Aggiornamento: ci sono un paio di nuovi riferimenti sul polimorfismo basato sull'ereditarietà:

Tutte le risposte valide sopra. Una cosa in più da tenere a mente: puoi anche avere un puro distruttore virtuale. L'unica differenza è che è ancora necessario implementarlo.

Confuso?


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

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

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

Il motivo principale per cui vorresti fare questo è se vuoi fornire metodi di interfaccia, come ho fatto io, ma renderli opzionali.

Per rendere la classe una classe di interfaccia richiede un metodo virtuale puro, ma tutti i metodi virtuali hanno implementazioni predefinite, quindi l'unico metodo rimasto per rendere virtuale puro è il distruttore.

Reimplementare un distruttore nella classe derivata non è affatto un grosso problema - ho sempre reimplementare un distruttore, virtuale o no, nelle mie classi derivate.

Se si utilizza il compilatore C ++ di Microsoft, è possibile effettuare le seguenti operazioni:

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

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

Mi piace questo approccio perché si traduce in un codice di interfaccia molto più piccolo e la dimensione del codice generato può essere significativamente più piccola. L'uso di novtable rimuove tutti i riferimenti al puntatore vtable in quella classe, quindi non puoi mai istanziarlo direttamente. Consulta la documentazione qui - novtable .

Una piccola aggiunta a ciò che è scritto laggiù:

Per prima cosa, assicurati che anche il tuo distruttore sia puro virtuale

In secondo luogo, potresti voler ereditare virtualmente (piuttosto che normalmente) quando lo fai, solo per buone misure.

Puoi anche prendere in considerazione le classi di contratto implementate con la NVI (Non Virtual Interface Pattern). Ad esempio:

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

Sono ancora nuovo nello sviluppo di C ++. Ho iniziato con Visual Studio (VS).

Tuttavia, sembra che nessuno abbia menzionato il __interface in VS (.NET) . Sono non molto sicuro se questo è un buon modo per dichiarare un'interfaccia. Ma sembra fornire una applicazione aggiuntiva (menzionata in il documenti ). Tale che non è necessario specificare esplicitamente virtual TYPE Method() = 0;, poiché verrà convertito automaticamente.

__interface IMyInterface {
   HRESULT CommitX();
   HRESULT get_X(BSTR* pbstrName);
};
  

Tuttavia, non lo uso perché sono preoccupato per la compatibilità della compilazione multipiattaforma, poiché è disponibile solo in .NET.

Se qualcuno ha qualcosa di interessante, per favore condividi. : -)

Grazie.

Sebbene sia vero che virtual è lo standard di fatto per definire un'interfaccia, non dimentichiamoci del classico modello a C, che viene fornito con un costruttore in C ++:

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

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

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

Questo ha il vantaggio di poter ricollegare il runtime degli eventi senza dover ricostruire la tua classe (poiché C ++ non ha una sintassi per cambiare i tipi polimorfici, questa è una soluzione alternativa per le classi camaleonte).

Punte:

  • È possibile ereditare da questo come classe di base (sono consentiti sia virtuali che non virtuali) e compilare click nel costruttore del discendente.
  • Potresti avere il puntatore a funzione come protected membro e avere un public riferimento e / o getter.
  • Come menzionato sopra, questo ti permette di cambiare l'implementazione in runtime. Quindi è anche un modo per gestire lo stato. A seconda del numero di if s rispetto alle variazioni di stato nel tuo codice, questo potrebbe essere più veloce di switch() es o std::function<> s (è prevista un'inversione di tendenza intorno a 3-4 IBase s , ma misura sempre prima.
  • Se scegli std::vector<IBase> sopra i puntatori a funzione, potresti essere in grado di gestire tutti i dati degli oggetti in <=>. Da questo punto, puoi avere schemi di valore per <=> (ad esempio, <=> funzionerà). Si noti che questo potrebbe essere più lento a seconda del compilatore e del codice STL; inoltre che le attuali implementazioni di <=> tendono ad avere un sovraccarico rispetto ai puntatori di funzione o persino alle funzioni virtuali (questo potrebbe cambiare in futuro).

Ecco la definizione di abstract class nello standard c ++

n4687

13.4.2

  

Una classe astratta è una classe che può essere utilizzata solo come classe base di un'altra classe; nessun oggetto di un astratto   classe può essere creata tranne come oggetti secondari di una classe derivata da essa. Una classe è astratta se almeno lo è   una pura funzione virtuale.

class Shape 
{
public:
   // pure virtual function providing interface framework.
   virtual int getArea() = 0;
   void setWidth(int w)
   {
      width = w;
   }
   void setHeight(int h)
   {
      height = h;
   }
protected:
    int width;
    int height;
};

class Rectangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height); 
    }
};
class Triangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height)/2; 
    }
};

int main(void)
{
     Rectangle Rect;
     Triangle  Tri;

     Rect.setWidth(5);
     Rect.setHeight(7);

     cout << "Rectangle area: " << Rect.getArea() << endl;

     Tri.setWidth(5);
     Tri.setHeight(7);

     cout << "Triangle area: " << Tri.getArea() << endl; 

     return 0;
}

Risultato: Area rettangolo: 35 Area del triangolo: 17

Abbiamo visto come una classe astratta ha definito un'interfaccia in termini di getArea () e altre due classi hanno implementato la stessa funzione ma con algoritmo diverso per calcolare l'area specifica della forma.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top