Dlaczego C ++ STL jest tak mocno oparty na szablonach? (i nie na * interfejsach *)

211

Mam na myśli, poza obowiązującą nazwą (Standardowa biblioteka szablonów) ...

C ++ początkowo zamierzał prezentować koncepcje OOP w C. To znaczy: możesz powiedzieć, co konkretna jednostka może, a czego nie może zrobić (niezależnie od tego, jak to robi) na podstawie swojej klasy i hierarchii klas. Niektóre kompozycje umiejętności są trudniejsze do opisania w ten sposób ze względu na problematykę wielokrotnego dziedziczenia oraz fakt, że C ++ obsługuje koncepcję interfejsów w nieco niezdarny sposób (w porównaniu z Javą itp.), Ale jest tam (i może być ulepszony).

Potem do gry weszły szablony wraz z STL. Wydawało się, że STL bierze klasyczne koncepcje OOP i spuszcza je do ścieków, używając zamiast tego szablonów.

Należy rozróżnić przypadki, w których szablony są używane do uogólnienia typów, w których same motywy typów są nieistotne dla działania szablonu (pojemniki, na przykład). Posiadanie vector<int>ma sens.

Jednak w wielu innych przypadkach (iteratory i algorytmy) typy szablonów powinny podążać za „koncepcją” (Iterator wejściowy, Iterator do przodu itp.), W której rzeczywiste szczegóły koncepcji są definiowane całkowicie przez wdrożenie szablonu funkcja / klasa, a nie klasa typu użytego w szablonie, co jest nieco przeciwdziałaniem OOP.

Na przykład możesz powiedzieć funkcji:

void MyFunc(ForwardIterator<...> *I);

Aktualizacja: Jak było niejasne w pierwotnym pytaniu, ForwardIterator jest w porządku, aby sam szablonować, aby umożliwić dowolny typ ForwardIterator. Przeciwnie, jest koncepcja ForwardIterator.

oczekuje iteratora do przodu tylko na podstawie jego definicji, w której należy spojrzeć na implementację lub dokumentację:

template <typename Type> void MyFunc(Type *I);

Dwa twierdzenia, które mogę wysunąć na korzyść korzystania z szablonów: skompilowany kod można zwiększyć, dostosowując szablon do każdego używanego typu, zamiast używać vtables. Oraz fakt, że szablonów można używać z rodzimymi typami.

Jednak szukam głębszego powodu, dla którego porzucenie klasycznego OOP na rzecz szablonowania dla STL? (Zakładając, że przeczytałeś tak daleko: P)

OB OB
źródło
4
Mógłbyś sprawdzić stackoverflow.com/questions/31693/... . Przyjęta odpowiedź jest doskonałym wyjaśnieniem tego, co oferują szablony w porównaniu do ogólnych.
James McMahon
6
@Jonas: To nie ma sensu. Ograniczenie pamięci podręcznej kosztuje cykle zegara, dlatego jest to ważne. Na koniec dnia to cykle zegara, a nie pamięć podręczna, określają wydajność. Pamięć i pamięć podręczna są ważne tylko w takim stopniu, w jakim wpływają na zużywane cykle zegara. Ponadto eksperyment można łatwo wykonać. Porównaj, powiedzmy, std :: for_Each wywoływane argumentem funktora, z równoważnym podejściem OOP / vtable. Różnica w wydajności jest oszałamiająca . Dlatego używana jest wersja szablonu.
jalf
7
i nie ma powodu, aby nadmiarowy kod wypełniał icache. Jeśli w moim programie tworzę instancję wektora <char> i wektora <int>, dlaczego kod wektora <char> powinien być ładowany do icache podczas przetwarzania wektora <int>? W rzeczywistości kod dla wektora <int> jest przycięty, ponieważ nie musi zawierać kodu do rzutowania, tabel vt i pośrednich.
jalf
3
Alex Stepanov wyjaśnia, dlaczego dziedziczenie i równość nie współgrają ze sobą.
fredoverflow
6
@BerndJendrissek: Uhm, blisko, ale nie ty. Tak, więcej kosztów kodu pod względem przepustowości pamięci i użycia pamięci podręcznej, jeśli jest faktycznie używana . Ale nie ma szczególnego powodu, aby spodziewać się vector<int>i vector<char>być używane w tym samym czasie. Mogą, oczywiście, ale możesz użyć dwóch dowolnych fragmentów kodu w tym samym czasie. To nie ma nic wspólnego z szablonami, C ++ lub STL. Nie ma niczego, w którym wystąpieniu vector<int>wymaga vector<char>się załadowania lub wykonania kodu.
lipiec

Odpowiedzi:

607

Krótka odpowiedź brzmi „ponieważ C ++ się zmieniło”. Tak, pod koniec lat 70. Stroustrup zamierzał stworzyć ulepszone C z funkcjami OOP, ale to już dawno. Do czasu ustandaryzowania języka w 1998 r. Nie był to już język OOP. To był język paradygmatu. Z pewnością miał pewne wsparcie dla kodu OOP, ale miał także nałożony kompletny język szablonu, pozwalał na metaprogramowanie w czasie kompilacji, a ludzie odkryli ogólne programowanie. Nagle OOP nie wydawało się aż tak ważne. Nie wtedy, gdy możemy napisać prostszy, bardziej zwięzły i wydajniejszy kod za pomocą technik dostępnych za pośrednictwem szablonów i ogólnego programowania.

