Jak mogę symulować interfejsy w C++?

Ponieważ w C++ brakuje interface funkcji Javy i C#, jaki jest preferowany sposób symulowania interfejsów w klasach C++? Zgaduję, że wielokrotne dziedziczenie klas abstrakcyjnych. Jakie są konsekwencje pod względem napowietrzności pamięci/wydajności? Czy są jakieś konwencje nazewnicze dla takich symulowanych interfejsów, jak SerializableInterface?

Author: Bill the Lizard, 2009-08-01

8 answers

Ponieważ C++ ma wiele dziedziczeń w przeciwieństwie do C# i Javy, tak, możesz utworzyć serię klas abstrakcyjnych.

Jeśli chodzi o konwencję, to zależy od ciebie; jednak lubię poprzedzić nazwy klas przez I.

class IStringNotifier
{
public:
  virtual void sendMessage(std::string &strMessage) = 0;
  virtual ~IStringNotifier() { }
};

Wydajność nie ma się czym martwić, jeśli chodzi o porównanie C# i Javy. Zasadniczo będziesz miał tylko narzut posiadania tabeli wyszukiwania dla swoich funkcji lub tabeli vtable, podobnie jak każdy rodzaj dziedziczenia z wirtualnymi metodami.

 32
Author: Brian R. Bondy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-11 23:31:05

"Jakie są implikacje pod względem napowietrzności pamięci/wydajności?"

Zazwyczaj żaden z wyjątkiem tych, które używają wirtualnych połączeń w ogóle, chociaż nic nie jest gwarantowane przez standard pod względem wydajności.

Na napowietrznej pamięci optymalizacja "pustej klasy bazowej" jawnie pozwala kompilatorowi na układanie struktur tak, że dodanie klasy bazowej, która nie ma elementów składowych danych, nie zwiększa rozmiaru obiektów. Myślę, że raczej nie będziesz miał do czynienia z kompilatorem co nie robi tego, ale mogę się mylić.

Dodanie pierwszej wirtualnej funkcji członka do klasy zwykle zwiększa obiekty o rozmiar wskaźnika, w porównaniu z pominięciem funkcji wirtualnego członka. Dodawanie kolejnych funkcji wirtualnego członka nie robi żadnej dodatkowej różnicy. Dodawanie wirtualnych klas bazowych może sprawić dodatkową różnicę, ale nie potrzebujesz tego do tego, o czym mówisz.

Dodanie wielu klas bazowych z wirtualnymi funkcjami członkowskimi prawdopodobnie oznacza, że w efekt pustą optymalizację klasy bazowej uzyskuje się tylko raz, ponieważ w typowej implementacji obiekt będzie potrzebował wielu wskaźników vtable. Więc jeśli potrzebujesz wielu interfejsów w każdej klasie, możesz dodawać do rozmiaru obiektów.

Jeśli chodzi o wydajność, wirtualne wywołanie funkcji ma trochę więcej narzutu niż nie-wirtualne wywołanie funkcji, a co ważniejsze, można założyć, że ogólnie (zawsze?) nie będzie inlined. Dodanie pustej klasy bazowej zwykle nie dodaje żadnego kodu do konstrukcja lub zniszczenie, ponieważ pusty konstruktor bazowy i destruktor mogą być wbudowane w Pochodny kod konstruktora / destruktora klasy.

Istnieją sztuczki, których możesz użyć, aby uniknąć funkcji wirtualnych, jeśli chcesz jawnych interfejsów, ale nie potrzebujesz dynamicznego polimorfizmu. Jeśli jednak próbujesz emulować Javę, zakładam, że tak nie jest.

Przykładowy kod:

#include <iostream>

// A is an interface
struct A {
    virtual ~A() {};
    virtual int a(int) = 0;
};

// B is an interface
struct B {
    virtual ~B() {};
    virtual int b(int) = 0;
};

// C has no interfaces, but does have a virtual member function
struct C {
    ~C() {}
    int c;
    virtual int getc(int) { return c; }
};

// D has one interface
struct D : public A {
    ~D() {}
    int d;
    int a(int) { return d; }
};

// E has two interfaces
struct E : public A, public B{
    ~E() {}
    int e;
    int a(int) { return e; }
    int b(int) { return e; }
};

int main() {
    E e; D d; C c;
    std::cout << "A : " << sizeof(A) << "\n";
    std::cout << "B : " << sizeof(B) << "\n";
    std::cout << "C : " << sizeof(C) << "\n";
    std::cout << "D : " << sizeof(D) << "\n";
    std::cout << "E : " << sizeof(E) << "\n";
}

Wyjście (GCC na 32-bitowej platformie):

A : 4
B : 4
C : 8
D : 8
E : 12
 7
Author: Steve Jessop,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-01 15:22:32

Naprawdę nie ma potrzeby 'symulowania' czegokolwiek, ponieważ nie jest tak, że C++ brakuje czegokolwiek, co Java może zrobić z interfejsami.

[12]}ze wskaźnika widoku C++, Java tworzy "sztuczny" dysk pomiędzy interface a class. An interface to po prostu class, których wszystkie metody są abstrakcyjne i nie mogą zawierać żadnych elementów danych.

Java wprowadza to ograniczenie, ponieważ nie pozwala na nieograniczone dziedziczenie wielokrotne, ale pozwala class na implement wielokrotność interfejsy.

