Mam hierarchię klas, dla której chciałbym oddzielić interfejs od implementacji. Moim rozwiązaniem jest posiadanie dwóch hierarchii: hierarchii klas uchwytów dla interfejsu i niepublicznej hierarchii klas dla implementacji. Podstawowa klasa uchwytu ma wskaźnik do implementacji, który pochodne klasy uchwytów rzutują na wskaźnik typu pochodnego (patrz funkcja getPimpl()
).
Oto szkic mojego rozwiązania dla klasy bazowej z dwiema klasami pochodnymi. Czy jest lepsze rozwiązanie?
Plik „Base.h”:
#include <memory>
class Base {
protected:
class Impl;
std::shared_ptr<Impl> pImpl;
Base(Impl* pImpl) : pImpl{pImpl} {};
...
};
class Derived_1 final : public Base {
protected:
class Impl;
inline Derived_1* getPimpl() const noexcept {
return reinterpret_cast<Impl*>(pImpl.get());
}
public:
Derived_1(...);
void func_1(...) const;
...
};
class Derived_2 final : public Base {
protected:
class Impl;
inline Derived_2* getPimpl() const noexcept {
return reinterpret_cast<Impl*>(pImpl.get());
}
public:
Derived_2(...);
void func_2(...) const;
...
};
Plik „Base.cpp”:
class Base::Impl {
public:
Impl(...) {...}
...
};
class Derived_1::Impl final : public Base::Impl {
public:
Impl(...) : Base::Impl(...) {...}
void func_1(...) {...}
...
};
class Derived_2::Impl final : public Base::Impl {
public:
Impl(...) : Base::Impl(...) {...}
void func_2(...) {...}
...
};
Derived_1::Derived_1(...) : Base(new Derived_1::Impl(...)) {...}
Derived_1::func_1(...) const { getPimpl()->func_1(...); }
Derived_2::Derived_2(...) : Base(new Derived_2::Impl(...)) {...}
Derived_2::func_2(...) const { getPimpl()->func_2(...); }
Base
, wystarczająca może być normalna abstrakcyjna klasa bazowa („interfejs”) i konkretne implementacje bez pimpl.Odpowiedzi:
Myślę, że
Derived_1::Impl
czerpanie z tego jest kiepską strategiąBase::Impl
.Głównym celem użycia idiomu Pimpl jest ukrycie szczegółów implementacyjnych klasy. Pozwalając
Derived_1::Impl
pochodzićBase::Impl
, pokonałeś ten cel. Teraz nie tylkoBase
zależy odBase::Impl
wdrożenia, aleDerived_1
także od wdrożeniaBase::Impl
.To zależy od tego, jakie kompromisy są dla Ciebie dopuszczalne.
Rozwiązanie 1
Uczyń
Impl
zajęcia całkowicie niezależnymi. Oznacza to, że będą dwa wskaźniki doImpl
klas - jeden w,Base
a drugi wDerived_N
.Rozwiązanie 2
Ujawnij klasy tylko jako uchwyty. W ogóle nie ujawniaj definicji klas i implementacji.
Plik nagłówka publicznego:
Oto szybka implementacja
Plusy i minusy
Przy pierwszym podejściu możesz konstruować
Derived
klasy na stosie. Przy drugim podejściu nie jest to opcja.Przy pierwszym podejściu ponosisz koszt dwóch dynamicznych alokacji i dezalokacji na budowę i zniszczenie
Derived
stosu. Jeśli konstruujesz i niszczyszDerived
obiekt ze stosu, ponosisz koszty jeszcze jednego przydziału i zwolnienia. Przy drugim podejściu ponosisz tylko koszt jednej dynamicznej alokacji i jednej dezalokacji dla każdego obiektu.Przy pierwszym podejściu masz możliwość korzystania z
virtual
funkcji członkaBase
. Przy drugim podejściu nie jest to opcja.Moja sugestia
Wybrałbym pierwsze rozwiązanie, aby móc korzystać z hierarchii klas i
virtual
funkcjiBase
składowych, nawet jeśli jest to nieco droższe.źródło
Jedyne ulepszenie, które widzę tutaj, to pozwolenie konkretnym klasom zdefiniować pole implementacji. Jeśli abstrakcyjne klasy podstawowe tego potrzebują, mogą zdefiniować właściwość abstrakcyjną, którą można łatwo zaimplementować w konkretnych klasach:
Base.h
Base.cpp
Wydaje mi się to bezpieczniejsze. Jeśli masz duże drzewo, możesz również przedstawić je
virtual std::shared_ptr<Impl1> getImpl1() =0
na środku drzewa.źródło