OOP nie jest świętym Graalem. To uroczy pomysł, który w latach 70. był lepszy od języków proceduralnych, kiedy został wymyślony. Ale szczerze mówiąc, to nie wszystko. W wielu przypadkach jest niezdarny i pełny i tak naprawdę nie promuje kodu wielokrotnego użytku ani modułowości.

Właśnie dlatego społeczność C ++ jest dziś znacznie bardziej zainteresowana programowaniem ogólnym i dlatego wszyscy zaczynają zdawać sobie sprawę, że programowanie funkcjonalne jest również dość sprytne. OOP samo w sobie nie jest ładnym widokiem.

Spróbuj narysować wykres zależności hipotetycznej STL „OOP-ified”. Ile klas musiałoby się o sobie dowiedzieć? Byłoby wiele zależności. Czy byłbyś w stanie dołączyć tylko vectornagłówek, bez wciągania, iteratora nawet iostreamwciągania? STL ułatwia to. Wektor wie o zdefiniowanym typie iteratora i to wszystko. Algorytmy STL nic nie wiedzą . Nie muszą nawet zawierać nagłówka iteratora, mimo że wszyscy akceptują iteratory jako parametry. Który jest więc bardziej modułowy?

STL może nie przestrzegać reguł OOP, jak definiuje to Java, ale czy nie osiąga celów OOP? Czy nie zapewnia możliwości ponownego użycia, niskiego sprzężenia, modułowości i enkapsulacji?

I czy nie osiąga tych celów lepiej niż w przypadku wersji OOP?

Jeśli chodzi o to, dlaczego STL został przyjęty w tym języku, wydarzyło się kilka rzeczy, które doprowadziły do ​​STL.

Najpierw szablony zostały dodane do C ++. Zostały dodane z tego samego powodu, dla którego generyczne zostały dodane do .NET. Dobrym pomysłem było pisanie takich rzeczy jak „kontenery typu T” bez wyrzucania bezpieczeństwa typu. Oczywiście wdrożenie, na którym się zdecydowali, było znacznie bardziej złożone i potężne.

Następnie ludzie odkryli, że dodany przez nich mechanizm szablonów był jeszcze potężniejszy niż oczekiwano. I ktoś zaczął eksperymentować z użyciem szablonów do napisania bardziej ogólnej biblioteki. Zainspirowany programowaniem funkcjonalnym i wykorzystujący wszystkie nowe możliwości C ++.

Przedstawił go komitetowi językowemu C ++, który przyzwyczaił się do niego, ponieważ wyglądał tak dziwnie i inaczej, ale ostatecznie zdał sobie sprawę, że działa lepiej niż tradycyjne odpowiedniki OOP, które musieliby uwzględnić w innym przypadku . Wprowadzili więc kilka zmian i zaadaptowali do standardowej biblioteki.

Nie był to wybór ideologiczny, nie był to wybór polityczny „chcemy być OOP czy nie”, ale bardzo pragmatyczny. Ocenili bibliotekę i zobaczyli, że działa ona bardzo dobrze.

W każdym razie oba powody, dla których wspominasz o STL, są absolutnie niezbędne.

Standardowa biblioteka C ++ musi być wydajna. Jeśli jest mniej wydajny niż, powiedzmy, równoważny ręcznie zwijany kod C, ludzie nie będą go używać. Obniżyłoby to produktywność, zwiększyło prawdopodobieństwo błędów i ogólnie było po prostu złym pomysłem.

A STL musi współpracować z typami pierwotnymi, ponieważ typy pierwotne są wszystkim, co masz w C, i są główną częścią obu języków. Gdyby STL nie działał z natywnymi tablicami, byłby bezużyteczny .

Twoje pytanie ma silne założenie, że OOP jest „najlepszy”. Jestem ciekawy, dlaczego. Pytasz, dlaczego „porzucili klasyczne OOP”. Zastanawiam się, dlaczego mieliby się tego trzymać. Jakie miałoby to zalety?