W C++, a class jest class i An interface jest class. extends jest osiągane przez dziedziczenie publiczne i {[11] } jest również osiągane przez dziedziczenie publiczne.

Dziedziczenie z wielu klas innych niż interfejsy może powodować dodatkowe komplikacje, ale może być przydatne w niektórych sytuacjach. Jeśli ograniczysz się tylko do dziedziczenia klas z co najwyżej jednej klasy bez interfejsu i dowolnej liczby całkowicie abstrakcyjnych klas, nie napotkasz żadnych innych trudności niż w Javie (z wyjątkiem innych różnic C++ / Java).

Jeśli chodzi o pamięć i koszty ogólne, jeśli ponownie tworzysz hierarchię klas w stylu Java, prawdopodobnie zapłaciłeś już koszt funkcji wirtualnych za swoje klasy. Biorąc pod uwagę, że i tak używasz różnych środowisk wykonawczych, nie będzie żadnej fundamentalnej różnicy w kosztach między tymi dwoma modelami dziedziczenia.

 6
Author: CB Bailey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-01 16:58:54

Interfejsy w C++ są klasami, które mają tylko czyste funkcje wirtualne. Np.:

class ISerializable
{
public:
    virtual ~ISerializable() = 0;
    virtual void  serialize( stream& target ) = 0;
};

To nie jest symulowany interfejs, jest to interfejs podobny do tego w Javie, ale nie niesie wad.

Np. możesz dodawać metody i członków bez negatywnych konsekwencji:

class ISerializable
{
public:
    virtual ~ISerializable() = 0;
    virtual void  serialize( stream& target ) = 0;
protected:
    void  serialize_atomic( int i, stream& t );
    bool  serialized;
};

Do konwencji nazewnictwa ... w języku C++ nie są zdefiniowane żadne rzeczywiste konwencje nazewnictwa. Więc wybierz ten w swoim środowisku.

Overhead to 1 statyczna tabela i w klasy pochodne, które nie miały jeszcze funkcji wirtualnych, wskaźnik do statycznej tabeli.

 4
Author: Christopher,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-01 15:25:01

W C++ możemy pójść dalej niż zwykłe interfejsy Java & co. Możemy dodać wyraźne umowy (jak w Design by Contract ) ze wzorem NVI.

struct Contract1 : noncopyable
{
    virtual ~Contract1();
    Res f(Param p) {
        assert(f_precondition(p) && "C1::f precondition failed");
        const Res r = do_f(p);
        assert(f_postcondition(p,r) && "C1::f postcondition failed");
        return r;
    }
private:
    virtual Res do_f(Param p) = 0;
};

struct Concrete : virtual Contract1, virtual Contract2
{
    ...
};
 2
Author: Luc Hermitte,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-03 11:47:35

Jeśli nie używasz dziedziczenia Wirtualnego, narzut nie powinien być gorszy niż zwykłe dziedziczenie z co najmniej jedną wirtualną funkcją. Każda abstrakcyjna klasa dziedziczona z doda wskaźnik do każdego obiektu.

Jednakże, jeśli zrobisz coś takiego jak Optymalizacja pustej klasy bazowej, możesz to zminimalizować:

struct A
{
    void func1() = 0;
};

struct B: A
{
    void func2() = 0;
};

struct C: B
{
    int i;
};

Rozmiar C będzie dwa słowa.

 1
Author: keraba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-01 15:07:20

Przy okazji MSVC 2008 ma słowo kluczowe __interface .

A Visual C++ interface can be defined as follows: 

 - Can inherit from zero or more base
   interfaces.
 - Cannot inherit from a base class.
 - Can only contain public, pure virtual
   methods.
 - Cannot contain constructors,
   destructors, or operators.
 - Cannot contain static methods.
 - Cannot contain data members;
   properties are allowed.

Ta funkcja jest specyficzna dla Microsoft. Uwaga: __interface nie ma Wirtualnego destruktora, który jest wymagany, jeśli usuniesz obiekty za pomocą wskaźników interfejsu.

 1
Author: Sergey Podobry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-12 06:27:03

Nie ma dobrego sposobu na zaimplementowanie interfejsu w sposób, o który prosisz. Problem z podejściem takim jak zupełnie abstrakcyjna klasa bazowa ISerializable polega na tym, że C++ implementuje dziedziczenie wielokrotne. Rozważmy następujące:

class Base
{
};
class ISerializable
{
  public:
    virtual string toSerial() = 0;
    virtual void fromSerial(const string& s) = 0;
};

class Subclass : public Base, public ISerializable
{
};

void someFunc(fstream& out, const ISerializable& o)
{
    out << o.toSerial();
}

Oczywiście intencją funkcji toSerial() jest serializacja wszystkich członków podklasy, w tym tych, które dziedziczy z klasy bazowej. Problem polega na tym, że nie ma ścieżki z ISerializable do bazy. Zobacz też graficznie jeśli wykonasz:

void fn(Base& b)
{
    cout << (void*)&b << endl;
}
void fn(ISerializable& i)
{
    cout << (void*)&i << endl;
}

void someFunc(Subclass& s)
{
    fn(s);
    fn(s);
}

Wartość wyjściowa pierwszego wywołania nie jest taka sama jak wartość wyjściowa drugiego wywołania. Mimo, że w obu przypadkach przekazywane jest odwołanie do s, kompilator dostosowuje adres przekazywany tak, aby pasował do właściwego typu klasy bazowej.

 0
Author: Randy Davis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-11 00:51:04