Uwaga: poniżej znajduje się kod C ++ 03, ale spodziewamy się przejścia na C ++ 11 w ciągu najbliższych dwóch lat, więc musimy o tym pamiętać.
Piszę wytyczne (dla początkujących, między innymi) na temat pisania abstrakcyjnego interfejsu w C ++. Przeczytałem oba artykuły Suttera na ten temat, przeszukałem w Internecie przykłady i odpowiedzi oraz wykonałem kilka testów.
Ten kod NIE może się kompilować!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Wszystkie powyższe zachowania znajdują źródło problemu w krojeniu : Interfejs abstrakcyjny (lub klasa niebędąca liściem w hierarchii) nie powinna być konstruowalna ani kopiowalna / przypisywalna, NAWET jeśli klasa pochodna może być.
Rozwiązanie 0: podstawowy interfejs
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
To rozwiązanie jest proste i nieco naiwne: nie spełnia wszystkich naszych ograniczeń: może być konstruowane domyślnie, konstruowane i przypisywane do kopii (nie jestem nawet pewien co do konstruktorów ruchów i przypisania, ale mam jeszcze 2 lata na wymyślenie to out).
- Nie możemy zadeklarować, że destruktor jest wirtualny, ponieważ musimy go utrzymywać w linii, a niektóre z naszych kompilatorów nie będą trawiły czystych metod wirtualnych z wbudowanym pustym ciałem.
- Tak, jedynym celem tej klasy jest uczynienie implementatorów praktycznie możliwym do zniszczenia, co jest rzadkim przypadkiem.
- Nawet gdybyśmy mieli dodatkową wirtualną czystą metodę (która stanowi większość przypadków), ta klasa nadal byłaby przypisywalna do kopiowania.
Więc nie...
1. rozwiązanie: boost :: noncopyable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
To rozwiązanie jest najlepsze, ponieważ jest proste, jasne i C ++ (bez makr)
Problem polega na tym, że nadal nie działa dla tego konkretnego interfejsu, ponieważ program VirtuallyConstructible może być nadal domyślnie konstruowany .
- Nie możemy zadeklarować, że destruktor jest wirtualny, ponieważ musimy utrzymywać go w linii, a niektóre z naszych kompilatorów go nie strawią.
- Tak, jedynym celem tej klasy jest uczynienie implementatorów praktycznie możliwym do zniszczenia, co jest rzadkim przypadkiem.
Innym problemem jest to, że klasy implementujące interfejs, który nie może być kopiowany, muszą jawnie zadeklarować / zdefiniować konstruktor kopiowania i operator przypisania, jeśli potrzebują tych metod (aw naszym kodzie mamy klasy wartości, do których nasz klient może uzyskać dostęp za pośrednictwem interfejsy).
Jest to sprzeczne z regułą zerową, do której chcemy iść: jeśli domyślna implementacja jest poprawna, powinniśmy być w stanie jej użyć.
Drugie rozwiązanie: zapewnij im ochronę!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Ten wzorzec jest zgodny z ograniczeniami technicznymi, które mieliśmy (przynajmniej w kodzie użytkownika): MyInterface nie może być konstruowany domyślnie, nie może być konstruowany jako kopia i nie może być przypisany do kopii.
Ponadto nie nakłada żadnych sztucznych ograniczeń na implementację klas , które mogą swobodnie przestrzegać Reguły Zero, a nawet zadeklarować kilka konstruktorów / operatorów jako „= default” w C ++ 11/14 bez problemu.
Teraz jest to dość szczegółowe, a alternatywą byłoby użycie makra, coś takiego:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
Chroniony musi pozostać poza makrem (ponieważ nie ma zasięgu).
Poprawnie „z przestrzenią nazw” (tzn. Z nazwą firmy lub produktu) makro powinno być nieszkodliwe.
Zaletą jest to, że kod jest uwzględniany w jednym źródle, zamiast kopiowania wklejonego we wszystkich interfejsach. Jeśli konstruktor ruchów i przypisanie ruchów zostaną wyraźnie wyłączone w ten sam sposób w przyszłości, byłaby to bardzo niewielka zmiana w kodzie.
Wniosek
- Czy mam paranoję, aby chronić kod przed krojeniem interfejsów? (Wierzę, że nie jestem, ale nigdy nie wiadomo ...)
- Jakie jest najlepsze rozwiązanie spośród powyższych?
- Czy istnieje inne, lepsze rozwiązanie?
Pamiętaj, że jest to wzorzec, który posłuży jako wskazówka dla początkujących (między innymi), więc rozwiązanie takie jak: „Każdy przypadek powinien mieć swoją implementację” nie jest realnym rozwiązaniem.
Nagroda i wyniki
Nagrodę przyznałem coredump ze względu na czas poświęcony na udzielenie odpowiedzi na pytania oraz trafność odpowiedzi.
Moje rozwiązanie problemu będzie prawdopodobnie dotyczyło czegoś takiego:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... z następującym makrem:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
To realne rozwiązanie mojego problemu z następujących powodów:
- Nie można utworzyć instancji tej klasy (konstruktory są chronione)
- Tę klasę można praktycznie zniszczyć
- Ta klasa może być dziedziczona bez nakładania nadmiernych ograniczeń na klasy dziedziczące (np. Klasa dziedzicząca może być domyślnie kopiowalna)
- Użycie makra oznacza, że interfejs „deklaracja” jest łatwo rozpoznawalny (i możliwy do przeszukiwania), a jego kod jest faktoryzowany w jednym miejscu, co ułatwia modyfikację (odpowiednio prefiksowana nazwa usunie niepożądane konflikty nazw)
Pamiętaj, że inne odpowiedzi dały cenny wgląd. Dziękuję wszystkim, którzy spróbowali.
Zauważ, że myślę, że wciąż mogę nałożyć kolejną nagrodę na to pytanie i cenię sobie oświecające odpowiedzi na tyle, że jeśli je zobaczę, otworzę nagrodę tylko po to, aby przypisać ją do tej odpowiedzi.
virtual ~VirtuallyDestructible() = 0
wirtualne dziedziczenie klas interfejsów (tylko z elementami abstrakcyjnymi). Prawdopodobnie możesz pominąć ten Wirtualnie Zniszczalny.
virtual void bar() = 0;
na przykład? Zapobiegnie to inicjalizacji Twojego interfejsu.