Polimorfizm w C ++

130

AFAIK:

C ++ udostępnia trzy różne typy polimorfizmu.

  • Funkcje wirtualne
  • Przeciążanie nazw funkcji
  • Przeciążanie operatorów

Oprócz powyższych trzech typów polimorfizmu istnieją inne rodzaje polimorfizmu:

  • w czasie wykonywania
  • czas kompilacji
  • polimorfizm ad hoc
  • parametryczny polimorfizm

Wiem, że polimorfizm w czasie wykonywania można osiągnąć za pomocą funkcji wirtualnych, a polimorfizm statyczny można osiągnąć za pomocą funkcji szablonowych

Ale dla pozostałych dwóch

  • polimorfizm ad hoc
  • parametryczne Polimorfizm strona mówi ,

polimorfizm ad hoc:

Jeśli zakres rzeczywistych typów, które mogą być użyte, jest ograniczony, a kombinacje muszą być indywidualnie określone przed użyciem, nazywa się to polimorfizmem ad hoc.

polimorfizm parametryczny:

Jeśli cały kod jest napisany bez wzmianki o jakimkolwiek konkretnym typie, a zatem może być używany w sposób przezroczysty z dowolną liczbą nowych typów, nazywa się to polimorfizmem parametrycznym.

Ledwo ich rozumiem :(

Czy ktoś może wyjaśnić je obie, jeśli to możliwe, na przykładzie? Mam nadzieję, że odpowiedzi na te pytania byłyby pomocne dla wielu nowych pasjonatów z ich uczelni.

Vijay
źródło
30
Właściwie C ++ ma cztery rodzaje polimorfizmu: parametryczny (generyczność za pomocą szablonów w C ++), włączanie (podtypowanie za pomocą metod wirtualnych w C ++), przeciążanie i wymuszanie (niejawne konwersje). Koncepcyjnie istnieje niewielka różnica między przeciążeniem funkcji a przeciążeniem operatora.
fredoverflow
Wygląda więc na to, że witryna, o której wspomniałem, wprowadza wielu w błąd… prawda?
Vijay
@zombie: ta strona internetowa porusza wiele dobrych koncepcji, ale nie jest precyzyjna i konsekwentna w używaniu terminologii (na przykład, gdy zaczyna mówić o polimorfizmie wirtualnego wysyłania / czasu wykonywania, zawiera wiele błędnych stwierdzeń na temat polimorfizmu ogólnie, ale prawdziwe w przypadku wirtualnej wysyłki). Jeśli już rozumiesz temat, możesz odnieść się do tego, co zostało powiedziane, i wprowadzić mentalnie niezbędne zastrzeżenia, ale trudno jest to osiągnąć, czytając stronę ....
Tony Delroy
Niektóre terminy są prawie synonimami lub są bardziej powiązane, ale bardziej ograniczone niż inne terminy. Na przykład termin „polimorfizm ad-hoc” jest w moim doświadczeniu najczęściej używany w Haskellu, jednak „funkcje wirtualne” są bardzo blisko spokrewnione. Niewielka różnica polega na tym, że „funkcje wirtualne” to termin zorientowany obiektowo, odnoszący się do funkcji składowych z „późnym wiązaniem”. „Wysyłka wielokrotna” jest również rodzajem polimorfizmu ad hoc. Jak mówi FredOverflow, zarówno operator, jak i przeciążanie funkcji to w zasadzie to samo.
Steve314
Poprawiłem dla Ciebie formatowanie. Przeczytaj pomoc dostępną po prawej stronie okienka edycji. Ktoś z> 200 pytaniami i> 3k powinien znać te podstawowe rzeczy. Możesz też kupić nową klawiaturę. Ten klawisz Shift wydaje się bez przerwy zawodzić. Aha, i: w C ++ nie ma czegoś takiego jak „funkcja szablonu” . Istnieją jednak szablony funkcji .
sbi

Odpowiedzi:

219

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 xi yjako dane wejściowe.

Aby wykazać polimorfizm, f()musi być w stanie operować wartościami co najmniej dwóch różnych typów (np. intI 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)
    // (note: in real code, use a longer uppercase name for a macro!)
    
  • 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; } // run-time polymorphic dispatch
    

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:

  • Kiedy wybierany jest kod specyficzny dla typu polimorficznego?

    • Czas wykonywania oznacza, że ​​kompilator musi generować kod dla wszystkich typów, które program może obsługiwać podczas działania, aw czasie wykonywania wybierany jest właściwy kod ( wirtualna wysyłka )
    • Czas kompilacji oznacza, że ​​wybór kodu specyficznego dla typu jest dokonywany podczas kompilacji. Konsekwencją tego: powiedzmy, że program wywołany fpowyżej tylko z intargumentami - w zależności od zastosowanego mechanizmu polimorficznego i opcji wbudowanych kompilator może uniknąć generowania jakiegokolwiek kodu f(double)lub wygenerowany kod może zostać wyrzucony w pewnym momencie kompilacji lub linkowania. ( wszystkie powyższe mechanizmy z wyjątkiem wirtualnej wysyłki )

  • Jakie typy są obsługiwane?

    • Ad-hoc, co oznacza, że ​​udostępniasz wyraźny kod do obsługi każdego typu (np. Przeciążenie, specjalizacja szablonów); jawnie dodajesz obsługę typu „dla tego” (zgodnie ze znaczeniem ad hoc ), innego typu „ten” i może też „tamtego” ;-).
    • Co oznacza parametryczne , możesz po prostu spróbować użyć funkcji dla różnych typów parametrów, nie robiąc nic, aby umożliwić jej obsługę (np. Szablony, makra). Obiekt z funkcjami / operatorami, które działają tak, jak szablon / makro, którego oczekuje od 1, to wszystko, czego szablon / makro potrzebuje do wykonania swojej pracy, przy czym dokładny typ nie ma znaczenia. „Koncepcje” wprowadzone przez C ++ 20 wyrażają i wymuszają takie oczekiwania - zobacz stronę cppreference tutaj .

      • Polimorfizm parametryczny zapewnia kaczkę na klawiaturze - koncepcję przypisywaną Jamesowi Whitcombowi Rileyowi, który najwyraźniej powiedział: „Kiedy widzę ptaka, który chodzi jak kaczka i pływa jak kaczka i kwacze jak kaczka, nazywam tego ptaka kaczką”. .

        template <typename Duck>
        void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
        
        do_ducky_stuff(Vilified_Cygnet());
        
    • Polimorfizm podtypów (inaczej włączający) umożliwia pracę nad nowymi typami bez aktualizacji algorytmu / funkcji, ale muszą one pochodzić z tej samej klasy bazowej (wirtualna wysyłka)

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 ints i doubles używając tej samej notacji, x += 2a 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. intI double), znajdując i wykonując kod odpowiedni dla typu .