jalf
źródło
22
To dobry opis, ale chciałbym podkreślić jeden szczegół. STL nie jest „produktem” C ++. W rzeczywistości STL jako koncepcja istniała przed C ++, a C ++ okazał się być wydajnym językiem posiadającym (prawie) wystarczającą moc do programowania ogólnego, więc STL został napisany w C ++.
Igor Krivokon
17
Ponieważ wciąż pojawiają się komentarze, tak, wiem, że nazwa STL jest niejednoznaczna. Ale nie mogę wymyślić lepszej nazwy dla „części standardowej biblioteki C ++ wzorowanej na STL”. Faktyczna nazwa tej części standardowej biblioteki to po prostu „STL”, mimo że jest ona ściśle niedokładna. :) Tak długo, jak ludzie nie używają STL jako nazwy całej standardowej biblioteki (w tym IOStreams i nagłówków C stdlib), jestem szczęśliwy. :)
jalf
5
@einpoklum A co dokładnie zyskałbyś z abstrakcyjnej klasy podstawowej? Weź std::setjako przykład. Nie dziedziczy po abstrakcyjnej klasie bazowej. Jak to ogranicza twoje użycie std::set? Czy jest coś, czego nie można zrobić, std::setponieważ nie dziedziczy po abstrakcyjnej klasie bazowej?
fredoverflow
22
@einpoklum, proszę spojrzeć na język Smalltalk, który Alan Kay zaprojektował jako język OOP, kiedy wymyślił termin OOP. Nie miał interfejsów. OOP nie dotyczy interfejsów ani abstrakcyjnych klas bazowych. Czy powiesz, że „Java, która nie jest niczym innym, niż to, co wynalazca terminu OOP miał na myśli, jest bardziej OOP niż C ++, co również nie jest niczym innym, co miał na myśli twórca terminu OOP”? Masz na myśli to, że „C ++ nie jest wystarczająco podobny do Javy jak na mój gust”. To uczciwe, ale nie ma to nic wspólnego z OOP.
czerwiec
8
@MasonWheeler, gdyby ta odpowiedź była rażącym nonsensem, nie zobaczyłbyś dosłownie setek programistów na całym świecie głosujących +1 w tej sprawie, a tylko trzy osoby robią inaczej
panda-34,
88

Najbardziej bezpośrednią odpowiedzią na to, o co myślę, że pytasz / narzekasz, jest następująca: założenie, że C ++ jest językiem OOP, jest błędnym założeniem.

C ++ jest językiem opartym na wielu paradygmatach. Można go programować przy użyciu zasad OOP, można programować programowo, można programować ogólnie (szablony), a w C ++ 11 (wcześniej znany jako C ++ 0x) niektóre rzeczy można nawet programować funkcjonalnie.

Projektanci C ++ postrzegają to jako zaletę, dlatego twierdzą, że ograniczenie C ++ do działania jak język wyłącznie OOP, gdy programowanie ogólne rozwiązuje problem lepiej i, cóż bardziej ogólnie , byłoby krokiem wstecz.

Tyler McHenry
źródło
4
„a przy C ++ 0x niektóre rzeczy można nawet programować funkcjonalnie” - można je programować funkcjonalnie bez tych funkcji, po prostu bardziej dosłownie.
Jonas Kölker
3
@Tyler Rzeczywiście, jeśli ograniczysz C ++ do czystego OOP, zostaniesz z Objective-C.
Justicle
@TylerMcHenry: Po tym pytaniu stwierdziłem, że właśnie wypowiedziałem tę samą odpowiedź, co ty! Tylko jeden punkt. Chciałbym dodać fakt, że Biblioteka Standardowa nie może być używana do pisania kodu zorientowanego obiektowo.
einpoklum
74

Rozumiem, że Stroustrup początkowo preferował projekt kontenera w stylu „OOP” i w rzeczywistości nie widział innego sposobu, aby to zrobić. Alexander Stepanov jest odpowiedzialny za STL, a jego cele nie obejmowały „zorientowania obiektowego” :

To jest podstawowa kwestia: algorytmy są zdefiniowane na strukturach algebraicznych. Kolejne lata zajęło mi zrozumienie, że musisz rozszerzyć pojęcie struktury, dodając wymagania dotyczące złożoności do regularnych aksjomatów. ... Wierzę, że teorie iteratorów są tak samo ważne dla informatyki, jak teorie pierścieni lub przestrzeni Banacha są kluczowe dla matematyki. Za każdym razem, gdy patrzę na algorytm, staram się znaleźć strukturę, na której jest on zdefiniowany. Chciałem więc ogólnie opisać algorytmy. To właśnie lubię robić. Mogę spędzić miesiąc pracując nad znanym algorytmem, próbując znaleźć jego ogólną reprezentację. ...

STL, przynajmniej dla mnie, stanowi jedyny możliwy sposób programowania. Rzeczywiście różni się on od programowania w C ++, ponieważ został zaprezentowany i nadal jest prezentowany w większości podręczników. Widzicie, nie próbowałem programować w C ++, próbowałem znaleźć właściwy sposób postępowania z oprogramowaniem. ...

Miałem wiele fałszywych startów. Na przykład spędziłem lata próbując znaleźć zastosowanie do dziedziczenia i wirtuozów, zanim zrozumiałem, dlaczego ten mechanizm jest zasadniczo wadliwy i nie powinien być używany. Bardzo się cieszę, że nikt nie widział wszystkich pośrednich kroków - większość z nich była bardzo głupia.

(Wyjaśnia, dlaczego dziedziczenie i wirtuozeria - czyli projekt obiektowy „był zasadniczo wadliwy i nie należy go używać” w dalszej części wywiadu).

Kiedy Stepanov zaprezentował swoją bibliotekę Stroustrupowi, Stroustrup i inni przeszli herkulesowe wysiłki, aby wprowadzić go do standardu ISO C ++ (ten sam wywiad):

