Jak skonfigurować klasę reprezentującą interfejs? Czy to tylko abstrakcyjna klasa podstawowa?
Jak skonfigurować klasę reprezentującą interfejs? Czy to tylko abstrakcyjna klasa podstawowa?
Odpowiedzi:
Aby rozwinąć odpowiedź bradtgmurray , możesz zrobić jeden wyjątek od listy czysto wirtualnych metod interfejsu, dodając wirtualny destruktor. Umożliwia to przekazanie własności wskaźnika innej stronie bez ujawnienia konkretnej klasy pochodnej. Destruktor nie musi nic robić, ponieważ interfejs nie ma żadnych konkretnych elementów. Definiowanie funkcji jako wirtualnej i wbudowanej może wydawać się sprzeczne, ale zaufaj mi - nie jest.
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
}
};
Nie musisz dołączać ciała do wirtualnego destruktora - okazuje się, że niektóre kompilatory mają problemy z optymalizacją pustego destruktora i lepiej jest użyć domyślnego.
=0
) destruktora z ciałem. Zaletą jest to, że kompilator może teoretycznie zobaczyć, że vtable nie ma teraz poprawnych elementów, i całkowicie go odrzucić. W przypadku wirtualnego destruktora z ciałem wspomniany destruktor można nazwać (wirtualnie) np. W środku konstrukcji za pomocą this
wskaźnika (gdy obiekt jest nadal Parent
typu), a zatem kompilator musi dostarczyć poprawną tabelę vt. Więc jeśli nie wywołujesz jawnie wirtualnych destruktorów this
podczas budowy :) możesz zaoszczędzić na rozmiarze kodu.
override
słowo kluczowe, aby umożliwić sprawdzanie argumentów czasu kompilacji i zwracanie typu wartości. Na przykład w deklaracji Childvirtual void OverrideMe() override;
Utwórz klasę za pomocą czysto wirtualnych metod. Użyj interfejsu, tworząc inną klasę, która zastąpi te wirtualne metody.
Metoda czysto wirtualna to metoda klasy zdefiniowana jako wirtualna i przypisana do 0.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
override
w C ++ 11
Cały powód, dla którego oprócz abstrakcyjnych klas bazowych w C # / Java masz specjalną kategorię typów interfejsów, to fakt, że C # / Java nie obsługuje wielokrotnego dziedziczenia.
C ++ obsługuje wielokrotne dziedziczenie, więc specjalny typ nie jest potrzebny. Abstrakcyjna klasa bazowa bez metod nieabstrakcyjnych (czysto wirtualnych) jest funkcjonalnie równoważna interfejsowi C # / Java.
Thread
instancją. Wielokrotne dziedziczenie może być złym projektem, a także kompozycją. Wszystko zależy od przypadku.
W C ++ nie ma pojęcia „interfejsu” jako takiego. AFAIK, interfejsy zostały po raz pierwszy wprowadzone w Javie, aby obejść brak wielokrotnego dziedziczenia. Ta koncepcja okazała się całkiem użyteczna, a ten sam efekt można osiągnąć w C ++, używając abstrakcyjnej klasy bazowej.
Abstrakcyjna klasa podstawowa to klasa, w której co najmniej jedna funkcja składowa (metoda w języku Java Lingo) jest czystą funkcją wirtualną zadeklarowaną przy użyciu następującej składni:
class A
{
virtual void foo() = 0;
};
Nie można utworzyć instancji abstrakcyjnej klasy bazowej, tzn. Nie można zadeklarować obiektu klasy A. Klasy można wywodzić tylko z A, ale każda klasa pochodna, która nie zapewnia implementacji foo()
, również będzie abstrakcyjna. Aby przestać być abstrakcyjnym, klasa pochodna musi zapewniać implementacje dla wszystkich dziedziczonych funkcji czysto wirtualnych.
Należy pamiętać, że abstrakcyjna klasa podstawowa może być czymś więcej niż interfejsem, ponieważ może zawierać elementy danych i funkcje elementów, które nie są czysto wirtualne. Odpowiednikiem interfejsu byłaby abstrakcyjna klasa bazowa bez danych posiadająca wyłącznie funkcje wirtualne.
I, jak zauważył Mark Ransom, abstrakcyjna klasa podstawowa powinna zapewniać wirtualny destruktor, podobnie jak każda inna klasa podstawowa.
O ile mogłem przetestować, bardzo ważne jest dodanie wirtualnego destruktora. Używam obiektów utworzonych new
i zniszczonych przy pomocy delete
.
Jeśli nie dodasz wirtualnego destruktora w interfejsie, wówczas destruktor odziedziczonej klasy nie zostanie wywołany.
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;
}
Jeśli uruchomisz poprzedni kod bez virtual ~IBase() {};
, zobaczysz, że destruktor Tester::~Tester()
nigdy nie jest wywoływany.
Moja odpowiedź jest w zasadzie taka sama jak inne, ale myślę, że są dwie inne ważne rzeczy do zrobienia:
Zadeklaruj wirtualny destruktor w swoim interfejsie lub stwórz chroniony nie-wirtualny, aby uniknąć niezdefiniowanych zachowań, jeśli ktoś spróbuje usunąć obiekt typu IDemo
.
Użyj wirtualnego dziedziczenia, aby uniknąć problemów z wielokrotnym dziedziczeniem. (Kiedy korzystamy z interfejsów, częściej występuje wielokrotne dziedziczenie).
I podobnie jak inne odpowiedzi:
Użyj interfejsu, tworząc inną klasę, która zastąpi te wirtualne metody.
class IDemo
{
public:
virtual void OverrideMe() = 0;
virtual ~IDemo() {}
}
Lub
class IDemo
{
public:
virtual void OverrideMe() = 0;
protected:
~IDemo() {}
}
I
class Child : virtual public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
}
W C ++ 11 można łatwo całkowicie uniknąć dziedziczenia:
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;
// ...
};
W takim przypadku interfejs ma semantykę odniesienia, tzn. Musisz upewnić się, że obiekt przeżywa interfejs (możliwe jest także tworzenie interfejsów z semantyką wartości).
Tego rodzaju interfejsy mają swoje zalety i wady:
Wreszcie, dziedziczenie jest źródłem wszelkiego zła w złożonym projektowaniu oprogramowania. W Sean Parent's Value Semantyka i polimorfizm oparty na pojęciach (wysoce zalecane, wyjaśniono tam lepsze wersje tej techniki) badany jest następujący przypadek:
Załóżmy, że mam aplikację, w której radzę sobie z kształtami polimorficznie za pomocą MyShape
interfejsu:
struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle
W swojej aplikacji robisz to samo z różnymi kształtami za pomocą YourShape
interfejsu:
struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...
Teraz powiedz, że chcesz użyć niektórych kształtów, które opracowałem w Twojej aplikacji. Koncepcyjnie, nasze kształty mają ten sam interfejs, ale aby moje kształty działały w Twojej aplikacji, musisz rozszerzyć moje kształty w następujący sposób:
struct Circle : MyShape, YourShape {
void my_draw() { /*stays the same*/ };
void your_draw() { my_draw(); }
};
Po pierwsze, modyfikowanie moich kształtów może być w ogóle niemożliwe. Co więcej, wielokrotne dziedziczenie prowadzi do kodu spaghetti (wyobraź sobie, że pojawia się trzeci projekt korzystający z TheirShape
interfejsu ... co się stanie, jeśli wywołają również funkcję rysowania my_draw
?).
Aktualizacja: Istnieje kilka nowych odniesień na temat polimorfizmu opartego na braku dziedziczenia:
Circle
klasa to kiepski projekt. W Adapter
takich przypadkach powinieneś użyć wzoru. Przepraszam, jeśli zabrzmi to trochę ostro, ale spróbuj użyć biblioteki z prawdziwego życia, tak jak Qt
przed osądzeniem o spadku. Dziedziczenie znacznie ułatwia życie.
Adapter
wzoru? Chcę zobaczyć jego zalety.
Square
nie ma? Czy znasz? Właśnie dlatego jest oderwany od rzeczywistości. W rzeczywistości, jeśli zdecydujesz się na bibliotekę „MyShape”, możesz dostosować się do jej interfejsu od samego początku. W przykładzie kształtów jest wiele bzdur (jednym z nich jest to, że masz dwie Circle
struktury), ale adapter wyglądałby mniej więcej
Wszystkie dobre odpowiedzi powyżej. Jedną dodatkową rzeczą, o której powinieneś pamiętać - możesz też mieć czysty wirtualny destruktor. Jedyną różnicą jest to, że nadal musisz ją wdrożyć.
Zmieszany?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
Głównym powodem, dla którego chcesz to zrobić, jest udostępnienie metod interfejsu, tak jak ja, ale zastąpienie ich opcjonalnym.
Aby klasa stała się klasą interfejsu, wymaga czystej metody wirtualnej, ale wszystkie metody wirtualne mają domyślne implementacje, więc jedyną metodą na stworzenie czystej wirtualnej jest destruktor.
Reimplementacja destruktora w klasie pochodnej nie jest niczym wielkim - zawsze reimplementuję destruktor, wirtualny lub nie, w moich klasach pochodnych.
Jeśli używasz kompilatora C ++ firmy Microsoft, możesz wykonać następujące czynności:
struct __declspec(novtable) IFoo
{
virtual void Bar() = 0;
};
class Child : public IFoo
{
public:
virtual void Bar() override { /* Do Something */ }
}
Podoba mi się to podejście, ponieważ powoduje znacznie mniejszy kod interfejsu, a generowany rozmiar kodu może być znacznie mniejszy. Zastosowanie novtable usuwa wszystkie odwołania do wskaźnika vtable w tej klasie, więc nigdy nie można bezpośrednio utworzyć jego instancji. Zobacz dokumentację tutaj - nowatorską .
novtable
standarduvirtual void Bar() = 0;
= 0;
które dodałem). Przeczytaj dokumentację, jeśli jej nie rozumiesz.
= 0;
i zakładałem, że był to po prostu niestandardowy sposób robienia dokładnie tego samego.
Mały dodatek do tego, co tam jest napisane:
Po pierwsze, upewnij się, że twój destruktor jest również czysto wirtualny
Po drugie, możesz chcieć odziedziczyć wirtualnie (zamiast normalnie) po wdrożeniu, tylko dla dobrych środków.
Możesz także rozważyć klasy kontraktów zaimplementowane przy użyciu NVI (Non Virtual Interface Pattern). Na przykład:
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.
};
Nadal jestem nowy w rozwoju C ++. Zacząłem od Visual Studio (VS).
Jednak wydaje się, że nikt nie wspomniał __interface
w VS (.NET) . Ja nie bardzo pewna, czy jest to dobry sposób, aby zadeklarować interfejs. Wydaje się jednak, że zapewnia dodatkowe egzekwowanie (wspomniane w dokumentach ). Tak, abyś nie musiał jawnie określać virtual TYPE Method() = 0;
, ponieważ zostanie on automatycznie przekonwertowany.
__interface IMyInterface {
HRESULT CommitX();
HRESULT get_X(BSTR* pbstrName);
};
Jednak nie używam go, ponieważ martwię się o kompatybilność kompilacji między platformami, ponieważ jest ona dostępna tylko w .NET.
Jeśli ktoś ma coś ciekawego na ten temat, prosimy o udostępnienie. :-)
Dzięki.
Chociaż prawdą virtual
jest, że jest to de facto standard definiujący interfejs, nie zapominajmy jednak o klasycznym wzorcu podobnym do C, który jest dostarczany z konstruktorem w C ++:
struct IButton
{
void (*click)(); // might be std::function(void()) if you prefer
IButton( void (*click_)() )
: click(click_)
{
}
};
// call as:
// (button.*click)();
Ma to tę zaletę, że można ponownie powiązać środowisko wykonawcze zdarzeń bez konieczności ponownego tworzenia klasy (ponieważ C ++ nie ma składni do zmiany typów polimorficznych, jest to obejście dla klas kameleonów).
Wskazówki:
click
konstruktora potomka.protected
członkowski i mieć public
odniesienie i / lub getter.if
zmian stanu w kodzie w porównaniu do stanu s, może to być szybsze niż switch()
es lub if
s (zmiana jest oczekiwana około 3-4 if
s, ale zawsze mierz najpierw.std::function<>
na wskaźniki funkcji, może być w stanie zarządzać wszystkimi danych obiektu wewnątrz IBase
. Od tego momentu możesz mieć schematy wartości IBase
(np. std::vector<IBase>
Będą działać). Zauważ, że może to być wolniejsze w zależności od twojego kompilatora i kodu STL; także, że obecne implementacje std::function<>
mają zwykle narzut, w porównaniu do wskaźników funkcji, a nawet funkcji wirtualnych (może się to zmienić w przyszłości).Oto definicja abstract class
standardu c ++
n4687
13.4.2
Klasa abstrakcyjna to klasa, której można używać tylko jako klasę podstawową innej klasy; nie można tworzyć żadnych obiektów klasy abstrakcyjnej, z wyjątkiem podobiektów pochodnej klasy. Klasa jest abstrakcyjna, jeśli ma co najmniej jedną czystą funkcję wirtualną.
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;
}
Wynik: obszar prostokąta: 35 obszar trójkąta: 17
Widzieliśmy, jak klasa abstrakcyjna zdefiniowała interfejs w kategoriach getArea (), a dwie inne klasy zaimplementowały tę samą funkcję, ale z innym algorytmem do obliczania obszaru specyficznego dla kształtu.