Interfejsy niejawne vs. jawne

9

Myślę, że rozumiem rzeczywiste ograniczenia polimorfizmu w czasie kompilacji i polimorfizmu w czasie wykonywania. Ale jakie są koncepcyjne różnice między jawnymi interfejsami (polimorfizm w czasie wykonywania, tj. Funkcje wirtualne i wskaźniki / referencje) a interfejsami niejawnymi (polimorfizm w czasie kompilacji, tj. Szablony) .

Uważam, że dwa obiekty, które oferują ten sam jawny interfejs, muszą być tego samego typu (lub mieć wspólnego przodka), podczas gdy dwa obiekty, które oferują ten sam niejawny interfejs, nie muszą być tego samego typu, i, z wyłączeniem niejawnego interfejs, który oferują oba, może mieć zupełnie inną funkcjonalność.

Masz jakieś przemyślenia na ten temat?

A jeśli dwa obiekty oferują ten sam niejawny interfejs, to jakie są powody (oprócz korzyści technicznych z braku potrzeby dynamicznego wysyłania z tabelą wyszukiwania funkcji wirtualnych itp.), Że te obiekty nie dziedziczą po obiekcie podstawowym, który deklaruje ten interfejs, a zatem czyniąc go jawnym interfejsem? Innym sposobem powiedzenia tego: czy możesz podać przypadek, w którym dwa obiekty, które oferują ten sam niejawny interfejs (i dlatego mogą być używane jako typy przykładowej klasy szablonów), nie powinny dziedziczyć po klasie podstawowej, która czyni ten interfejs jawnym?

Niektóre powiązane posty:


Oto przykład, aby uczynić to pytanie bardziej konkretnym:

Interfejs niejawny:

class Class1
{
public:
  void interfaceFunc();
  void otherFunc1();
};

class Class2
{
public:
  void interfaceFunc();
  void otherFunc2();
};

template <typename T>
class UseClass
{
public:
  void run(T & obj)
  {
    obj.interfaceFunc();
  }
};

Jawny interfejs:

class InterfaceClass
{
public:
  virtual void interfaceFunc() = 0;
};

class Class1 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc1();
};

class Class2 : public InterfaceClass
{
public:
  virtual void interfaceFunc();
  void otherFunc2();
};

class UseClass
{
public:
  void run(InterfaceClass & obj)
  {
    obj.interfaceFunc();
  }
};

Jeszcze bardziej dogłębny, konkretny przykład:

Niektóre problemy C ++ można rozwiązać za pomocą:

  1. klasa szablonowa, której typ szablonu zapewnia niejawny interfejs
  2. klasa bez szablonów, która przyjmuje wskaźnik klasy bazowej, który zapewnia jawny interfejs

Kod, który się nie zmienia:

class CoolClass
{
public:
  virtual void doSomethingCool() = 0;
  virtual void worthless() = 0;
};

class CoolA : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that an A would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

class CoolB : public CoolClass
{
public:
  virtual void doSomethingCool()
  { /* Do cool stuff that a B would do */ }

  virtual void worthless()
  { /* Worthless, but must be implemented */ }
};

Przypadek 1 . Klasa bez szablonów, która przyjmuje wskaźnik klasy podstawowej, który zapewnia jawny interfejs:

class CoolClassUser
{
public:  
  void useCoolClass(CoolClass * coolClass)
  { coolClass.doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Przypadek 2 . Klasa szablonowa, której typ szablonu zapewnia niejawny interfejs:

template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  CoolA * c1 = new CoolClass;
  CoolB * c2 = new CoolClass;

  CoolClassUser<CoolClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Przypadek 3 . Klasa szablonowa, której typ szablonu zapewnia niejawny interfejs (tym razem nie pochodzący z CoolClass:

class RandomClass
{
public:
  void doSomethingCool()
  { /* Do cool stuff that a RandomClass would do */ }

  // I don't have to implement worthless()! Na na na na na!
}


template <typename T>
class CoolClassUser
{
public:  
  void useCoolClass(T * coolClass)
  { coolClass->doSomethingCool(); }
};

int main()
{
  RandomClass * c1 = new RandomClass;
  RandomClass * c2 = new RandomClass;

  CoolClassUser<RandomClass> user;
  user.useCoolClass(c1);
  user.useCoolClass(c2);

  return 0;
}

Przypadek 1 wymaga, aby przekazywany obiekt był useCoolClass()dzieckiem CoolClass(i implementował worthless()). Natomiast przypadki 2 i 3 zajmą każdą klasę, która ma doSomethingCool()funkcję.

Gdyby użytkownicy kodu zawsze mieli dobre podklasy CoolClass, to przypadek 1 ma intuicyjny sens, ponieważ CoolClassUserzawsze oczekiwałby implementacji CoolClass. Załóżmy jednak, że ten kod będzie częścią frameworka API, więc nie mogę przewidzieć, czy użytkownicy będą chcieli podklasować CoolClasslub rzutować własną klasę, która ma doSomethingCool()funkcję.

Chris Morris
źródło
Może czegoś mi brakuje, ale czy ważna różnica nie została już zwięźle wskazana w pierwszym akapicie, a mianowicie, że jawne interfejsy to polimorfizm w czasie wykonywania, podczas gdy ukryte interfejsy to polimorfizm w czasie kompilacji?
Robert Harvey
2
Istnieją pewne problemy, które można rozwiązać, mając klasę lub funkcję, która przenosi wskaźnik do klasy abstrakcyjnej (która zapewnia jawny interfejs), lub klasę lub funkcję wzorcową, która wykorzystuje obiekt, który zapewnia interfejs niejawny. Oba rozwiązania działają. Kiedy chcesz skorzystać z pierwszego rozwiązania? Drugi?
Chris Morris
myślę, że większość z tych rozważań rozpada się, gdy bardziej otwierasz koncepcje. na przykład, gdzie pasowałbyś do statycznego polimorfizmu bez dziedziczenia?
Javier

Odpowiedzi:

8

Zdefiniowałeś już ważny punkt - jeden to czas wykonywania, a drugi to czas kompilacji . Rzeczywista informacja, której potrzebujesz, to konsekwencje tego wyboru.

Czas oczekiwania:

  • Pro: Interfejsy w czasie kompilacji są znacznie bardziej szczegółowe niż interfejsy w czasie wykonywania. Rozumiem przez to, że możesz używać tylko wymagań pojedynczej funkcji lub zestawu funkcji, jak je nazywasz. Nie musisz zawsze robić całego interfejsu. Wymagania są tylko i dokładnie tym, czego potrzebujesz.
  • Pro: Techniki takie jak CRTP oznaczają, że można używać domyślnych interfejsów do domyślnych implementacji takich rzeczy jak operatory. Nigdy nie można zrobić czegoś takiego z dziedziczeniem w czasie wykonywania.
  • Pro: Domniemane interfejsy są znacznie łatwiejsze do komponowania i mnożenia „dziedziczenia” niż interfejsy w czasie wykonywania i nie nakładają żadnych ograniczeń binarnych - na przykład klasy POD mogą używać interfejsów niejawnych. Nie ma potrzeby virtualdziedziczenia ani innych shenaniganów z ukrytymi interfejsami - duża zaleta.
  • Pro: Kompilator może znacznie zoptymalizować interfejsy czasu kompilacji. Ponadto dodatkowe bezpieczeństwo typu zapewnia bezpieczniejszy kod.
  • Pro: Pisanie wartości dla interfejsów wykonawczych jest niemożliwe, ponieważ nie znasz wielkości ani wyrównania końcowego obiektu. Oznacza to, że każdy przypadek, który potrzebuje / korzysta z typowania wartości, czerpie duże korzyści z szablonów.
  • Wada: Szablony są dziwką do kompilacji i używania, i mogą być kłopotliwe w przenoszeniu między kompilatorami
  • Przeciw: Szablony nie mogą być ładowane w czasie wykonywania (oczywiście), więc mają na przykład ograniczenia w wyrażaniu dynamicznych struktur danych.

Środowisko wykonawcze:

  • Pro: Ostateczny typ nie musi być ustalony do czasu uruchomienia. Oznacza to, że dziedziczenie w czasie wykonywania może znacznie łatwiej wyrażać niektóre struktury danych, jeśli szablony mogą to zrobić w ogóle. Można także eksportować typy polimorficzne w czasie wykonywania poza granice C, na przykład COM.
  • Pro: O wiele łatwiej jest określić i wdrożyć dziedziczenie w czasie wykonywania i tak naprawdę nie uzyskasz żadnego zachowania specyficznego dla kompilatora.
  • Przeciw: dziedziczenie w czasie wykonywania może być wolniejsze niż dziedziczenie w czasie kompilacji.
  • Przeciw: Dziedziczenie w czasie wykonywania traci informacje o typie.
  • Przeciw: Dziedziczenie w czasie wykonywania jest znacznie mniej elastyczne.
  • Przeciw: Wielokrotne dziedziczenie to dziwka.

Biorąc pod uwagę listę względną, jeśli nie potrzebujesz konkretnej korzyści z dziedziczenia w czasie wykonywania, nie używaj jej. Jest wolniejszy, mniej elastyczny i mniej bezpieczny niż szablony.

Edycja: Warto zauważyć, że w C ++ szczególnie istnieją zastosowania do dziedziczenia inne niż polimorfizm w czasie wykonywania. Na przykład możesz dziedziczyć typedefs, używać go do oznaczania typów lub używać CRTP. Ostatecznie jednak te techniki (i inne) naprawdę należą do „czasu kompilacji”, nawet jeśli są implementowane za pomocą class X : public Y.

DeadMG
źródło
Jeśli chodzi o pierwszego profesjonalistę na czas kompilacji, jest to związane z jednym z moich głównych pytań. Czy chciałbyś kiedyś wyjaśnić, że chcesz pracować tylko z jawnym interfejsem? To znaczy. „Nie obchodzi mnie, czy masz wszystkie funkcje, których potrzebuję, jeśli nie dziedziczysz po klasie Z, nie chcę mieć z tobą nic wspólnego”. Ponadto dziedziczenie w czasie wykonywania nie traci informacji o typie podczas używania wskaźników / referencji, prawda?
Chris Morris
@ChrisMorris: Nie. Jeśli to działa, to działa, na tym wszystkim powinieneś się przejmować. Po co zmusić kogoś do napisania dokładnie tego samego kodu w innym miejscu?
jmoreno
1
@ChrisMorris: Nie, nie zrobiłbym tego. Jeśli potrzebuję tylko X, to jest to jedna z podstawowych zasad enkapsulacji, o którą powinienem tylko prosić i dbać o X. Ponadto traci informacje o typie. Nie można na przykład przydzielić stosu obiektu tego typu. Nie można utworzyć wystąpienia szablonu z jego prawdziwym typem. Nie można wywoływać na nich funkcji składających się z szablonów.
DeadMG
Co z sytuacją, w której masz klasę Q, która korzysta z jakiejś klasy. Q przyjmuje parametr szablonu, więc zrobi to każda klasa, która udostępnia interfejs niejawny, a przynajmniej tak nam się wydaje. Okazuje się, że klasa Q oczekuje również, że klasa wewnętrzna (nazwij ją H) będzie używać interfejsu Q. Na przykład, gdy obiekt H jest zniszczony, powinien wywołać jakąś funkcję Q's. Nie można tego określić w niejawnym interfejsie. Dlatego szablony zawodzą. Mówiąc bardziej precyzyjnie, ciasno powiązany zestaw klas, który wymaga czegoś więcej niż tylko domniemanych interfejsów, wydaje się wykluczać użycie szablonów.
Chris Morris
Czas kompilacji: brzydki do debugowania, konieczność umieszczenia definicji w nagłówku
JFFIGK