a()sam uruchamia kod specjalnie dla doublei 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 42się 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::cinjest 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");  // invokes `std::string::string(const char*)`

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ść xnależ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 intlub drugiej doublei 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:

  • „z dala” od typów parametrów

    • z wielu typów danych, które obsługuje polimorficzny kod algorytmiczny

    • do kodu napisanego dla (potencjalnie mniejszej) liczby (takich samych lub innych) typów.

  • „na” typy parametryczne z wartości typu stałego

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.

Tony Delroy
źródło
1
-1 Świetna odpowiedź z wyjątkiem dyskusji terminologicznej. Standard C ++ definiuje termin „polimorficzny” w §1.8 / 1, odwołując się tam do sekcji 10.3 o funkcjach wirtualnych. Nie ma więc miejsca na poruszanie się, dyskusję, osobiste zdanie: w kontekście standardowego C ++ termin ten jest definiowany raz na zawsze. I odgrywa rolę w praktyce. Na przykład §5.2.7 / 6 dynamic_castwymaga „wskaźnika do lub lwartości typu polimorficznego”. Pozdrawiam i hth.,
Pozdrawiam i hth. - Alf
@Alf: świetne odniesienie - chociaż myślę, że twoja perspektywa jest zbyt wąska. Z pytania wymieniającego przeciążenie, polimorfizm ad-hoc i parametryczny itp. Jasno wynika, że ​​odpowiedź powinna odnosić się do możliwości C ++ do ogólnego Comp. Sci. znaczenie terminów. Rzeczywiście, słownik Stroustrupa mówi „polimorfizm - zapewniający pojedynczy interfejs dla jednostek różnych typów. Funkcje wirtualne zapewniają dynamiczny (w czasie wykonywania) polimorfizm poprzez interfejs dostarczany 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. ”
Tony Delroy
@ Tony: to nie jest główna myśl, że twoja odpowiedź jest zła. jest ok, jest super. to tylko to wrt. terminologia, która się odwróciła: formalna terminologia akademicka jest wąska zdefiniowana przez Święty Międzynarodowy Standard, a nieformalna, szorstka terminologia, w której ludzie mogą mieć na myśli nieco inne rzeczy, jest głównie używana w tym pytaniu i odpowiedzi. Pozdrawiam i hth.,
Pozdrawiam i hth. - Alf
@Alf: Chciałbym, żeby odpowiedź była świetna - „Inne mechanizmy” trzeba przepisać w jednej piątej wersów, a ja rozważam / szkicuję bardziej konkretne cechy i implikacje, kontrastujące z mechanizmami polimorficznymi. W każdym razie, rozumiem, że formalne akademickie znaczenie skoncentrowane wyłącznie na C ++ może być wąskie, ale formalne akademickie ogólne, Comp. Sci. znaczenie nie jest, o czym świadczy słownik Stroustrupa. Potrzebujemy czegoś definitywnego - np. Definicji Knutha - nie mamy jeszcze szczęścia w googlowaniu. Doceniam, że jesteś guru C ++, ale czy możesz wskazać konkretne dowody na ten temat?
Tony Delroy
1
@Alf: po drugie, jestem przekonany, że polimorfizm jest formalnie zdefiniowany w każdym przyzwoitym ogólnym Comp. Sci. książka w (ponadczasowy, stabilny) sposób zgodny z moim użytkowaniem (i sposobem Stroustrupa). Artykuł w Wikipedii łączy kilka publikacji akademickich, które definiują to w ten sposób: „Funkcje polimorficzne to funkcje, których operandy (rzeczywiste parametry) mogą mieć więcej niż jeden typ. Typy polimorficzne to typy, których operacje mają zastosowanie do wartości więcej niż jednego typu”. (z lucacardelli.name/Papers/OnUnderstanding.A4.pdf ). Zatem pytanie brzmi: „kto mówi w imieniu Comp. Sci”…?
Tony Delroy
15