Wsparcie Bjarne Stroustrup było kluczowe. Bjarne naprawdę chciał STL w standardzie i jeśli Bjarne czegoś chce, dostaje to. ... Zmusił mnie nawet do wprowadzenia zmian w STL, których nigdy bym nie zrobił dla nikogo innego ... jest najbardziej samotną osobą, jaką znam. On załatwia sprawy. Chwilę zajęło mu zrozumienie, o co chodzi w STL, ale kiedy to zrobił, był gotów to przeforsować. Przyczynił się również do STL, broniąc poglądu, że więcej niż jeden sposób programowania był ważny - bez końca flak i hype przez ponad dekadę, i dążył do połączenia elastyczności, wydajności, przeciążenia i bezpieczeństwa typu w szablony, które umożliwiły STL. Chciałbym wyraźnie powiedzieć, że Bjarne jest wybitnym projektantem językowym mojego pokolenia.

Max Lybbert
źródło
2
Ciekawy wywiad. Jestem pewien, że przeczytałem go już jakiś czas temu, ale zdecydowanie warto było się z nim ponownie zapoznać. :)
jalf
3
Jeden z najciekawszych wywiadów na temat programowania, które kiedykolwiek czytałem. Mimo to pragnę więcej szczegółów ...
Felixyz
Wiele skarg, które wysuwa na temat języków takich jak Java („Nie można napisać w Javie ogólnej wartości max (), która przyjmuje dwa argumenty pewnego typu i ma zwracaną wartość tego samego typu”) dotyczyły tylko bardzo wczesnych wersji języka, zanim dodano generyczne. Nawet od samego początku wiadomo było, że generyczne zostaną w końcu dodane, chociaż (po ustaleniu wykonalnej składni / semantyki), jego krytyka jest w dużej mierze bezpodstawna. Tak, generycy w jakiejś formie są potrzebne, aby zachować bezpieczeństwo typów w statycznie pisanym języku, ale nie, to nie czyni OO bezwartościowym.
Some Guy
1
@SomeGuy Nie są to skargi na Javę per se. Mówi o „ standardowym” programowaniu OO SmallTalk lub, powiedzmy, Java ”. Wywiad pochodzi z późnych lat 90. (wspomina o pracy w SGI, którą odszedł w 2000 r. Do pracy w AT&T). Generyczne zostały dodane do Javy dopiero w 2004 roku w wersji 1.5 i są odchyleniem od „standardowego” modelu OO.
melpomene
24

Odpowiedź znajduje się w tym wywiadzie ze Stepanovem, autorem STL:

Tak. STL nie jest zorientowany obiektowo. Myślę, że orientacja obiektowa jest niemal tak samo mistyfikacją jak Sztuczna Inteligencja. Nie widziałem jeszcze interesującego fragmentu kodu pochodzącego od tych osób z OO.

StackedCrooked
źródło
Niezły klejnot; Czy wiesz z jakiego roku?
Kos,
2
@Kos, zgodnie z web.archive.org/web/20000607205939/http://www.stlport.org/... pierwsza wersja połączonej strony pochodzi z 7 czerwca 2001 r. Sama strona u dołu mówi o prawach autorskich 2001- 2008.
alfC
@Kos Stepanov wspomina o pracy w SGI w pierwszej odpowiedzi. Opuścił SGI w maju 2000 r., Więc prawdopodobnie wywiad jest starszy.
melpomene
18

Dlaczego czysty projekt OOP do biblioteki struktury danych i algorytmów byłby lepszy? OOP nie jest rozwiązaniem dla wszystkich rzeczy.

IMHO, STL to najbardziej elegancka biblioteka, jaką kiedykolwiek widziałem :)

na twoje pytanie

nie potrzebujesz polimorfizmu środowiska uruchomieniowego, STL ma tę zaletę, że implementuje bibliotekę przy użyciu polimorfizmu statycznego, co oznacza wydajność. Spróbuj napisać ogólny algorytm sortowania lub odległości lub jakikolwiek inny algorytm dotyczący WSZYSTKICH kontenerów! Twoje Sortowanie w Javie wywoływałoby funkcje, które są dynamiczne na n-poziomach do wykonania!

Potrzebujesz głupoty, takiej jak boks i rozpakowanie, aby ukryć paskudne założenia tak zwanych języków Pure OOP.

Jedyny problem, jaki widzę w STL i ogólnie szablonach, to okropne komunikaty o błędach. Które zostaną rozwiązane przy użyciu Koncepcji w C ++ 0X.

Porównywanie STL do kolekcji w Javie jest jak porównywanie Taj Mahal do mojego domu :)

ARAK
źródło
12
Co, Taj Mahal jest mały i elegancki, a twój dom jest wielkości góry i kompletnego bałaganu? ;)
jalf
Pojęcia nie są już częścią c ++ 0x. Niektóre komunikaty o błędach można pominąć za pomocą static_assertbyć może.
KitsuneYMG
GCC 4.6 poprawił komunikaty o błędach szablonów i uważam, że 4.7+ są z nim jeszcze lepsze.
David Stone
Koncepcja jest zasadniczo „interfejsem”, o który prosił PO. Jedyną różnicą jest to, że „dziedziczenie” pojęć jest niejawne (jeśli klasa ma wszystkie odpowiednie funkcje składowe, jest automatycznie podtypem pojęcia), a nie jawne (klasa Java musi jawnie zadeklarować, że implementuje interfejs) . Jednak zarówno ukryte, jak i jawne podtytuły są poprawnymi OO, a niektóre języki OO mają niejawne dziedziczenie, które działa podobnie jak Koncepcje. Mówi się więc w zasadzie: „OO do bani: używaj szablonów. Ale szablony mają problemy, więc używaj Koncepcji (które są OO)”.
Some Guy
11

