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:
- https://stackoverflow.com/a/7264550/635125
- https://stackoverflow.com/a/7264689/635125
- https://stackoverflow.com/a/8009872/635125
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ą:
- klasa szablonowa, której typ szablonu zapewnia niejawny interfejs
- 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ż CoolClassUser
zawsze 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ć CoolClass
lub rzutować własną klasę, która ma doSomethingCool()
funkcję.
źródło
Odpowiedzi:
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:
virtual
dziedziczenia ani innych shenaniganów z ukrytymi interfejsami - duża zaleta.Środowisko wykonawcze:
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
.źródło