W C ++ ważną różnicą jest powiązanie w czasie wykonywania i w czasie kompilacji. Ad-hoc vs. parametrric tak naprawdę nie pomaga, co wyjaśnię później.

|----------------------+--------------|
| Form                 | Resolved at  |
|----------------------+--------------|
| function overloading | compile-time |
| operator overloading | compile-time |
| templates            | compile-time |
| virtual methods      | run-time     |
|----------------------+--------------|

Uwaga - polimorfizm w czasie wykonywania może nadal zostać rozwiązany w czasie kompilacji, ale to tylko optymalizacja. Potrzeba wydajnego wspierania rozwiązywania problemów w czasie wykonywania i wymiany z innymi problemami jest częścią tego, co doprowadziło do tego, że funkcje wirtualne są tym, czym są. I to jest naprawdę kluczowe dla wszystkich form polimorfizmu w C ++ - każda wynika z innego zestawu kompromisów dokonanych w innym kontekście.

Przeciążanie funkcji i operatora to to samo pod każdym względem. Nazwy i składnia ich używania nie wpływają na polimorfizm.

Szablony umożliwiają jednoczesne określenie wielu przeciążeń funkcji.

Jest inny zestaw nazw dla tego samego pomysłu na czas rozwiązania ...

|---------------+--------------|
| early binding | compile-time |
| late binding  | run-time     |
|---------------+--------------|