typy szablonów powinny podążać za „koncepcją” (Iterator wejściowy, Iterator do przodu itp.), w której rzeczywiste szczegóły koncepcji są definiowane całkowicie przez implementację funkcji / klasy szablonu, a nie przez klasę typu używane z szablonem, który jest nieco anty-użycie OOP.

Myślę, że źle rozumiesz zamierzone użycie pojęć według szablonów. Na przykład Iterator do przodu jest bardzo dobrze zdefiniowaną koncepcją. Aby znaleźć wyrażenia, które muszą być poprawne, aby klasa mogła być iteratorem do przodu, oraz ich semantykę, w tym złożoność obliczeniową, zapoznaj się ze standardem lub na stronie http://www.sgi.com/tech/stl/ForwardIterator.html (aby zobaczyć to wszystko, musisz użyć linków do Input, Output i Trivial Iterator).

Ten dokument jest doskonale dobrym interfejsem, a „rzeczywiste szczegóły koncepcji” są zdefiniowane właśnie tam. Nie są one zdefiniowane przez implementacje Iteratorów do przodu, ani nie są zdefiniowane przez algorytmy wykorzystujące Iteratory do przodu.

Różnice w obsłudze interfejsów między STL i Javą są trzykrotne:

1) STL definiuje prawidłowe wyrażenia za pomocą obiektu, podczas gdy Java definiuje metody, które muszą być wywoływalne na obiekcie. Oczywiście prawidłowe wyrażenie może być wywołaniem metody (funkcji członka), ale nie musi tak być.

2) Interfejsy Java są obiektami wykonawczymi, podczas gdy koncepcje STL nie są widoczne w środowisku wykonawczym, nawet przy RTTI.

3) Jeśli nie sprawdzisz poprawnych wymaganych poprawnych wyrażeń dla koncepcji STL, pojawi się nieokreślony błąd kompilacji, gdy utworzysz szablon z tym typem. Jeśli nie uda się wdrożyć wymaganej metody interfejsu Java, pojawi się komunikat o błędzie kompilacji.

Trzecia część dotyczy sytuacji, gdy podoba Ci się rodzaj „pisania kaczego”: interfejsy mogą być niejawne. W Javie interfejsy są dość wyraźne: klasa „jest” Iterowalna tylko wtedy, gdy mówi, że implementuje Iterable. Kompilator może sprawdzić, czy wszystkie podpisy jego metod są obecne i poprawne, ale semantyka jest nadal niejawna (tj. Jest albo udokumentowana, czy nie, ale tylko więcej kodu (testy jednostkowe) może powiedzieć, czy implementacja jest poprawna).

