Zrozumienie / wymagania dotyczące polimorfizmu
Aby zrozumieć polimorfizm - jak termin jest używany w informatyce - warto zacząć od prostego testu i zdefiniowania go. Rozważać:
Type1 x;
Type2 y;
f(x);
f(y);
Tutaj f()
ma wykonać jakąś operację i otrzymuje wartości x
i y
jako dane wejściowe.
Aby wykazać polimorfizm, f()
musi być w stanie operować wartościami co najmniej dwóch różnych typów (np. int
I double
), znajdować i wykonywać odrębny kod odpowiedni dla typu.
Mechanizmy C ++ dla polimorfizmu
Jawny polimorfizm określony przez programistę
Możesz napisać w f()
taki sposób, aby działał na wielu typach w dowolny z następujących sposobów:
Przetwarzanie wstępne:
#define f(X) ((X) += 2)
Przeciążenie:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Szablony:
template <typename T>
void f(T& x) { x += 2; }
Wirtualna wysyłka:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; }
Inne powiązane mechanizmy
Polimorfizm dostarczony przez kompilator dla typów wbudowanych, standardowych konwersji i rzutowania / koercji są omówione później dla kompletności jako:
- i tak są powszechnie rozumiane intuicyjnie (co uzasadnia reakcję „ och, ta ”),
- wpływają na próg wymagający i bezproblemowość w korzystaniu z powyższych mechanizmów oraz
- wyjaśnienie jest frywolnym odwróceniem uwagi od ważniejszych pojęć.
Terminologia
Dalsza kategoryzacja
Biorąc pod uwagę powyższe mechanizmy polimorficzne, możemy je kategoryzować na różne sposoby:
1 - Szablony są niezwykle elastyczne. SFINAE (zobacz także std::enable_if
) skutecznie dopuszcza kilka zestawów oczekiwań dla polimorfizmu parametrycznego. Na przykład możesz zakodować, że gdy typ przetwarzanych danych ma element .size()
członkowski, użyjesz jednej funkcji, w przeciwnym razie inna funkcja, która nie potrzebuje .size()
(ale prawdopodobnie w jakiś sposób cierpi - np. Używając wolniejszego strlen()
lub nie drukującego jako przydatna wiadomość w dzienniku). Możesz również określić zachowania ad hoc, gdy szablon jest tworzony z określonymi parametrami, pozostawiając niektóre parametry parametryczne ( częściowa specjalizacja szablonu ) lub nie ( pełna specjalizacja ).
"Polimorficzny"
Alf Steinbach komentuje, że w C ++ Standard polimorfizm odnosi się tylko do polimorfizmu w czasie wykonywania przy użyciu wirtualnej wysyłki. Ogólne Comp. Sci. znaczenie jest bardziej inkluzywne, zgodnie z glosariuszem twórcy C ++ Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ):
polimorfizm - zapewnienie jednego interfejsu dla bytów różnych typów. Funkcje wirtualne zapewniają polimorfizm dynamiczny (w czasie wykonywania) za pośrednictwem interfejsu zapewnianego przez klasę bazową. Przeciążone funkcje i szablony zapewniają statyczny (w czasie kompilacji) polimorfizm. TC ++ PL 12.2.6, 13.6.1, D&E 2.9.
Ta odpowiedź - podobnie jak pytanie - wiąże funkcje C ++ z plikiem Comp. Sci. terminologia.
Dyskusja
Ze standardem C ++ używającym węższej definicji „polimorfizmu” niż w przypadku Comp. Sci. społeczność, w celu zapewnienia wzajemnego zrozumienia dla Twojego rozważyć publiczność ...
- używając jednoznacznej terminologii („czy możemy uczynić ten kod wielokrotnego użytku dla innych typów?” lub „czy możemy użyć wirtualnej wysyłki?” zamiast „czy możemy uczynić ten kod polimorficznym?”) i / lub
- jasno określając terminologię.
Jednak kluczem do bycia świetnym programistą C ++ jest zrozumienie, co naprawdę robi dla Ciebie polimorfizm ...
pozwalając na jednorazowe napisanie „algorytmicznego” kodu, a następnie zastosowanie go do wielu typów danych
... a następnie bądź bardzo świadomy tego, jak różne mechanizmy polimorficzne odpowiadają Twoim rzeczywistym potrzebom.
Polimorfizm w czasie wykonywania:
- dane wejściowe przetwarzane metodami fabrycznymi i wypluwane jako niejednorodny zbiór obiektów obsługiwany za pomocą
Base*
s,
- implementacja wybrana w czasie wykonywania na podstawie plików konfiguracyjnych, przełączników linii poleceń, ustawień interfejsu użytkownika itp.,
- implementacja różniła się w czasie wykonywania, na przykład dla wzorca maszyny stanów.
Kiedy nie ma jasnego sterownika dla polimorfizmu w czasie wykonywania, często preferowane są opcje kompilacji. Rozważać:
- aspekt kompilacji, jak to się nazywa, klas opartych na szablonach jest lepszy od grubych interfejsów, które kończą się niepowodzeniem w czasie wykonywania
- SFINAE
- CRTP
- optymalizacje (wiele z nich obejmuje eliminację wbudowanego i martwego kodu, rozwijanie pętli, statyczne tablice oparte na stosie vs sterty)
__FILE__
, __LINE__
, String literal konkatenacji i inne unikalne możliwości makr (które pozostają złe ;-))
- szablony i makra testowe użycie semantyczne jest obsługiwane, ale nie ograniczaj sztucznie sposobu, w jaki ta obsługa jest zapewniana (jak ma to miejsce w przypadku wirtualnego wysyłania, wymagając dokładnie dopasowanych zastąpień funkcji składowych)
Inne mechanizmy wspierające polimorfizm
Zgodnie z obietnicą, w celu zapewnienia kompletności omówiono kilka dodatkowych tematów:
- przeciążenia dostarczone przez kompilator
- konwersje
- rzuty / przymus
Ta odpowiedź kończy się dyskusją na temat tego, jak powyższe łączą się, aby wzmocnić i uprościć kod polimorficzny - zwłaszcza polimorfizm parametryczny (szablony i makra).
Mechanizmy mapowania do operacji specyficznych dla typu
> Niejawne przeciążenia dostarczone przez kompilator
Koncepcyjnie kompilator przeciąża wiele operatorów dla typów wbudowanych. Koncepcyjnie nie różni się od przeciążenia określonego przez użytkownika, ale jest wymieniony, ponieważ łatwo go przeoczyć. Na przykład możesz dodać do int
s i double
s używając tej samej notacji, x += 2
a kompilator wygeneruje:
- instrukcje procesora specyficzne dla typu
- wynik tego samego typu.
Przeciążanie, a następnie płynnie obejmuje typy zdefiniowane przez użytkownika:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Dostarczane przez kompilator przeciążenia dla typów podstawowych są powszechne w językach komputerowych wysokiego poziomu (3GL +), a jawna dyskusja na temat polimorfizmu generalnie oznacza coś więcej. (2GL - języki asemblera - często wymagają od programisty jawnego użycia różnych mnemoników dla różnych typów).
> Konwersje standardowe
Czwarta sekcja standardu C ++ opisuje konwersje standardowe.
Pierwszy punkt podsumowuje ładnie (ze starego projektu - miejmy nadzieję, że nadal merytorycznie poprawny):
-1- Konwersje standardowe to niejawne konwersje zdefiniowane dla typów wbudowanych. Klauzula conv wylicza pełny zestaw takich konwersji. Standardowa sekwencja konwersji to sekwencja standardowych konwersji w następującej kolejności:
Zero lub jedna konwersja z następującego zestawu: konwersja l-wartości do r-wartości, konwersja tablicy do wskaźnika i konwersja funkcji do wskaźnika.
Zero lub jedna konwersja z następującego zestawu: promocje integralne, promocja zmiennoprzecinkowa, konwersje integralne, konwersje zmiennoprzecinkowe, konwersje zmiennoprzecinkowe, konwersje wskaźnika, konwersje wskaźnika na składowe i konwersje logiczne.
Zero lub jedna konwersja kwalifikacji.
[Uwaga: standardowa sekwencja konwersji może być pusta, tj. Nie może składać się z żadnych konwersji. ] W razie potrzeby do wyrażenia zostanie zastosowana standardowa sekwencja konwersji, aby przekonwertować je na wymagany typ docelowy.
Te konwersje zezwalają na kod taki jak:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Zastosowanie wcześniejszego testu:
Aby być polimorficznym, [ a()
] musi być w stanie operować wartościami co najmniej dwóch różnych typów (np. int
I double
), znajdując i wykonując kod odpowiedni dla typu .
a()
sam uruchamia kod specjalnie dla double
i dlatego nie jest polimorficzny.
Ale w drugim wywołaniu a()
kompilatora wie wygenerować kod typu, właściwe dla „zmiennoprzecinkowej promocji” (standard §4) do przekształcania 42
się 42.0
. Ten dodatkowy kod znajduje się w funkcji wywołującej . W podsumowaniu omówimy znaczenie tego.
> Wymuszanie, rzutowanie, niejawne konstruktory
Te mechanizmy umożliwiają klasom zdefiniowanym przez użytkownika określanie zachowań podobnych do standardowych konwersji typów wbudowanych. Spójrzmy:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Tutaj obiekt std::cin
jest oceniany w kontekście logicznym, przy pomocy operatora konwersji. Można to koncepcyjnie zgrupować z „integralnymi promocjami” i innymi ze Standardowych konwersji w powyższym temacie.
Niejawne konstruktory skutecznie robią to samo, ale są kontrolowane przez typ rzutowania:
f(const std::string& x);
f("hello");
Implikacje przeciążeń, konwersji i wymuszania dostarczonych przez kompilator
Rozważać:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Jeśli chcemy ilość x
należy traktować jako liczbę rzeczywistą podczas podziału (tj wynosić 6,5 zamiast zaokrąglona w dół do 6), my tylko potrzebujemy zmiany typedef double Amount
.
To fajne, ale nie byłoby zbyt wiele pracy, aby kod wyraźnie „poprawiał typ”:
void f() void f()
{ {
typedef int Amount; typedef double Amount;
Amount x = 13; Amount x = 13.0;
x /= 2; x /= 2.0;
std::cout << double(x) * 1.1; std::cout << x * 1.1;
} }
Ale weźmy pod uwagę, że możemy przekształcić pierwszą wersję w template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
To dzięki tym małym „wygodnym funkcjom” można tak łatwo utworzyć instancję dla jednej int
lub drugiej double
i działać zgodnie z przeznaczeniem. Bez tych funkcji potrzebowalibyśmy jawnych rzutowań, cech typu i / lub klas zasad, a także jakiegoś rozwlekłego, podatnego na błędy bałaganu, takiego jak:
template <typename Amount, typename Policy>
void f()
{
Amount x = Policy::thirteen;
x /= static_cast<Amount>(2);
std::cout << traits<Amount>::to_double(x) * 1.1;
}
Tak więc przeciążanie operatorów dostarczone przez kompilator dla typów wbudowanych, konwersje standardowe, rzutowanie / wymuszanie / niejawne konstruktory - wszystkie one zapewniają subtelną obsługę polimorfizmu. Z definicji u góry tej odpowiedzi odnoszą się do „znajdowania i wykonywania kodu odpowiedniego dla typu” poprzez mapowanie:
Oni nie ustanawiają konteksty polimorficznych przez siebie, ale nie pomagają Empower / kod uprościć wewnątrz takich kontekstach.
Możesz czuć się oszukany ... to nie wydaje się dużo. Istotne jest to, że w parametrycznych kontekstach polimorficznych (tj. Wewnątrz szablonów lub makr) staramy się obsługiwać dowolnie duży zakres typów, ale często chcemy wyrazić na nich operacje w kategoriach innych funkcji, literałów i operacji, które zostały zaprojektowane dla mały zestaw typów. Zmniejsza potrzebę tworzenia prawie identycznych funkcji lub danych na podstawie typu, gdy operacja / wartość jest logicznie taka sama. Funkcje te współpracują ze sobą, aby dodać podejście „najlepszego wysiłku”, robiąc to, czego intuicyjnie oczekiwano, wykorzystując ograniczone dostępne funkcje i dane i zatrzymując się z błędem tylko wtedy, gdy pojawia się prawdziwa niejasność.
Pomaga to ograniczyć potrzebę stosowania kodu polimorficznego obsługującego kod polimorficzny, tworząc ściślejszą sieć wokół stosowania polimorfizmu, aby lokalne użycie nie wymuszało powszechnego stosowania, a także udostępnianie korzyści z polimorfizmu w razie potrzeby bez nakładania kosztów związanych z koniecznością ujawniania implementacji czasu kompilacji, mają wiele kopii tej samej funkcji logicznej w kodzie obiektowym do obsługi używanych typów i wykonywania wirtualnego wysyłania w przeciwieństwie do wywołań wewnętrznych lub przynajmniej rozstrzyganych w czasie kompilacji. Jak to jest typowe w C ++, programista ma dużą swobodę kontrolowania granic, w ramach których używany jest polimorfizm.