Nazwy te są bardziej powiązane z OOP, więc trochę dziwne jest stwierdzenie, że szablon lub inna funkcja niebędąca składnikiem używa wczesnego wiązania.

Aby lepiej zrozumieć związek między funkcjami wirtualnymi a przeciążeniem funkcji, warto również zrozumieć różnicę między „wysyłką pojedynczą” a „wysyłką wielokrotną”. Pomysł można rozumieć jako postęp ...

  • Po pierwsze, istnieją funkcje monomorficzne. Implementacja funkcji jest jednoznacznie identyfikowana przez nazwę funkcji. Żaden z parametrów nie jest wyjątkowy.
  • Następnie następuje pojedyncza wysyłka. Jeden z parametrów jest uważany za specjalny i używany (wraz z nazwą) do określenia, której implementacji należy użyć. W OOP myślimy o tym parametrze jako o „obiekcie”, wymieniamy go przed nazwą funkcji itp.
  • Następnie istnieje wiele wysyłek. Wszelkie / wszystkie parametry przyczyniają się do określenia, której implementacji użyć. Dlatego po raz kolejny żaden z parametrów nie musi być wyjątkowy.

Oczywiście OOP jest czymś więcej niż wymówką, by wyznaczyć jeden parametr jako specjalny, ale to tylko jedna z części. Wracając do tego, co powiedziałem o kompromisach - pojedyncza wysyłka jest dość łatwa do wykonania i wydajnie (zwykle implementacja nazywa się „wirtualnymi tabelami”). Wysyłanie wielokrotne jest bardziej niewygodne, nie tylko pod względem wydajności, ale także w przypadku osobnej kompilacji. Jeśli jesteś ciekawy, możesz sprawdzić „problem z wyrażeniem”.

Tak jak trochę dziwne jest używanie terminu „wczesne wiązanie” dla funkcji niebędących składnikami, trochę dziwne jest używanie terminów „pojedyncze wysłanie” i „wielokrotne wysłanie”, gdzie polimorfizm jest rozwiązywany w czasie kompilacji. Zwykle uważa się, że C ++ nie ma wielu wysyłek, co jest uważane za szczególny rodzaj rozwiązania w czasie wykonywania. Jednak przeciążenie funkcji może być postrzegane jako wielokrotne wysyłanie wykonywane w czasie kompilacji.

Wracając do polimorfizmu parametrycznego i ad-hoc, terminy te są bardziej popularne w programowaniu funkcjonalnym i nie do końca działają w C ++. Nawet jeśli...

Polimorfizm parametryczny oznacza, że ​​masz typy jako parametry i używany jest dokładnie ten sam kod, niezależnie od typu użytego dla tych parametrów.

Polimorfizm ad-hoc jest ad-hoc w tym sensie, że podajesz inny kod w zależności od poszczególnych typów.

Funkcje przeciążeniowe i wirtualne są przykładami polimorfizmu ad-hoc.

Znowu jest kilka synonimów ...

|------------+---------------|
| parametric | unconstrained |
| ad-hoc     | constrained   |
|------------+---------------|

Z wyjątkiem tego, że nie są to całkiem synonimy, chociaż są powszechnie traktowane tak, jakby były, i właśnie tam prawdopodobnie pojawi się zamieszanie w C ++.

Powodem traktowania ich jako synonimów jest to, że ograniczając polimorfizm do określonych klas typów, staje się możliwe użycie operacji specyficznych dla tych klas typów. Słowo „klasy” może być tutaj interpretowane w sensie OOP, ale tak naprawdę odnosi się tylko do (zwykle nazwanych) zestawów typów, które współużytkują pewne operacje.