W C ++, podobnie jak w Pythonie, zarówno semantyka, jak i składnia są niejawne, chociaż w C ++ (i w Pythonie, jeśli masz preprocesor silnego pisania), otrzymujesz pomoc od kompilatora. Jeśli programista wymaga jawnej deklaracji interfejsów podobnej do języka Java przez klasę implementującą, wówczas standardowym podejściem jest użycie cech typu (a wielokrotne dziedziczenie może zapobiec zbyt szczegółowemu określeniu). W porównaniu z Javą brakuje jednego szablonu, który mogę utworzyć z moim typem i który skompiluje się wtedy i tylko wtedy, gdy wszystkie wymagane wyrażenia będą poprawne dla mojego typu. To powiedziałoby mi, czy zaimplementowałem wszystkie wymagane bity „zanim go użyję”. To wygoda, ale nie jest to podstawa OOP (i nadal nie testuje semantyki,

STL może, ale nie musi, być wystarczająco OO dla twojego gustu, ale z pewnością oddziela interfejs czysto od implementacji. Brakuje mu zdolności Java do refleksji nad interfejsami i inaczej raportuje naruszenia wymagań interfejsu.

możesz powiedzieć tej funkcji ... oczekuje, że iterator będzie tylko patrząc na jej definicję, w której musisz spojrzeć na implementację lub dokumentację ...

Osobiście uważam, że typy ukryte są siłą, jeśli są właściwie stosowane. Algorytm mówi, co robi ze swoimi parametrami szablonu, a implementator upewnia się, że te rzeczy działają: jest to dokładnie wspólny mianownik tego, co powinny robić „interfejsy”. Ponadto w przypadku STL jest mało prawdopodobne, abyś używał, powiedzmy, std::copyna podstawie znalezienia swojej deklaracji przekazywania w pliku nagłówkowym. Programiści powinni opracowywać to, co funkcja bierze na podstawie dokumentacji, a nie tylko podpisu funkcji. Dzieje się tak w C ++, Python lub Java. Istnieją ograniczenia dotyczące tego, co można osiągnąć za pomocą pisania w dowolnym języku, a próba pisania na klawiaturze w celu zrobienia czegoś, czego nie robi (sprawdź semantykę) byłaby błędem.

To powiedziawszy, algorytmy STL zwykle nazywają parametry swoich szablonów w sposób, który wyjaśnia, która koncepcja jest wymagana. Ma to jednak na celu dostarczenie użytecznych dodatkowych informacji w pierwszym wierszu dokumentacji, a nie uczynienie deklaracji przekazywanych bardziej pouczającymi. Jest więcej rzeczy, które musisz wiedzieć, niż można zawrzeć w typach parametrów, więc musisz przeczytać dokumenty. (Na przykład w algorytmach, które przyjmują zakres wejściowy i iterator wyjściowy, są szanse, że iterator wyjściowy potrzebuje wystarczającej „przestrzeni” na pewną liczbę wyników na podstawie wielkości zakresu wejściowego i być może zawartych w nim wartości. Spróbuj mocno to wpisać. )

Oto Bjarne na temat wyraźnie zadeklarowanych interfejsów: http://www.artima.com/cppsource/cpp0xP.html

W przypadku generics argumentem musi być klasa wywodząca się z interfejsu (odpowiednikiem interfejsu C ++ jest klasa abstrakcyjna) określonego w definicji rodzaju ogólnego. Oznacza to, że wszystkie ogólne typy argumentów muszą pasować do hierarchii. Nakładające niepotrzebne ograniczenia na projekty wymagają nieuzasadnionego przewidywania ze strony programistów. Na przykład, jeśli napiszesz ogólną, a ja zdefiniuję klasę, ludzie nie będą mogli użyć mojej klasy jako argumentu dla twojej ogólnej, chyba że będę wiedział o interfejsie, który określiłeś i wyprowadził z niego moją klasę. To jest sztywne.

Patrząc na to odwrotnie, za pomocą pisania kaczego można zaimplementować interfejs, nie wiedząc, że interfejs istnieje. Albo ktoś może napisać interfejs celowo, tak aby klasa go zaimplementowała, po skonsultowaniu się z dokumentami, aby zobaczyć, że nie prosi o nic, czego jeszcze nie zrobiłeś. To jest elastyczne.

Steve Jessop
źródło
Na wyraźnie zadeklarowanych interfejsach dwa słowa: klasy typów. (Co już Stepanow rozumie przez „koncept”.)
piątek
„Jeśli nie sprawdzisz poprawnych wymaganych poprawnych wyrażeń dla koncepcji STL, pojawi się nieokreślony błąd kompilacji, gdy utworzysz szablon z tym typem.” - to nieprawda. Przekazanie do stdbiblioteki czegoś, co nie pasuje do koncepcji, jest zwykle „źle sformułowane, nie wymaga diagnostyki”.
Yakk - Adam Nevraumont
To prawda, że ​​grałem szybko i luźno z terminem „ważny”. Chodziło mi tylko o to, że jeśli kompilator nie może skompilować jednego z wymaganych wyrażeń, wówczas coś zgłosi.
Steve Jessop
8

„OOP oznacza dla mnie tylko przesyłanie wiadomości, lokalne przechowywanie i ochronę oraz ukrywanie procesów państwowych oraz ekstremalne późne wiązanie wszystkich rzeczy. Można to zrobić w Smalltalk i LISP. Możliwe są inne systemy, w których jest to możliwe, ale Nie jestem ich świadomy ”. - Alan Kay, twórca Smalltalk.

C ++, Java i większość innych języków są dalekie od klasycznego OOP. To powiedziawszy, argumentowanie za ideologiami nie jest zbyt produktywne. C ++ nie jest w żadnym sensie czysty, więc implementuje funkcjonalność, która wydaje się mieć w tym czasie pragmatyczny sens.

Ben Hughes
źródło
7

Firma STL rozpoczęła działalność z zamiarem zapewnienia dużej biblioteki obejmującej najczęściej używany algorytm - w celu zapewnienia spójnego zachowania i wydajności . Szablon stał się kluczowym czynnikiem umożliwiającym wdrożenie i osiągnięcie celu.

Aby podać kolejne odniesienie:

Al Stevens przeprowadza wywiad z DDJ Alexem Stepanovem:

Stepanov wyjaśnił swoje doświadczenie zawodowe i wybór dotyczący dużej biblioteki algorytmów, która ostatecznie przekształciła się w STL.

Powiedz nam coś o swoim długoterminowym zainteresowaniu programowaniem ogólnym

..... Potem zaproponowano mi pracę w Bell Laboratories, pracując w grupie C ++ nad bibliotekami C ++. Zapytali mnie, czy mógłbym to zrobić w C ++. Oczywiście nie znałem C ++ i oczywiście powiedziałem, że mogę. Ale nie mogłem tego zrobić w C ++, ponieważ w 1987 C ++ nie miał szablonów, które są niezbędne do włączenia tego stylu programowania. Dziedziczenie było jedynym mechanizmem pozwalającym uzyskać ogólność i nie było wystarczające.

Nawet teraz dziedziczenie w C ++ nie ma większego zastosowania w programowaniu ogólnym. Porozmawiajmy dlaczego. Wiele osób próbowało wykorzystać dziedziczenie do implementacji struktur danych i klas kontenerów. Jak wiemy teraz, było niewiele udanych prób. Dziedziczenie w C ++ i związany z nim styl programowania są drastycznie ograniczone. Niemożliwe jest wdrożenie projektu, który zawiera tak trywialną rzecz jak równość. Jeśli zaczniesz od klasy podstawowej X w katalogu głównym swojej hierarchii i zdefiniujesz w tej klasie wirtualny operator równości, który przyjmuje argument typu X, następnie wyprowadzasz klasę Y z klasy X. Jaki jest interfejs równości? Ma równość, która porównuje Y z X. Używając zwierząt jako przykładu (ludzie OO kochają zwierzęta), zdefiniuj ssaka i uzyskaj żyrafę od ssaka. Następnie zdefiniuj wiązanie funkcji członka, gdzie zwierzę łączy się ze zwierzęciem i zwraca zwierzę. Następnie wyprowadzasz żyrafę ze zwierzęcia i, oczywiście, ma ona funkcję partnera, gdzie żyrafa łączy się ze zwierzęciem i zwraca zwierzę. To zdecydowanie nie to, czego chcesz. Chociaż kojarzenie może nie być bardzo ważne dla programistów C ++, równość jest. Nie znam żadnego algorytmu, w którym nie jest używana żadna równość.

yowkee
źródło
5

Podstawowy problem z

void MyFunc(ForwardIterator *I);

jak w bezpieczny sposób uzyskać rodzaj rzeczy, którą zwraca iterator? W przypadku szablonów jest to wykonywane w momencie kompilacji.


źródło
1
Cóż, ja też: 1. Nie próbuję tego zdobyć, ponieważ piszę ogólny kod. Lub: 2. Pobierz za pomocą dowolnego mechanizmu refleksyjnego, który oferuje obecnie C ++.
einpoklum
2

Przez chwilę pomyślmy o standardowej bibliotece jako w zasadzie bazie danych kolekcji i algorytmów.

Jeśli studiowałeś historię baz danych, niewątpliwie wiesz, że na początku bazy danych były w większości „hierarchiczne”. Hierarchiczne bazy danych bardzo ściśle odpowiadały klasycznemu OOP - a konkretnie odmianie z jednym dziedzictwem, takim jak stosowane przez Smalltalk.

Z czasem stało się jasne, że hierarchiczne bazy danych można wykorzystać do modelowania prawie wszystkiego, ale w niektórych przypadkach model z pojedynczym spadkiem był dość ograniczający. Jeśli miałeś drewniane drzwi, warto było spojrzeć na nie albo jako drzwi, albo na kawałek surowca (stal, drewno itp.)

Tak więc wymyślili bazy danych modeli sieciowych. Bazy danych modeli sieciowych bardzo ściśle odpowiadają wielokrotnemu dziedziczeniu. C ++ całkowicie obsługuje wielokrotne dziedziczenie, podczas gdy Java obsługuje ograniczoną formę (możesz dziedziczyć tylko z jednej klasy, ale możesz także implementować tyle interfejsów, ile chcesz).

Zarówno bazy danych modelu hierarchicznego, jak i modelu sieci przeważnie przestały być używane ogólnego przeznaczenia (choć kilka pozostaje w dość specyficznych niszach). W większości przypadków zostały one zastąpione relacyjnymi bazami danych.

Wiele powodów, dla których relacyjne bazy danych przejęły, to wszechstronność. Model relacyjny jest funkcjonalnie nadzbiorem modelu sieciowego (który z kolei jest nadzbiorem modelu hierarchicznego).

C ++ w dużej mierze podążył tą samą ścieżką. Zgodność między pojedynczym spadkiem a modelem hierarchicznym oraz między wielokrotnym spadkiem a modelem sieci jest dość oczywista. Zgodność między szablonami C ++ a modelem hierarchicznym może być mniej oczywista, ale i tak jest dość ścisła.

Nie widziałem tego formalnie, ale wierzę, że możliwości szablonów są nadzbiorem tych zapewnianych przez wielokrotne dziedziczenie (co jest wyraźnie nadzbiorem pojedynczego dziedziczenia). Jedyną trudną częścią jest to, że szablony są w większości związane statycznie - to znaczy, że całe wiązanie odbywa się w czasie kompilacji, a nie w czasie wykonywania. Jako taki, formalny dowód, że dziedziczenie stanowi nadzór nad możliwościami dziedziczenia, może być nieco trudny i złożony (lub nawet niemożliwy).

W każdym razie myślę, że to jest prawdziwy powód, dla którego C ++ nie używa dziedziczenia dla swoich kontenerów - nie ma prawdziwego powodu, aby to zrobić, ponieważ dziedziczenie zapewnia tylko podzbiór możliwości zapewnianych przez szablony. Ponieważ szablony są w niektórych przypadkach koniecznością, równie dobrze można ich używać niemal wszędzie.

Jerry Coffin
źródło
0

Jak robisz porównania z ForwardIterator *? To znaczy, w jaki sposób sprawdzasz, czy przedmiot, którego szukasz, jest tym, czego szukasz, czy przeszedłeś?

Przez większość czasu używałbym czegoś takiego:

void MyFunc(ForwardIterator<MyType>& i)

co oznacza, że ​​wiem, że wskazuję na MyType i wiem, jak je porównać. Chociaż wygląda jak szablon, tak naprawdę nie jest (brak słowa kluczowego „szablon”).

Tanktalus
źródło
możesz po prostu użyć operatora <,> i = tego typu i nie wiedzieć, co to są (chociaż może nie to, co miałeś na myśli)
lhahne
W zależności od kontekstu mogą one nie mieć sensu lub mogą działać dobrze. Trudno powiedzieć, nie wiedząc więcej o MyType, co, przypuszczalnie, użytkownik wie, a my nie.
Tanktalus
0

To pytanie ma wiele wspaniałych odpowiedzi. Należy również wspomnieć, że szablony obsługują otwarty projekt. Przy obecnym stanie zorientowanych obiektowo języków programowania, do radzenia sobie z takimi problemami należy używać wzorca gościa, a prawdziwy OOP powinien obsługiwać wielokrotne dynamiczne wiązanie. Zobacz Open Multi-Methods dla C ++, P. Pirkelbauer i in.za bardzo ciekawe czytanie.

Innym interesującym punktem szablonów jest to, że można ich używać również do polimorfizmu środowiska wykonawczego. Na przykład

template<class Value,class T>
Value euler_fwd(size_t N,double t_0,double t_end,Value y_0,const T& func)
    {
    auto dt=(t_end-t_0)/N;
    for(size_t k=0;k<N;++k)
        {y_0+=func(t_0 + k*dt,y_0)*dt;}
    return y_0;
    }

Zauważ, że ta funkcja będzie działać również, jeśli Valuejest w pewnym sensie wektorem ( nie std :: vector, który należy wywołać)std::dynamic_array aby uniknąć pomyłek)

