2010-06-22 14 views
7

Niedawno przełączyłem się z Java i Ruby na C++, i ku mojemu zdziwieniu muszę przekompilować pliki, które używają publicznego interfejsu, gdy zmienię sygnaturę metody metoda prywatna, ponieważ również prywatne części znajdują się w pliku .h.utrzymanie prywatnych części poza nagłówkami C++: czysta wirtualna klasa bazowa vs pimpl

Szybko wymyśliłem rozwiązanie, które, jak sądzę, jest typowe dla programisty Java: interfejsy (= czyste wirtualne klasy bazowe). Na przykład:

BananaTree.h:

class Banana; 

class BananaTree 
{ 
public: 
    virtual Banana* getBanana(std::string const& name) = 0; 

    static BananaTree* create(std::string const& name); 
}; 

BananaTree.cpp:

class BananaTreeImpl : public BananaTree 
{ 
private: 
    string name; 

    Banana* findBanana(string const& name) 
    { 
    return //obtain banana, somehow; 
    } 

public: 
    BananaTreeImpl(string name) 
    : name(name) 
    {} 

    virtual Banana* getBanana(string const& name) 
    { 
    return findBanana(name); 
    } 
}; 

BananaTree* BananaTree::create(string const& name) 
{ 
    return new BananaTreeImpl(name); 
} 

Jedynym kłopotem tutaj jest to, że nie mogę używać new i musi zamiast zadzwonić BananaTree::create(). Nie sądzę, żeby to był naprawdę problem, zwłaszcza, że ​​i tak spodziewam się dużo używać fabryk.

Teraz, mędrcy sławy C++, jednak wymyślili inne rozwiązanie, pImpl idiom. Z tym, jeśli rozumiem go poprawnie, mój kod będzie wyglądać następująco:

BananaTree.h:

class BananaTree 
{ 
public: 
    Banana* addStep(std::string const& name); 

private: 
    struct Impl; 
    shared_ptr<Impl> pimpl_; 
}; 

BananaTree.cpp:

struct BananaTree::Impl 
{ 
    string name; 

    Banana* findBanana(string const& name) 
    { 
    return //obtain banana, somehow; 
    } 

    Banana* getBanana(string const& name) 
    { 
    return findBanana(name); 
    } 

    Impl(string const& name) : name(name) {} 
} 

BananaTree::BananaTree(string const& name) 
    : pimpl_(shared_ptr<Impl>(new Impl(name))) 
{} 

Banana* BananaTree::getBanana(string const& name) 
{ 
    return pimpl_->getBanana(name); 
} 

oznaczałoby to trzeba zaimplementować dekorator styl przekazywania dla każdej publicznej metody BananaTree, w tym przypadku getBanana. To brzmi jak dodatkowy poziom złożoności i wysiłku w zakresie konserwacji, których wolę nie wymagać.

Na pytanie: co jest złego w przypadku podejścia opartego na czystej wirtualnej klasie? Dlaczego podejście pImpl jest o wiele lepiej udokumentowane? Czy coś ominąłem?

Odpowiedz

12

mogę myśleć kilka różnic:

Dzięki wirtualnej klasy bazowej złamać niektóre z semantyki ludzie oczekują od grzecznych klas C++:

spodziewałbym (lub wymagać, nawet) klasa do wystąpienia na stosie, tak:

BananaTree myTree("somename"); 

inaczej, tracę RAII, i muszę ręcznie rozpocząć śledzenie przydziałów, co prowadzi do wielu bóle głowy i wycieków pamięci.

ja też oczekiwać, że skopiowanie klasy, mogę po prostu zrobić to

BananaTree tree2 = mytree; 

o ile oczywiście, kopiowanie jest niedozwolone przez oznaczenie konstruktora kopii prywatnej, w tym przypadku, że linia nie będzie nawet skompilować.

W powyższych przypadkach oczywiście mamy problem z tym, że twoja klasa interfejsu nie ma konstruktywnych konstruktorów. Ale gdybym spróbował użyć kodu takiego jak powyższe przykłady, również borykałbym się z wieloma problemami z krojeniem. W przypadku obiektów polimorficznych na ogół wymagane jest trzymanie wskaźników lub odniesień do obiektów, aby zapobiec krojeniu. Jak w moim pierwszym punkcie, na ogół nie jest to pożądane i znacznie utrudnia zarządzanie pamięcią.

Czy czytelnik kodu zrozumie, że BananaTree zasadniczo nie działa, że ​​musi zamiast tego używać BananaTree* lub BananaTree&?

Zasadniczo Twój interfejs po prostu nie gra tak dobrze z nowoczesnym C++, gdzie wolimy

  • uniknąć wskazówek jak najwięcej, a
  • stos przydzielić wszystkie obiekty skorzystać formularz automatycznego żywotność zarządzanie.

Przy okazji, Twoja wirtualna klasa podstawowa zapomniała o wirtualnym destruktorze. To wyraźny błąd.

Wreszcie, prostszym wariantem pimpl, którego czasami używam do zmniejszenia ilości kodu standardowego, jest nadanie "zewnętrznemu" obiektowi dostępu do elementów danych obiektu wewnętrznego, aby uniknąć powielania interfejsu. Każda funkcja na zewnętrznym obiekcie bezpośrednio uzyskuje dostęp do danych, których potrzebuje bezpośrednio z obiektu wewnętrznego, lub wywołuje funkcję pomocniczą na obiekcie wewnętrznym, który nie ma odpowiednika na obiekcie zewnętrznym.

W przykładzie można usunąć funkcję i Impl::getBanana i zamiast realizować BananaTree::getBanana takiego:

potem trzeba tylko wdrożyć jedną getBanana funkcji (w klasie BananaTree), a jeden findBanana funkcja (w klasie Impl).

1

W rzeczywistości jest to tylko decyzja projektowa. I nawet jeśli podejmiesz "niewłaściwą" decyzję, nie jest to takie trudne.

pimpl służy również do zapewniania obiektów ligowych na stosie lub do prezentacji "kopii" poprzez odniesienie do tego samego obiektu implementacji.
Funkcje delegacji mogą być kłopotliwe, ale jest to niewielki problem (prosty, więc nie ma prawdziwej złożoności dodanej), szczególnie w przypadku ograniczonych klas.

Interfejsy w C++ są zazwyczaj bardziej używane w sposób podobny do strategii, w których oczekuje się, że będą w stanie wybrać implementacje, chociaż nie jest to wymagane.

Powiązane problemy