Tak więc polimorfizm parametryczny jest zwykle przyjmowany (przynajmniej domyślnie), aby implikować polimorfizm nieograniczony. Ponieważ ten sam kod jest używany niezależnie od parametrów typu, jedynymi obsługiwanymi operacjami są te, które działają dla wszystkich typów. Pozostawiając zestaw typów bez ograniczeń, poważnie ograniczasz zestaw operacji, które możesz zastosować do tych typów.

Na przykład w Haskell możesz mieć ...

myfunc1 :: Bool -> a -> a -> a
myfunc1 c x y = if c then x else y

aTutaj jest niewymuszony polimorficzny typu. Może to być wszystko, więc niewiele możemy zrobić z wartościami tego typu.

myfunc2 :: Num a => a -> a
myfunc2 x = x + 3

Tutaj ajest ograniczony do bycia członkiem Numklasy - typy, które zachowują się jak liczby. To ograniczenie pozwala ci robić rzeczy numeryczne z tymi wartościami, na przykład je dodawać. Nawet z 3wnioskowania o typie polimorficznym wynika, że ​​masz na myśli 3typ a.

Myślę o tym jako o ograniczonym polimorfizmie parametrycznym. Jest tylko jedna implementacja, ale można ją zastosować tylko w ograniczonych przypadkach. Aspekt doraźny to wybór +i 3wykorzystanie. Każda „instancja” Numma swoją własną odrębną implementację. Więc nawet w Haskell „parametryczne” i „nieograniczone” nie są tak naprawdę synonimami - nie wiń mnie, to nie moja wina!

W C ++ zarówno przeciążanie, jak i funkcje wirtualne są polimorfizmem ad-hoc. Definicja polimorfizmu ad-hoc nie ma znaczenia, czy implementacja jest wybierana w czasie wykonywania, czy w czasie kompilacji.

C ++ zbliża się do polimorfizmu parametrycznego z szablonami, jeśli każdy parametr szablonu ma typ typename. Istnieją parametry typu i istnieje jedna implementacja bez względu na używane typy. Jednak reguła „Niepowodzenie podstawienia nie jest błędem” oznacza, że ​​niejawne ograniczenia powstają w wyniku użycia operacji w szablonie. Dodatkowe komplikacje obejmują specjalizację szablonów w zakresie udostępniania szablonów alternatywnych - różne (ad-hoc) implementacje.

Więc w pewnym sensie C ++ ma polimorfizm parametryczny, ale jest on niejawnie ograniczony i może być zastąpiony przez alternatywy ad-hoc - tj. Ta klasyfikacja tak naprawdę nie działa dla C ++.

Steve314
źródło
+1 Wiele interesujących punktów i spostrzeżeń. Spędziłem tylko kilka godzin na czytaniu o Haskellu, więc „ aoto nieskrępowany typ polimorficzny [...] więc niewiele możemy zrobić z wartościami tego typu”. był interesujący - w C ++ sans Concepts nie jesteś ograniczony tylko do próby wykonania określonego zestawu operacji na argumencie typu określonego jako parametr szablonu ... biblioteki takie jak koncepcje boost działają w drugą stronę - upewniając się, że typ obsługuje operacje określasz, zamiast chronić się przed przypadkowym użyciem dodatkowych operacji.
Tony Delroy,
@Tony - Pojęcia są sposobem na jawne ograniczenie polimorfizmu szablonów. Ukryte ograniczenia oczywiście nie znikną ze względu na kompatybilność, ale jawne ograniczenia zdecydowanie poprawią sytuację. Jestem prawie pewien, że niektóre wcześniejsze plany koncepcyjne były w pewnym stopniu związane z typeklasami Haskella, chociaż nie zagłębiałem się w nie tak dogłębnie, a kiedy ostatnio patrzyłem „płytko”, nie znałem zbyt wiele Haskella.
Steve314
„Ukryte ograniczenia oczywiście nie znikną ze względu na kompatybilność” - z pamięci, C ++ 0x Pojęcia zapobiegły (obiecując: - /) „niejawne ograniczenia” - można było używać typu tylko w sposób obiecany przez Koncepcje.
Tony Delroy,
2