Jeśli funcjest mała, ta funkcja wiele zyska na wstawianiu. Przykładowe użycie

auto result=euler_fwd(10000,0.0,1.0,1.0,[](double x,double y)
    {return y;});

W takim przypadku powinieneś znać dokładną odpowiedź (2.718 ...), ale łatwo jest zbudować proste ODE bez elementarnego rozwiązania (wskazówka: użyj wielomianu w y).

Teraz masz dużą ekspresję funci używasz solvera ODE w wielu miejscach, więc twój plik wykonywalny jest zanieczyszczony instancjami szablonu wszędzie. Co robić? Pierwszą rzeczą, na którą należy zwrócić uwagę, jest to, że działa wskaźnik zwykłej funkcji. Następnie chcesz dodać curry, aby napisać interfejs i jawną instancję

class OdeFunction
    {
    public:
        virtual double operator()(double t,double y) const=0;
    };

template
double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction& func);

Ale powyższa instancja działa tylko dla double, dlaczego nie napisać interfejsu jako szablonu:

template<class Value=double>
class OdeFunction
    {
    public:
        virtual Value operator()(double t,const Value& y) const=0;
    };

i specjalizujemy się w niektórych popularnych typach wartości:

template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction<double>& func);

template vec4_t<double> euler_fwd(size_t N,double t_0,double t_end,vec4_t<double> y_0,const OdeFunction< vec4_t<double> >& func); // (Native AVX vector with four components)

