Beste manier om een ​​interface in C++11 te declareren

Zoals we allemaal weten, hebben sommige talen het idee van interfaces. Dit is Java:

public interface Testable {
  void test();
}

Hoe kan ik dit in C++ (of C++11) op de meest compacte manier en met weinig coderuis bereiken? Ik zou een oplossing waarderen die geen aparte definitie nodig heeft (laat de kop voldoende zijn). Dit is een heel eenvoudige benadering die zelfs ik een bug vind 😉

class Testable {
public:
  virtual void test() = 0;
protected:
  Testable();
  Testable(const Testable& that);
  Testable& operator= (const Testable& that);
  virtual ~Testable();
}

Dit is nog maar het begin… en al langer dan ik zou willen. Hoe het te verbeteren? Misschien is er ergens in de std-naamruimte een basisklasse die speciaal hiervoor is gemaakt?


Antwoord 1, autoriteit 100%

Voor dynamisch (runtime) polymorfisme raad ik het gebruik van het Non-Virtual-Interface(NVI)-idioom aan. Dit patroon houdt de interface niet-virtueel en openbaar, de destructor virtueel en openbaar, en de implementatie puur virtueel en privé

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

Het leuke van dynamisch polymorfisme is dat je -tijdens runtime- elke afgeleide klasse kunt doorgeven waar een pointer of verwijzing naar de interface-basisklasse wordt verwacht. Het runtime-systeem zal de this-aanwijzer automatisch downcasten van het statische basistype naar het dynamische afgeleide type en de bijbehorende implementatie aanroepen (meestal gebeurt dit via tabellen met verwijzingen naar virtuele functies).

Voor statisch (compile-time polymorfisme) raad ik aan om het Curiously Recurring Template Pattern(CRTP) te gebruiken. Dit is aanzienlijk ingewikkelder omdat het automatisch downcasten van basis naar afgeleid van dynamisch polymporphism moet worden gedaan met static_cast. Deze statische casting kan worden gedefinieerd in een hulpklasse waarvan elke statische interface is afgeleid

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

Vervolgens definieert u een statische interface als volgt:

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

en tot slot maak je een implementatie die voortkomt uit de interface met zichzelf als parameter

class StaticImplementation
:
    public StaticInterface< StaticImplementation > 
{
private:
    // implementation
    friend class StaticInterface< StaticImplementation > ;
    void do_fun() { /* your implementation here */ }
};

Hierdoor kunt u nog steeds meerdere implementaties van dezelfde interface hebben, maar u moet tijdens het compileren weten welke implementatie u aanroept.

Dus wanneer gebruik je welk formulier?Met beide formulieren kun je een gemeenschappelijke interface hergebruiken en pre-/postconditietesten in de interfaceklasse injecteren. Het voordeel van dynamisch polymorfisme is dat je runtime-flexibiliteit hebt, maar daarvoor betaal je in virtuele functieaanroepen (meestal een aanroep via een functieaanwijzer, met weinig mogelijkheid voor inlining). Statisch polymporhisme is daar de spiegel van: geen virtuele functie-aanroep overhead, maar het nadeel is dat je meer boilerplate-code nodig hebt en dat je moet weten wat je aanroept tijdens het compileren. Eigenlijk een afweging tussen efficiëntie en flexibiliteit.

OPMERKING:voor compile-time polymporhism, kunt u ook sjabloonparameters gebruiken. Het verschil tussen een statische interface via het CRTP-idioom en gewone sjabloonparameters is dat de interface van het CRTP-type expliciet is (gebaseerd op lidfuncties) en dat de sjablooninterface impliciet is (gebaseerd op geldige uitdrukkingen)


Antwoord 2, autoriteit 89%

Hoe zit het met:

class Testable
{
public:
    virtual ~Testable() { }
    virtual void test() = 0;
}

In C++ heeft dit geen gevolgen voor de kopieerbaarheid van onderliggende klassen. Dit alles zegt dat het kind testmoet implementeren (wat precies is wat je wilt voor een interface). Je kunt deze klasse niet instantiëren, dus je hoeft je geen zorgen te maken over impliciete constructors, omdat ze nooit rechtstreeks kunnen worden aangeroepen als het bovenliggende interfacetype.

Als je wilt dat onderliggende klassen een destructor implementeren, kun je dat ook puur maken (maar je moet het nog steeds in de interface implementeren).

Houd er rekening mee dat als je geen polymorfe vernietiging nodig hebt, je ervoor kunt kiezen om je destructor niet-virtueel te beschermen.


Antwoord 3, autoriteit 52%

Volgens Scott Meyers (Effective Modern C++): bij het declareren van interface (of polymorfe basisklasse) heb je een virtuele destructor nodig voor de juiste resultaten van bewerkingen zoals deleteof typeidop een afgeleid klasseobject dat toegankelijk is via een aanwijzer of referentie van een basisklasse.

virtual ~Testable() = default;

Een door de gebruiker opgegeven destructor onderdrukt echter het genereren van de
verplaatsingsbewerkingen, dus om verplaatsingsbewerkingen te ondersteunen moet u het volgende toevoegen:

Testable(Testable&&) = default; 
Testable& operator=(Testable&&) = default;

Als u de verplaatsingsbewerkingen declareert, worden kopieerbewerkingen uitgeschakeld en hebt u ook het volgende nodig:

Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;

En het eindresultaat is:

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

Nog een interessant artikel hier: De regel van nul in C++


Antwoord 4, autoriteit 26%

Door het woord classte vervangen door struct, zijn alle methoden standaard openbaar en kun je een regel opslaan.

Het is niet nodig om de constructor te beveiligen, aangezien je toch geen klasse kunt instantiëren met pure virtuele methoden. Dit geldt ook voor de kopie-constructor. De door de compiler gegenereerde standaardconstructor is leeg omdat u geen gegevensleden heeft en is volledig voldoende voor uw afgeleide klassen.

Je hebt gelijk als je je zorgen maakt over de operator =, aangezien de door de compiler gegenereerde operator zeker het verkeerde zal doen. In de praktijk maakt niemand zich er ooit zorgen over, omdat het kopiëren van het ene interface-object naar het andere nooit zin heeft; het is geen fout die vaak voorkomt.

Destructors voor een overerfbare klasse moeten altijdopenbaar en virtueel zijn, of beschermd en niet-virtueel. Ik geef in dit geval de voorkeur aan openbaar en virtueel.

Het eindresultaat is slechts één regel langer dan het Java-equivalent:

struct Testable {
    virtual void test() = 0;
    virtual ~Testable();
};

Antwoord 5, autoriteit 15%

Houd er rekening mee dat de “regel van drie” niet nodig is als je geen aanwijzers, handvatten en/of alle gegevensleden van de klasse hun eigen destructors hebt die elke opschoning zullen beheren. Ook in het geval van een virtuele basisklasse, omdat de basisklasse nooit direct kan worden geïnstantieerd, is het niet nodig om een ​​constructor te declareren als je alleen een interface wilt definiëren die geen gegevensleden heeft … de compiler standaardwaarden zijn prima. Het enige item dat je zou moeten houden is de virtuele destructor als je van plan bent om deleteaan te roepen op een aanwijzer van het interface-type. Dus in werkelijkheid kan uw interface zo eenvoudig zijn als:

class Testable 
{
    public:
        virtual void test() = 0;  
        virtual ~Testable();
}

Other episodes