Jeśli chodzi o polimorfizm ad-hoc, oznacza to przeciążanie funkcji lub przeciążanie operatorów. Sprawdź tutaj:

http://en.wikipedia.org/wiki/Ad-hoc_polymorphism

Jeśli chodzi o polimorfizm parametryczny, funkcje szablonowe również mogą być uwzględnione, ponieważ niekoniecznie przyjmują parametry typów FIXED. Na przykład jedna funkcja może sortować tablicę liczb całkowitych, a także może sortować tablicę ciągów itd.

http://en.wikipedia.org/wiki/Parametric_polymorphism

Eric Z
źródło
1
Niestety, chociaż jest to poprawne, jest to mylące. Funkcje szablonów mogą uzyskać niejawne ograniczenia z powodu reguły SFINAE - użycie operacji w szablonie niejawnie ogranicza polimorfizm - a specjalizacja szablonów może zapewnić ad-hoc alternatywne szablony, które zastępują bardziej ogólne szablony. Tak więc szablon (domyślnie) zapewnia nieograniczony polimorfizm parametryczny, ale nie ma tego wymuszania - istnieją co najmniej dwa sposoby, w jaki może zostać ograniczony lub ad-hoc.
Steve314
W rzeczywistości twój przykład - sortowanie - sugeruje ograniczenie. Sortowanie działa tylko dla uporządkowanych typów (tj. Należy podać <operatory i podobne). W Haskell wyraziłbyś to wymaganie jawnie za pomocą klasy Ord. Fakt, że otrzymujesz różne w <zależności od konkretnego typu (dostarczonego przez instancję Ord), byłby uważany za polimorfizm ad hoc.
Steve314
2

To może nie być pomocne, ale zrobiłem to, aby wprowadzić moich przyjaciół w programowanie, przekazując określone funkcje, takie jak START, i ENDdla funkcji głównej, więc nie było to zbyt trudne (używali tylko pliku main.cpp ). Zawiera klasy i struktury polimorficzne, szablony, wektory, tablice, dyrektywy preprocesora, przyjaźń, operatory i wskaźniki (z których wszystkie prawdopodobnie powinieneś wiedzieć przed przystąpieniem do polimorfizmu):

Uwaga: to jeszcze nie koniec, ale możesz mieć pomysł

main.cpp

#include "main.h"
#define ON_ERROR_CLEAR_SCREEN false
START
    Library MyLibrary;
    Book MyBook("My Book", "Me");
    MyBook.Summarize();
    MyBook += "Hello World";
    MyBook += "HI";
    MyBook.EditAuthor("Joe");
    MyBook.EditName("Hello Book");
    MyBook.Summarize();
    FixedBookCollection<FairyTale> FBooks("Fairytale Books");
    FairyTale MyTale("Tale", "Joe");
    FBooks += MyTale;
    BookCollection E("E");
    MyLibrary += E;
    MyLibrary += FBooks;
    MyLibrary.Summarize();
    MyLibrary -= FBooks;
    MyLibrary.Summarize();
    FixedSizeBookCollection<5> Collection("My Fixed Size Collection");
    /* Extension Work */ Book* Duplicate = MyLibrary.DuplicateBook(&MyBook);
    /* Extension Work */ Duplicate->Summarize();
END

main.h