template vec8_t<float> euler_fwd(size_t N,double t_0,double t_end,vec8_t<float> y_0,const OdeFunction< vec8_t<float> >& func); // (Native AVX vector with 8 components)

template Vector<double> euler_fwd(size_t N,double t_0,double t_end,Vector<double> y_0,const OdeFunction< Vector<double> >& func); // (A N-dimensional real vector, *not* `std::vector`, see above)

Gdyby funkcja została najpierw zaprojektowana wokół interfejsu, musiałbyś dziedziczyć po ABC. Teraz masz tę opcję, a także wskaźnik funkcji, lambda lub dowolny inny obiekt funkcji. Kluczem tutaj jest to, że musimy mieć operator()()i musimy być w stanie korzystać z niektórych operatorów arytmetycznych na jego typie zwrotu. Zatem maszyna szablonów zepsułaby się w tym przypadku, gdyby C ++ nie miał przeciążenia operatora.

użytkownik877329
źródło
-1

Koncepcja oddzielenia interfejsu od interfejsu i możliwości wymiany implementacji nie jest nieodłączna od programowania obiektowego. Uważam, że to pomysł, który został stworzony podczas rozwoju opartego na komponentach, takiego jak Microsoft COM. (Zobacz moją odpowiedź na temat rozwoju opartego na komponentach?) Dorastając i ucząc się języka C ++, ludzie dostali dziedzictwo i polimorfizm. Dopiero w latach 90. ludzie zaczęli mówić „program do„ interfejsu ”, a nie„ implementacja ”i„ faworyzowanie ”składu obiektów nad„ dziedziczenie klas ”. (przy okazji oba cytowane z GoF).

Potem pojawiła się Java z wbudowanym śmieciarzem i interfacesłowem kluczowym i nagle stało się praktyczne oddzielenie interfejsu i implementacji. Zanim się zorientujesz, pomysł stał się częścią OO. C ++, szablony i STL poprzedzają to wszystko.

Eugene Yokota
źródło
Uzgodniono, że interfejsy to nie tylko OO. Ale zdolność polimorfizmu w systemie typów jest (tak było w Simuli w latach 60.). Interfejsy modułowe istniały w Modula-2 i Adzie, ale myślę, że działały one w układzie typów inaczej.
andygavin