#include <iostream>
#include <sstream>
#include <vector>
#include <string>
#include <type_traits>
#include <array>
#ifndef __cplusplus
#error Not C++
#endif
#define START int main(void)try{
#define END GET_ENTER_EXIT return(0);}catch(const std::exception& e){if(ON_ERROR_CLEAR_SCREEN){system("cls");}std::cerr << "Error: " << e.what() << std::endl; GET_ENTER_EXIT return (1);}
#define GET_ENTER_EXIT std::cout << "Press enter to exit" << std::endl; getchar();
class Book;
class Library;
typedef std::vector<const Book*> Books;
bool sContains(const std::string s, const char c){
    return (s.find(c) != std::string::npos);
}
bool approve(std::string s){
    return (!sContains(s, '#') && !sContains(s, '%') && !sContains(s, '~'));
}
template <class C> bool isBook(){
    return (typeid(C) == typeid(Book) || std::is_base_of<Book, C>());
}
template<class ClassToDuplicate> class DuplicatableClass{ 
public:
    ClassToDuplicate* Duplicate(ClassToDuplicate ToDuplicate){
        return new ClassToDuplicate(ToDuplicate);
    }
};
class Book : private DuplicatableClass<Book>{
friend class Library;
friend struct BookCollection;
public:
    Book(const char* Name, const char* Author) : name_(Name), author_(Author){}
    void operator+=(const char* Page){
        pages_.push_back(Page);
    }
    void EditAuthor(const char* AuthorName){
        if(approve(AuthorName)){
            author_ = AuthorName;
        }
        else{
            std::ostringstream errorMessage;
            errorMessage << "The author of the book " << name_ << " could not be changed as it was not approved";
            throw std::exception(errorMessage.str().c_str());
        }
    }
    void EditName(const char* Name){
        if(approve(Name)){
            name_ = Name;
        }
        else{
            std::ostringstream errorMessage;
            errorMessage << "The name of the book " << name_ << " could not be changed as it was not approved";
            throw std::exception(errorMessage.str().c_str());
        }
    }
    virtual void Summarize(){
        std::cout << "Book called " << name_ << "; written by " << author_ << ". Contains "
            << pages_.size() << ((pages_.size() == 1) ? " page:" : ((pages_.size() > 0) ? " pages:" : " pages")) << std::endl;
        if(pages_.size() > 0){
            ListPages(std::cout);
        }
    }
private:
    std::vector<const char*> pages_;
    const char* name_;
    const char* author_;
    void ListPages(std::ostream& output){
        for(int i = 0; i < pages_.size(); ++i){
            output << pages_[i] << std::endl;
        }
    }
};
class FairyTale : public Book{
public:
    FairyTale(const char* Name, const char* Author) : Book(Name, Author){}
};
struct BookCollection{
friend class Library;
    BookCollection(const char* Name) : name_(Name){}
    virtual void operator+=(const Book& Book)try{
        Collection.push_back(&Book); 
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
    virtual void operator-=(const Book& Book){
        for(int i = 0; i < Collection.size(); ++i){
            if(Collection[i] == &Book){
                Collection.erase(Collection.begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
private:
    const char* name_;
    Books Collection;
};
template<class FixedType> struct FixedBookCollection : public BookCollection{
    FixedBookCollection(const char* Name) : BookCollection(Name){
        if(!isBook<FixedType>()){
            std::ostringstream errorMessage;
            errorMessage << "The type " << typeid(FixedType).name() << " cannot be initialized as a FixedBookCollection";
            throw std::exception(errorMessage.str().c_str());
            delete this;
        }
    }
    void operator+=(const FixedType& Book)try{
        Collection.push_back(&Book); 
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
    void operator-=(const FixedType& Book){
        for(int i = 0; i < Collection.size(); ++i){
            if(Collection[i] == &Book){
                Collection.erase(Collection.begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
private:
    std::vector<const FixedType*> Collection;
};
template<size_t Size> struct FixedSizeBookCollection : private std::array<const Book*, Size>{
    FixedSizeBookCollection(const char* Name) : name_(Name){ if(Size < 1){ throw std::exception("A fixed size book collection cannot be smaller than 1"); currentPos = 0; } }
    void operator+=(const Book& Book)try{
        if(currentPos + 1 > Size){
            std::ostringstream errorMessage;
            errorMessage << "The FixedSizeBookCollection " << name_ << "'s size capacity has been overfilled";
            throw std::exception(errorMessage.str().c_str());
        }
        this->at(currentPos++) = &Book;
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
private:
    const char* name_;
    int currentPos;
};
class Library : private std::vector<const BookCollection*>{
public:
    void operator+=(const BookCollection& Collection){
        for(int i = 0; i < size(); ++i){
            if((*this)[i] == &Collection){
                std::ostringstream errorMessage;
                errorMessage << "The BookCollection " << Collection.name_ << " was already in the library, and therefore cannot be added";
                throw std::exception(errorMessage.str().c_str());
            }
        }
        push_back(&Collection);
    }
    void operator-=(const BookCollection& Collection){
        for(int i = 0; i < size(); ++i){
            if((*this)[i] == &Collection){
                erase(begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The BookCollection " << Collection.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
    Book* DuplicateBook(Book* Book)const{
        return (Book->Duplicate(*Book));
    }
    void Summarize(){
        std::cout << "Library, containing " << size() << ((size() == 1) ? " book collection:" : ((size() > 0) ? " book collections:" : " book collections")) << std::endl;
        if(size() > 0){
            for(int i = 0; i < size(); ++i){
                std::cout << (*this)[i]->name_ << std::endl;
            }
        }
    }
};
Joe
źródło
1

Oto podstawowy przykład użycia klas polimorficznych

#include <iostream>

class Animal{
public:
   Animal(const char* Name) : name_(Name){/* Add any method you would like to perform here*/
    virtual void Speak(){
        std::cout << "I am an animal called " << name_ << std::endl;
    }
    const char* name_;
};

class Dog : public Animal{
public:
    Dog(const char* Name) : Animal(Name) {/*...*/}
    void Speak(){
        std::cout << "I am a dog called " << name_ << std::endl;
    }
};

int main(void){
    Animal Bob("Bob");
    Dog Steve("Steve");
    Bob.Speak();
    Steve.Speak();
    //return (0);
}
user2976089
źródło
0

Polimorfizm oznacza wiele form jako takich, jest używany dla operatora, aby działał inaczej w różnych przypadkach. Do implementacji dziedziczenia używany jest polimorfizm. Na przykład zdefiniowaliśmy funkcję fn draw () dla kształtu klasy, a następnie rysowanie fn można zaimplementować do rysowania okręgu, ramki, trójkąta i innych kształtów. (które są obiektami o kształcie klasy)

Jayraj Srikriti Naidu
źródło
-3

Jeśli ktoś powie CUT tym ludziom

The Surgeon
The Hair Stylist
The Actor

Co się stanie?

The Surgeon would begin to make an incision.
The Hair Stylist would begin to cut someone's hair.
The Actor would abruptly stop acting out of the current scene, awaiting directorial guidance.

Tak więc powyższa reprezentacja pokazuje, czym jest polimorfizm (ta sama nazwa, inne zachowanie) w OOP.

Jeśli idziesz na rozmowę kwalifikacyjną, a prowadzący rozmowę poprosi Cię o podanie / pokazanie na żywo przykładu polimorfizmu w tym samym pokoju, w którym siedzimy, powiedz:

Odpowiedź - drzwi / okna

Zastanawiasz się jak?

Przez drzwi / okno - może przyjść człowiek, powietrze, światło, deszcz itp.

tj. Jedna forma innego zachowania (polimorfizm).

Aby lepiej to zrozumieć iw prosty sposób posłużyłem się powyższym przykładem. Jeśli potrzebujesz odniesienia do kodu, postępuj zgodnie z powyższymi odpowiedziami.

Sanchit
źródło
Jak wspomniałem dla lepszego zrozumienia polimorfizmu w c ++, użyłem powyższego przykładu. Może to pomóc świeższym w zrozumieniu i powiązaniu znaczenia lub tego, co dzieje się za kodem podczas rozmowy kwalifikacyjnej. Dziękuję Ci!
Sanchit,
op zapytał „polimorfizm w języku c ++”. twoja odpowiedź jest zbyt abstrakcyjna.
StahlRat