Transkrypcja
Wstęp i luźne rozmowy
Cześć Wojtek.
Cześć Michał.
Kurde, dzisiaj jesteśmy ponoć na wizji, to trzeba się będzie zachowywać na nowo.
Specjalnie się za pomysł ubrałem trochę lepiej niż zwykle. Mam koszulkę w kąciku i w kącik. Spodnie mam, spodnie nie mam krótkie, ale w kanciapie ma być krótkie.
Tak.
Dodałem także tego.
To może skończmy z tą Javą.
A może to tak za dużo tej zabawy było? To może dzisiaj o C++?
Geneza pomysłu i popularność języków
O C++? Skąd ten pomysł?
Ten pomysł jest stąd, że ostatnio w projekcie jestem prawie od roku z C++ owym i… No i fajny taki ten projekt, także moglibyśmy coś opowiadać, jak to tam jest, coś spróbować pielęgnować, może skonfrontować C++ z Javą i zobaczyć, co z tego wyjdzie, bo tak wszyscy mówią: „A ten C++ taki słaby, co to w ogóle jakiś archaizm i takie tam głupoty, naleciałości z C”. O tym wszystkim pewnie zdążymy powiedzieć, ale jak on wygląda na tle Javy? Albo może, jak Java wygląda na jego tle? Zobaczymy, co tamten tłem jest.
No, ja tutaj oczywiście udałem zdziwienie, bo się troszkę przygotowałem i zasięgnąłem opinii. Jak tam wygląda właśnie udział rynku, czy w ogóle popularność…
…Opinii biegłych od opinii biegłych? Oczywiście.
Sam byłem ciekaw, bo rzeczywiście nie śledzę może tak bardzo regularnie, jak tam się rozkłada popularność akurat w tej chwili. Ja też muszę przyznać, że w ogóle nie śledzę.
Rewelacyjnie.
Porównanie C++ i Javy – dane statystyczne
To może tu przytoczyć parę takich liczb a propos porównania tych dwóch. Wiadomo, że jeśli chodzi o porównanie co do czego jest stosowane, to jest zupełnie inna bajka.
No tak, jak jest stosowane, to też nam wyjdzie z tych dalszych rozważań. Tak więc to jest zupełnie inna rzecz. Nie chcemy tutaj też w żaden sposób stawiać jednego przed drugim.
Oczywiście, oczywiście.
Chyba że może samo wyjdzie, albo będzie kontekst bardzo jasno zbudowany. Będziemy wiedzieć jasno, bo jakby nie, to się dowiemy, czemu ten C++ jest lepszy od Javy, na przykład.
Dokładnie, czyli jak słychać, tutaj nie będzie „pod tezę” odcinka, bo może nie jest to…
To się okaże na koniec, nie jest napisane, czy jest, czy nie jest.
No tak, tak, tak. Cały czas spoglądamy na to w leasing. Mam zakusy do zakupu fotela. Nie wiem, co to ma, ale co poradzić.
Dobra, tak więc wracając do tematu, rzeczywiście zasięgnąłem troszkę opinii, jak to, jak to mniej więcej wygląda. Jednym z takich indeksów, które od kilku już dziesięciu lat sprawdzają popularność języków w wyszukiwarkach internetowych jest TIOBE Index.
Tak, TIOBE Index. Nawet ciężko powiedzieć, jak to się wymawia, ale to taki indeks, który właśnie sprawdza popularność języków programowania właśnie w wyszukiwarkach. Czyli jak często czegoś szukamy, co, powiedzmy, przekłada się jakoś tam na popularność danego języka.
Stack… Stack Overflow uwzględnia też…
Stack Overflow ma swój własny, fajny survey, który co roku wydaje i też tam zostanie podsumowane w troszkę bardziej szczegółowych kategoriach. Natomiast ciekawym aspektem tego indeksu jest to, że po raz pierwszy w grudniu zeszłego roku, 2022, C++ wyprzedził właśnie w tym rankingu popularności Javę i wskoczył na trzecie miejsce, a Java spadła na czwarte. Indeks wprowadzono od 2001, więc po raz pierwszy od dwudziestu lat i w historii tego indeksu taka roszada nastąpiła. Co prawda, pewnie na drugie miejsce, a na pierwszym assembler.
No, blisko. Natomiast różnica z bazą nieznaczna, bo to mówimy o niecałym pół procenta popularności w kontekście wszystkich searchy, które akurat ten indeks gdzieś tam indeksuje, więc nie jest jakaś znacząca różnica. Więc można powiedzieć, że wciąż idą łeb w łeb i gdzieś to jest na granicy błędu statystycznego. Natomiast jest to jakiś, można powiedzieć, proces. Zależy, kto robi ankietę. Tutaj zdaje się, że robią to użytkownicy, którzy wyszukują, więc zakładam, że liczba jakby tych wyszukiwań rzeczywiście była bardzo duża.
Natomiast Stack Overflow wspomniany wcześniej ma coś takiego jak Survey, taką ankietę coroczną, gdzie zadaje bardzo dużo ciekawych pytań i pyta użytkowników Stack Overflow. Jest to, zdaje się, ich około siedemdziesiąt tysięcy. Odpowiedzi było w ankiecie w zeszłym roku, więc próbka jest też dość miarodajna. No i oczywiście baza jest też miarodajna, bo to wiadomo, są najczęściej ludzie z branży, że tak powiem, i jakoś tam aktywnie pewnie szukających rzeczy na temat „co mi tu nie działa, co mi tu nie działa”. I o ile w takim pierwszym rankingu popularności, to tutaj Java wciąż prowadzi z C++ – to jest 33% kontra 22% – natomiast to jeszcze to badanie, ono pokazuje po prostu co i jak często jest używane, czyli niekoniecznie tutaj jeszcze mamy wybór tego użytkownika, tylko po prostu kto w jakim języku pracuje. I tak się przedstawia, że Java wciąż ma te 10%, że tak powiem, większej tutaj używalności.
Natomiast bardzo ciekawym jest ta druga statystyka, która mówi, jakie języki użytkownicy kochają, a jakich nie lubią. I tutaj z kolei w kategorii „kochają” prowadzi już C++ o niecałe 3%. Co prawda, jest 48% kontra 45%. Natomiast to jest taki fajny wskaźnik, że jeśli nie mamy wyboru, w sensie po prostu pracujemy w czymś, to Java jest popularniejsza, a zdaje się, że użytkownicy, przynajmniej z takiego swojego własnego doświadczenia czy punktu widzenia, delikatnie preferują C++, a trochę mniej Javę, ale to też jest niewielka różnica.
Przez ten Garbage Collector…
Podejrzewam, że to może być jeden z powodów, ale jest to nieznaczna różnica. I taki trzeci, jest to ostatni, jedna z ostatnich sekcji tej ankiety, jest właśnie średnia roczna pensja, gdzie C++ też prowadzi o 4 tysiące dolarów rocznie. Więcej można zarobić.
Cztery tysiące na drodze nie leżą w C++.
Tak, to jest 64 tysiące kontra 68 tysięcy dolarów. Mówimy tu głównie o rynku amerykańskim, to jest rocznie. Natomiast co do pierwszego komputera, jest to po prostu 6 tysięcy. To jednak troszkę brakuje, więc Groovy jest najbardziej wygrywającym jednak.
No jednak, jednak tak, ale skali nie było. Była, ale nie pamiętam, gdzie się plasowała na tej skali, więc zabrakło dla mnie skali.
Gdzieś tam był, ale musiałbym sobie teraz zerknąć w cały ten interfejs. To tylko pokazuje, że generalnie jeśli chodzi o takie market share i popularność, są to bardzo zbliżone pozycje. Można znaleźć to czy inne badanie, które pokaże, że jeden czy drugi język jest mniej lub bardziej popularny i to się widać troszkę. Są pewne fluktuacje, pewne zmiany. Nie widać raczej jakiegoś wielkiego trendu na korzyść jednego czy drugiego, więc widać, że to są po prostu języki równie często wybierane, chociaż oczywiście do zupełnie różnych zastosowań.
Zastosowania języków i historia projektu
No właśnie, może mają… mają troszkę inne zastosowania.
Tak, historycznie Java została chyba powołana do życia po to, żeby w sferze biznesowej…
…Tam można było rozszyfrować min C++.
Tak, gdzieś słyszałem tą wersję. Tylko pytanie, co jest prostsze i kiedy? Bo jeżeli przyjdzie do strojenia, jak wiemy, na przykład pod kątem tych właśnie domniemań optymalizacyjnych, co to o nich opowiadaliśmy ostatnim razem, to tam po prostu można dostać oczopląsu.
Pytanie, jak często to musimy robić, bo to też nie jest codzienność i większość pewnie użytkowników tego nie potrzebuje na co dzień.
Większość i do codziennej pracy takiej po prostu „widłowej”. Co to w ogóle…
Tak, tak, ale jak przychodzi do takiego Netfliksa czy Blizzarda, czy co tam byś to chciał jakiegoś dużego na tym puścić, no to już wtedy niestety, ale pewnie i te parametry rozgrzewające maszynkę trzeba jakoś dobrze dobrać. Albo na przykład jakieś zastosowanie, stosowania serverless tych takich lambda owych, to albo po prostu kompilować to do natywnych binarek, albo naprawdę przysiąść do tych parametrów, bo tam tego jest…
Z kolei flag w C++ kompilatora też nie brakuje.
No właśnie, właśnie dokładnie takich problemów niskopoziomowych, które można natknąć się pisząc nawet „Hello Worlda”.
Modułów również.
Można. To zaraz chętnie opowiem.
Aha, ale pisałeś coś więcej niż „Hello World”? Przez ten rok sam takiego większego „Hello Worlda” nie, właściwie nie mogę tam oczywiście zdradzać szczegółów zanadto projektu, jak to przy każdym projekcie.
To chociaż branża.
Branża – lotnictwo cywilne, czyli poważna branża.
Branża poważna, bo niedobrze jest, jak się samoloty na niebie zatrzymują. Wtedy pasażerowie są niezadowoleni. Czyli jednak dobrze, jakby to wszystko działało.
Na trzymają, spadają.
No właśnie. Albo w ogóle nie ruszają z lotniska, bo tam są srogie opłaty za takie cuda.
No tak.
Więc lepiej, jak jest, żeby to wszystko działało. Napisanie aplikacji biznesowej w C++, która ma tak z trzydzieści lat miejscami, to naprawdę jest ciekawe, bo napisanie jej pewnie było jakimś tam pomysłem, jak ją zaczynali pisać, ale kontynuowanie tego w C++ i utrzymywanie całego tego grajdołka to jest bardzo ciekawy pomysł.
Zdaje się, że ogólnie to jest problem też branży lotniczej jako takiej, że te systemy są mocno przestarzałe, począwszy od systemów rezerwacji i tego typu, bardziej user-centric, a skończywszy właśnie na takich systemach, które gdzieś tam pod spodem zarządzają. Kiedyś słyszałem takie porównanie, że sporo z tych systemów, jeśli chodzi o wiek, to dosłownie bliżej im do czasów braci Wright, kiedy samolot został skonstruowany, niż od tamtego czasu systemów tworzenia do naszych czasów. Więc to pokazuje, jak stare są to systemy.
Można to poczuć troszkę, jak się w czymś takim koduje, bo faktycznie domena tam jest niekiepsko zakręcona. Jest po prostu całe mrowie parametrów przy najdrobniejszych tam zagadnieniach i nawet, nawet coś tak pozornie niegroźnego jak obsługa notyfikacji, które są przesyłane po prostu po całym świecie, emitowane przez centra notyfikacji rozmieszczone w bardzo wielu miejscach. Obsługa tego w taki sposób, żeby te aplikacje nie ginęły, żeby docierały tam, gdzie trzeba i żeby jeszcze zrobiono z nimi użytek, to też jest zagadnienie. No, na taki nie najmniejszy system można by powiedzieć, i to jeszcze dobrze, jak to działa w czasie zbliżonym do rzeczywistego, bo nie ma bezpośrednio wymagań na to, że to musi w czasie rzeczywistym działać. To takie wymagania są narzucone na części takie bardziej „w powietrzu”, można powiedzieć, nie? Obsługuje bezpośrednią komunikację z samolotem albo systemy w samolocie, albo systemy gdzieś na płycie lotniska, które muszą ze wszystkich tych czujników, które tam są, zbierać sygnały, żeby generować odpowiednie ostrzeżenia. Na przykład, nie wiem, że nie można podchodzić do lądowania, bo wiatr jest aktualnie teraz za duży albo pod złym kątem dla pasa. Tego typu rzeczy, nie. To musi być obsługiwane naprawdę starannie, ale cała reszta rzeczy z tej domeny jest dość wymagająca, bo jednak jest… jest dużo parametrów, na które trzeba zwrócić uwagę i podejrzewam, że właśnie z tego powodu te systemy tak sobie rosną od tamtych niepamiętnych czasów braci Wright, bo coś zostało zbudowane, a później się tylko odbudowuje na tym, bo to co jest, to już działa. Wymiana tego czegoś w takim systemie, takim legacy C++ owym wszystkim od 35 lat, to już jest zadanie wysoce nietrywialne.
No, pewnie tak, bo jednak mimo wszystko chyba łatwiej mi wyobrazić wymianę jakiegoś mainframe’a w banku, gdzie nawet jeśli coś pójdzie nie tak, to powiedzmy, też później przed komisją, coś…
…Jednak…
…W najgorszym razie stracimy pieniądze, tak jak było, natomiast w branży lotniczej w najgorszym razie niestety ludzie mogą stracić życie, więc troszkę rozumiem, że tutaj opór przed zmianami jest duży. Ale to zdaje się osobna anegdota z branży finansowej, tutaj niedaleko. Po tym kryzysie z 2008, jak komisja senacka w Stanach przysłuchiwała szefów Fedu i tam jeden z senatorów zadał pytanie: „Gdzie wam się podziało?” Tam najpierw mi przytoczył jakieś fakty, nie podliczenie, później wyszło, że mają w plecy, czyli 8 miliardów dolarów. Bo on się pyta tego szefa Fedu, gdzie im się to podziało. On tak: „Gdzieś nam się zawieruszyło”.
No, nie da się zrobić.
Dokładnie. No a tutaj nie. Tutaj jednak trzeba przyłożyć wagę do tego, czemu akurat C++.
No pewnie, w tamtych czasach nie za bardzo. Nie było, to tylko system powstawał, to później, jak już popatrzymy, twórcy Javy popatrzyli, po co oni to napisali w tym C++? „Choć napiszemy im Javę”. To i później to już jakoś poleciało.
Porównanie zastosowań i różnic w filozofii języków
No tak. No to czyli z tego zestawienia wychodzi, że Java i C++ teraz idą łeb w łeb? Bo pomijając tam aspekt jakichś zastosowań, że ten jest bardziej do mniej biznesowych rzeczy, a ona jest do rzeczy biznesowych, to nasze… To mimo wszystko popularność… Każdy sobie jakoś tam trzyma, bo każdy ma swoją taką działkę. Rzeczywiście Java taką bardziej biznesową. C++ na pewno, jeśli chodzi o gamedev szeroko rozumiany, to tam na pewno króluje. Gry, czy silniki i wszystko, co jest bliżej jednak metalu i bliżej takich potrzeb wydajnościowych, to wiadomo, że C++ będzie tutaj oczywistym wyborem. To są takie dość jasne. Jeszcze C# tam wrzucają. C# jest do tego.
Ale to teraz tak trochę może same trzewia silników pewnie są dalej, albo w C++, albo w C ze wstawkami w asemblerze. O czym John Carmack swoje wiedział.
No, więc ciężko, ciężko. Pewne przyczółki pewnie zdobyć już, już ustanowiono dobrze. Natomiast tak jak mówisz, tutaj ta branża, w której teraz masz ten projekt, ale też oczywiście miałem okazję wiele lat pracować w Javie. Jak tak wstępnie wygląda to Twoje porównanie? Co… co… Czy jesteś w stanie jakoś to porównać, czy to drobnostek? Bo wszyscy mieliśmy tu rozpiskę na tą stronę.
Bo też warto pewnie powiedzieć, że w Polsce, zdaje się, C i C++ swego czasu było bardzo na uczelniach popularne. To był główny język wyboru. I tak za moich czasów, czy też za naszych czasów, to, to dwadzieścia lat temu, co się zaczynało od C, to była podstawa i zawsze. Ja w sumie też myślałem, że zawsze jakoś tak naturalnie do tego C++ przejdę i będę w tym pracował, robiąc coś. No a później przyszła pierwsza praca w Javie i poszło tak.
Programowanie niskopoziomowe i zarządzanie pamięcią
Ok. Gdzieś tam pod koniec studiów zaczęła się pojawiać i rzeczywiście tam u mnie się pojawiła na piątym roku i tam trzeba było jakieś głupoty pisać, takie co już dawno były obcykane na C++, jak… W C++ do tego momentu to już zdążyłem napisać. No, przynajmniej ze trzy programy.
Dużo, dużo.
Nie no, trochę więcej, bo było trochę projektów i też były takie fajniejsze rzeczy. Było… Znaczy, dobra, było. W pewnym momencie nawet można było sobie popróbować, jak to się język C zachowuje w takiej aplikacji klient-serwer. To po to, żeby poznać zagadnienia związane z soketami, ale głównie to te niżej poziomów są, niżej poziomu możliwości ich języków. Wiemy, że C++ bazuje na C, wywodzi się z niego. Zresztą Java to właśnie zgodnie z tą legendą, ona się wywodzi z nich obydwóch naraz, bo powstała jako odpowiedź na C++, a ponoć w pewnym sensie. Więc niskopoziomowe to było coś, z czym się można było zetknąć już na samym początku. I później przyszły czasy Javy i się okazało, że na co to komu to na niskopoziomowych, jak teraz się wysokopoziomowo programuje? I tam niech sobie ci tam to tam, niech oni tam sobie robią w tych niskopoziomowych, a my to będziemy obiektowo jechać.
No, to był taki naturalny trend, zdaje się. Wiadomo, te lata 90., jakby odkrycia takiego paradygmatu obiektowego i że wszystko będzie obiektowe i w ogóle ktoś rządzi światem, no to tak nam właśnie fajnie w to wpisała, zapisała się i później pociągnięto to do granic.
Gdzie jeszcze te obliczenia można przesyłać przez modem?
Bo tak, tak, technologia nie była jeszcze gotowa na obiektowość, która wtedy wyprzedziła swoje czasy. Jest oczywiście pewna popularność, nawet tutaj na tym mocno zyskamy. Natomiast z takiego właśnie porównania prywatnego, ja bym zaczął od takiego wstępu teoretycznego na początek, żeby zbudować grunt pod to porównanie.
No i właśnie już tutaj pierwsza taka przesłanka jest, że język C++, jako że ma za rodzica język C, a ten z kolei jest bardzo blisko już metalu i praktycznie za pan brat z asemblerem, to język C++ też siłą rzeczy umożliwia programowanie niskopoziomowe i zapewne to przesądza o tym, że używa się go do takich niskopoziomowych rzeczy. Gdzie do jakiegoś, do pewnego momentu, nie pamiętam niestety jakimiś datami strzelać, bo nie sprawdzałem, a z pamięci to mi też wyparowało. Ale do pewnego momentu panowało takie przeświadczenie, że nie opłaca się pisać czegoś, co ma być ultra wydajne w języku C++, bo on ma swoje narzuty. Już w ogóle zapomnijmy w tym momencie. To były takie czasy, że trzeba to pisać w C ze wstawkami asemblerowymi, żeby było wydajnie. Tak samo było w Turbo Pascalu. Kiedyś napisał Turbo Pascalu to każdy szanujący się programista Turbo Pascal zawsze robił wstawki asemblerowe, nawet jeśli to miał zrobić tylko proste budowanie, żeby pokazać, że…
Ja tutaj umiem optymalizować i to zawsze należy.
To co wiadomo, że trzeba zmienną wsadzić do rejestru i wtedy rata, bo tutaj to się dzieje tak elegancko. Zawsze na zielono było podświetlone w edytorze i wtedy wiedziałeś, że jesteś pro, bo masz wstawkę w tym takim razie tekstowo, w sensie tym dosowym.
Tak, tak, tak.
No dobra, nie będę nazwy, ale miałem na końcu języka. No więc mamy to te niskopoziomowe w C++, gdzie Java już od samego początku w ogóle nie była do takiej rzeczy pomyślana. Tam o wstawce asemblerowej to zapomnij. Nawet wstawek byte kodowych nie dali. Szkoda, czasem by człowiek wstawił taki żarcik oczywiście.
Ale oprócz tego, że język C++ jest niskopoziomowy, to też nie wolno zapominać, że on jest wieloparadygmatowy i to też się nam pojawi gdzieś w rozważaniach. Bo w tym języku od samego początku, oprócz tego, że może sobie strzelić w kolano na tysiąc sposobów, na tym niższym poziomie można było zrobić tysiąc jeden na wyższym. I na przykład paradygmat funkcyjny jest dostępny od samego początku w C++, tylko że jest niewykorzystywane. C++ został pomyślany jako język obiektowy i tam też zastrzelono trochę z wielokrotnym, wielobazowym dziedziczeniem, bo wiadomo, że ta słynna historia z rombem dziedziczenia to właśnie stąd pochodzi. Chociaż pewnie inne języki, które coś takiego mają, jak najbardziej doświadczają tego typu ciekawostek.
Ale tutaj w C++…
Czasami się podaje jako wadę Javy, że nie ma tylko tego dziedziczenia. I co wtedy zrobić? Jak mam zbudować moją wspaniałą strukturę? Gdzie mam…
Wyliczać przykłady, gdzie na przykład jak to już ktoś coś tam programował, trochę zobaczył kawałek świata, to to na przykład przychodzi do C++ i pyta: „Gdzie to są interfejsy?”. No, nie ma. Są tak naprawdę, bo w C++ wszystko jest, tylko trochę inaczej.
No właśnie taki, żeby o tej niskopoziomowości można było, w sensie, żeby można było wyjaśnić pewne różnice w mechanizmach tych dwóch języków, to najpierw może sobie przypomnimy, co to w ogóle jest ta pamięć i jak to jest, że się tam do niej sięga. Jak ktoś programował cokolwiek kiedykolwiek w jakimkolwiek asemblerze, a na przykład mikrokontrolery, to jest bardzo wdzięczna rzecz, żeby zacząć. To jest taka… W ogóle mikrokontrolery widziane od strony asemblera, bo w języku C to już trochę jest ukryte. Ale jak sobie oprogramować taki mikrokontroler w asemblerze, to nie ma innej opcji, jak tylko grzebanie po komórkach pamięci.
Się wtedy okaże, że mamy ileś tam kilobajtów, pewnie. No to w takich prostych zastosowaniach, a megabajty w poważniejszych. Tych komórek pamięci, które są poukładane w jakieś tam słowa. To słowo ma długość taką, jaką ma magistrala w procesorze. Czyli mamy procesory 8-bitowe, 16-bitowe, 32-bitowe, 64-bitowe i pewnie więcej, tylko 64-bitowe teraz królują w takich zastosowaniach poważnych, nawet 32-bitowe. Do niedawna, to już trochę do dawna, ale tak jeszcze pamiętamy o tym, że na przykład jak wyszła Amiga 1200, to już wyszło z procesorem 32-bitowym. Jeśli to był po prostu cios, sens maszyny, cios dla blaszaka ówczesnego. Ale szybko się potem podniósł, bo tym chcieli w Amidze.
No cóż, zachłysnęli się trochę swoim sukcesem.
Jacek Trzmiel coś nie tego. Nie do… nie jest wadą.
No tak, że mamy to słowo maszynowe, które odpowiada szerokością magistrali w procesorze po to, żeby można było te komórki pamięci zaciągać pojedynczymi instrukcjami. No i są instrukcje ładujące tą pamięć i do jakichś tam buforów podręcznych i później procesor sobie z tych buforów bierze te kolejne słowa i coś tam sobie z nimi robi. I tak jest skonstruowany każdy program praktycznie, to zresztą omawialiśmy to w poprzednich odcinkach. Jak tam rozkładać na czynniki pierwsze maszynę wirtualną i jej optymalizację. I tutaj jest analogicznie. Tylko że kiedy już zaczynamy mówić nie o nim zmiennych, a o samej pamięci, to już się zaczynają nam rysować takie ciągłe bloki tych komórek pamięci.
No i w C++, jako że wychodzi on sobie z C, mamy coś takiego jak wskaźnik na void – void*
. W ogóle mamy wskaźnik, gdzie w Javie nie ma.
No właśnie. Czyli po pierwsze, żeby…
Różnica, tak. Żeby, a wskaźnik to i tak już jest coś więcej niż adres, znaczy pojęciowo, niż goły adres w pamięci, bo adres w pamięci to jest liczba. Wskaźnik to jest coś w stylu takiej zmiennej, która jest tą liczbą i pokazuje dokładnie tą komórkę o tym numerze w tym bloku. Więc po co nam te wskaźniki i dlaczego w danym momencie sobie w C++ nie brakuje wskaźnik do czegoś, czy coś do wskaźnika? Powiedz, czy taka tablica, taka tablica to będzie wskaźnik po prostu C i w C++ też może być. Oczywiście jak będziemy typować ładnie i będziemy ją sobie indeksować i tam będą wszystko, wszystko będzie wyglądało tak, jak trzeba, to nie musimy się do tego odwoływać jak do takiego nie wiadomo czego. Ale jak to zrzutujemy na void*
, to z naszej tablicy robi się taki ciąg komórek pamięci na odpowiednim rozmiarze. Wiadomo, był tam wskaźnik, to wskaźnik to nie jest takie byle co, to ma swoją arytmetykę. Jak masz wskaźnik na coś cztero-bitowe, czterobitowego, to jak dodasz do tego 1, to co? To przeskoczy 4, bo on po prostu odmierza te komórki pamięci. Stąd są to wszelkie problemy związane z błędem adresowania pamięci, wyskoczenia gdzieś tam poza…
Tak jest, więc to bardzo łatwo sobie tutaj, tak jak wspomniałem, na tysiąc sposobów strzelić, czyli przez kolano.
I potężny możemy sobie zrobić potężną…
Odpowiedzialność.
Potem odpowiadam, tak, tak, tak. No i wpadłem na pomysł, że zamiast takich wskaźników lepiej się posługiwać referencjami. Więc język C++. Tutaj pan Bjarne stanął na wysokości zadania, bo ten człowiek to jest po prostu chyba jakiś geniusz, taki język wynalazł, że działa i że jak się, jak się w nim zachowa odpowiednie zasady BHP, to nawet da się pisać tam tak, żeby się to nie za bardzo chciało spieprzyć.
No właśnie, jeden z takich mechanizmów, tylko że on jest też bardziej upierdliwym mechanizmem, jeżeli się tamtego BHP nie stosuje odpowiednio, jest mechanizm referencji. Ta referencja to jest coś. Jest taki uchwyt do zmiennej. Musi być zainicjowana. To jest taka bardzo ważna cecha referencji. Czyli nie można sobie zrobić referencji do niczego. Jak robimy referencje, to musimy coś mieć najpierw w garści. Stykamy to w te referencje i wtedy już możemy to sobie przekazywać gdzieś dalej. Więc jeżeli widzimy, że metoda przyjmuje referencje albo najlepiej referencje do czegoś stałego, czyli gdzieś tam się pojawia słowo const
, to już mamy pewność, że nie dość, że to jest solidna zmienna, to jeszcze jej nie zepsujemy, bo kompilator nie pozwoli. Co jest czasem oczywiście upierdliwe strasznie, bo…
No bo jeszcze trzeba ten const
mieć na metodzie, żeby to się dobrze tam zachowywało. No i takie tam są jeszcze różne ciekawostki, a jak już odpukać chcemy coś jednak na tej referencji zrobić, no to ten const
, który już tam siedział w bibliotece, wybitnie nam to przeszkadza. No i się zaczynają jakieś tam rzutowania, fascynacje. No, ale mamy sobie taki dwojaki sposób dostępu do zmiennych, w ogóle do zawartości pamięci, poprzez wskaźniki i poprzez referencje. Do wskaźników jeszcze trochę, trochę wrócimy, bo tego… No bo tam właśnie raz, że przy operatorach to nam się przyda, a dwa jeszcze może nawiążemy do tego, że właściwie to chyba już teraz możemy nawiązać do tego modelu pamięci, który w Javie jest taki bardzo niedostępny. I tam mamy już tylko referencje. I to referencje są czymś takim pośrednim pomiędzy referencją z C++ a wskaźnikiem z C.
Czyli wskaźnik możemy sobie zainicjować dowolną wartością, nawet taką, która wykracza gdzieś poza nasz dozwolony obszar pamięci i jeżeli coś takiego nam się stanie, to będziemy mieli błąd typu segmentation fault. To jest błąd. Klasyczny błąd, który po prostu zabija wszystko. Jak jest segmentation fault, to znaczy, że program próbował dobrać się do komórki pamięci spoza dozwolonego obszaru.
Takim klasycznym odpowiednikiem tego segmentation fault po stronie Javy jest NullPointerException, czyli po prostu niezainicjalizowana zmienna, która planowo przyjmuje wartość null
. Jeżeli jej nie ustawimy, to możemy w ten sposób otworzyć nową… Czyli mamy taki przypadek.
NullPointerException to jest jeszcze w miarę rzecz niegroźna, bo bardzo łatwo wytropić przyczynę czegoś takiego. I tutaj ukłon dla geniuszu twórców Javy, że po prostu potrafi tak prostą rzecz zrobić. I fajnie. Takiego segmentation faulta się tropi trochę trudniej, bo to, co się objawia, nie tym, że wskaźnik ma wartość 0, bo jak ma wartość 0, to jeszcze spoko, ale jak ma wartość taką totalnie randomową, no to już gorzej.
Nie, ciężej się dowiedzieć, skąd on taką wartość ma.
Bo nie jesteśmy w stanie powiedzieć jasno, czy to z zakresu adresów pamięci naszej aplikacji, czy skąd to się wzięło.
I tak, dokładnie. A tu mogliśmy na przykład nie wiem, dodać sobie do niego jakąś wartość przez przypadek, za dużo o 13 i wyszliśmy poza naszą stronę pamięci. I wtedy kaplica. Bo te mechanizmy to one nie wybaczają.
Te mechanizmy kontroli pamięci, czy chociażby takie rzeczy, mogą też bez naszej wiedzy się zdarzyć przez wszelkie problemy z przepełnieniem bufora, czyli też wszelkie rzeczy, gdzie nie przewidzimy inputu, na przykład ze strony użytkownika, który może nam ten bufor…
Dać, to też. To znajduje też odzwierciedlenie w tych takich słynnych atakach związanych z przepełnieniem bufora.
Dokładnie. Dokładnie. Także tutaj trzeba na pewno bardzo dobrze dbać o to, żeby jednak tego bufora nie przepełnić, bo w Javie poleci nam ArrayIndexOutOfBoundsException. I możemy popełniać te bufory. Zresztą, co za problem.
A to po prostu, jeżeli użyjemy chociażby klasyczny przykład, to jest łańcuch znaków, taki oldskulowy, który jest po prostu tablicą znaków, a znak w najprostszej wersji, kiedy mówimy o kodowaniu ANSI, to jest jeden bajt, więc możemy sobie wziąć łańcuch, który jest zrzutowany na void*
– wskaźnik na nic i tam na nim coś zrobić, z łatwością przeskoczyć nawet ten znak kończący, bo takie łańcuchy w C++ to jest taki łańcuchy z C, takie właśnie najbardziej hardkorowe. One są ciurkiem bajtów zakończonym znakiem 0, czyli bajt o wartości 0 stoi sobie na końcu takiego łańcucha i my musimy wykryć to miejsce, w którym on jest. Jak je przeskoczymy, to już mamy po zawodach.
Ziemia niczyja i płaszczyzna do ataku jest otwarta.
Dokładnie. I to właśnie wykorzystują te różne podstępne ataki, które oczywiście tam community Linuksa skrupulatnie tępi. Już prawie nic nie zostało, więc czekamy na kolejną koniunkcję sfer, bo metody wszystkie potwory znalezione.
No, także arytmetyka wskaźników to jest fajne narzędzie, kiedy się go nie używa praktycznie. Są wszelako zastosowania, kiedy się tego używa, bo na przykład taki wskaźnik może sobie spokojnie pełnić rolę iteratora. Bo i w na przykład algorytmach biblioteki standardowej istnieje takie pojęcie jak iterator. Jest tam używane po prostu nagminnie. Tam się zakresy kolekcji odmierza iteratorem początku, który jest pierwszym wskaźnikiem na pierwszy, pierwszy element, gdzie już w tym ujęciu z algorytmicznych wskaźnik będziemy traktować troszkę inaczej, niż wskazanie palcem na tą konkretną komórkę pamięci, bo to jest po prostu wskazanie takie. Taka abstrakcja wskazania na któryś tam obiekt w kontenerze jakimś obiektów, nie? Mamy jakąś kolekcję dowolnego typu i tenże wskaźnik pokazuje na element tego konkretnego typu. Stąd właśnie przy obliczeniach jakichś arytmetycznych do tego, do tej wartości, tej i tego adresu w pamięci, na którą wskazuje, jest dodawana wielokrotność rozmiaru tego czegoś, co jest przechowywane, czyli tego typu. I stąd konieczność operatora sizeof
, którego nam zaoszczędzono w Javie.
No tak, bo nie potrzebują. Tak, nie potrzebują niskopoziomowych, ale nie chcą wiedzieć, ile obiektów zajmuje w pamięci.
Pomijając już to, stracilibyśmy się widząc wartość, jaką się zawiera. Kiedyś, bo ja byłem cwany i zrobiłem ich milion i podzieliłem przez milion.
No tak, tak, tak. No, wtedy już widać, obiekt ma…
Optymalizacja niskopoziomowa w C++
No, także tu w C++ można zobaczyć, ile w ogóle obiekt ma. Można jeszcze w dodatku zażądać, jak on ma być rozłożony w pamięci. Czyli jeżeli widzimy, że nasza klasa ma jakiegoś int
a, jakiegoś znaka, jakąś tablicę czegoś tam, to jeszcze możemy sobie tak pożonglować kolejnością tych zmiennych z tych memberów w środku, żeby nam się to wszystko zamknęło w określony sposób na granicach słów. Nie są oczywiście do tego tam flagi kompilatora. Czyli jak tego tak nie zrobimy, to kompilator może to zrobić za nas. Czyli możemy się też w debuggerze zdziwić, jakbyśmy sobie to wzięli do kompa z pamięci, że kolejność w klasie pewnie będzie inna. Ale możemy sobie też samemu wymusić te kolejności i możemy jeszcze dopasować alignment, tak zwane, tych poszczególnych elementów, że tego nie zrobimy.
A tutaj, że to jest taka niskopoziomowa optymalizacja, która idzie w parze z odczytywaniem przez te mechanizmy dostępu do pamięci kolejnych tak naprawdę wierszy tego cache’a. No, wtedy tam wiadomo, że wszystko musi być wyrównane do adresów podzielonych przez wielokrotność długości słowa maszynowego. To zależy od tego, jak często potrzebujemy takich właśnie. My tak często nie potrzebujemy, ale tutaj na przykład twórcy sterowników do kart graficznych, albo jakiś takich wybitnie wymagających mechanizmów potrzebują pamięci. Liczy się każda tam… sekunda dokładna.
Optymalizacja i polimorfizm w C++
No, no, sekundy się tam liczą, zabawy także. Tam już można to po prostu na kartce w kratkę narysować i to się będzie zgadzać. Oczywiście jeśli się unika… dziedziczenia tam, czyli dziedziczenia można nie unikać, ale funkcji wirtualnych lepiej unikać, jakiegoś polimorfizmu. Czyli po prostu można sobie zastosować język C++, tak trochę będąc jeszcze jedną nogą w C. Chociaż w języku C da się robić też polimorfizm i na przykład Quake jest tak napisany. Tutaj pan John stanął na wysokości zadania. Da się zrobić, bo to jest ten sam mechanizm po prostu, tylko, w sensie, ten sam mechanizm da się zaimplementować, który robi nam wywołania wirtualne. Tylko tutaj w C++ robi nam to kompilator i mamy…
No to mamy z dynki.
Ale, ale warto pamiętać, że jak na przykład mamy chociaż jedną funkcję, która ma modyfikator virtual
, to już dostajemy tablicę funkcji wirtualnych w tym konkretnym obiekcie jako pierwsze pole – to jest wskaźnik, czyli ma sizeof(void*)
. Także z tego można sobie po prostu bardzo niskopoziomowo wywnioskować, jak będzie wyglądał rozkład naszych obiektów w pamięci, jeżeli potrzebujemy na przykład… Trochę wyprzedzam, bo zaraz to operator będziemy mówić. Ale gdybyśmy chcieli zaimplementować wzorzec puli obiektów, to możemy sobie taką pulę niskopoziomowo w pamięci zorganizować właśnie dobierając ten tak zwany alignment. I te obiekty będą leżały nam w tych adresach kolejnych, na jakich nam zależy, po to, żeby te odczyty były szybsze, żeby te, te linie też zawierały dokładnie to, co chcemy i żeby tak się działo. Jeżeli na przykład nasz obiekt ma długość 7, to wystarczy dodać padding do niego. Kiedyś to się robiło dodając jakiegoś pustego int
a, który był niewykorzystywany, jakiś taki ledwo co nazwany. Przylegał tam sobie w tej klasie i tyle, ale wtedy sizeof
się zgadzał. Podzielne przez 4.
Czy to jest też taka ciekawa heurystyka, że jeśli nasza aplikacja nie potrzebuje mieć dobrego alignmentu obiektów w pamięci, to wtedy nie potrzebujemy C++, a Javie to wystarczy. W Javie nic z tego co statyczne. To tam o alignment nam garbage collector zadba. Bo to jest taka znacząca różnica. Jak widać, rzeczywiście zupełnie inny poziom.
Całkiem inny świat, inny świat. Rzeczywiście, no w Javie jest to bardzo mocno przykryte i nie mamy na to żadnego wpływu.
Kto wie, co gorsze. Kto wie, co gorsze. Wszystko zależy od zastosowań. To jest jasne.
No, ale to widać chociażby po takim jednym prostym aspekcie tych języków. Jaka to jest niesamowita różnica. I ile kontroli zostało tym programistom odebrane, ale też ile spokoju zyskali. I do spokoju zyskali.
To jest jednak programista Javy, jednak śpi spokojnie.
Zdecydowanie.
Pewnie więcej jest siwych tych od C++, a na pewno patrząc na staż, jakby…
Nawet. Ale to ci młodzi szybciej śmieją się na nowo. Teraz tempo życia jest za duże.
No, ale to wcale nie jest wina języka. Można sobie na pewno strzelić w stopę, w kolano albo w obydwa naraz, jak się tam nieodpowiednio z tego skorzysta. Ale są takie sytuacje, kiedy to jest akurat korzystne. No i właśnie to rozgraniczenie zastosowań tych języków pokazuje tę sytuację tak naprawdę w tych aplikacjach, w tych zastosowaniach. Ultra wydajne tam, gdzie są wymagania. Bo co? Nie oszukujmy się, jeżeli nasz feedback loop ma z tym modem ultra śmigać jak marzenie, to ten sterownik grafiki to tam musi wyciskać siódme poty z tego procesora. A ten procesor to jest jeszcze macierzą w dodatku, nie? Więc jak już mamy takie przetwarzanie w potoku tej grafiki, to zapomnij tam o obiektach. Tam ciurkiem leci wszystko w pamięci i najczęściej jest to, z tego co słyszałem czytałem, tak naprawdę to jest to stabilizowane wszystko, czyli bardziej to przypomina strukturą bazy danych, tylko w takim troszkę innym ujęciu, niż jakiekolwiek obiekty.
Shadery i przyszłe tematy
No bo tego typu operacje są znacznie lepiej wspierane przez procesory, zwłaszcza procesory kart graficznych, więc cały sprzęt naokoło procesora jest zoptymalizowany po to, żeby właśnie w tych operacjach jak najwięcej wycisnąć wydajności. Stąd te wszystkie wiersze cache’a. Stąd te wszystkie dopasowania, ten cały alignment. Jakby tak zejść troszkę na meandry obok języka C++ i w języki shaderów wejść, to tam się dopiero kosmos robi. Tam jest praktycznie czysta matematyka i jakieś takie totalnie bazowe konstrukty językowe, jak po prostu deklarowanie prymitywów. I tyle. I na tym się robi cały processing i robi się je w takich malutkich tkankach, które później się układają w potoki. I właśnie na tych procesorach macierzowych tam sobie lecą na tych gridach.
No, ale my nie o tym.
Skończymy następować…
W następnym odcinku, za jakiś tydzień, albo w którymś z następnych, bo zaraz nam się ten skończy, co?
No tak, że tego. Mamy te niskopoziomowe wskaźniki. Możemy je zapakować w coś takiego bardziej obiektowego jak smart pointery. No i wtedy już, wtedy wkraczamy w ten świat obiektowy.
I tu wkraczamy już w nieco wyższe, że tak powiem, obszary tego języka, bogatsze w treść, najbezpieczniejsze przede wszystkim. Ale z takich jeszcze niskopoziomowych konstruktów, coś, co nie występuje w Javie, to jest na przykład przeciążanie operatorów.
Przeciążanie operatorów w C++
Moje ulubione. Wiadomo, każdy kto miał zajęcia z C++, to musiał przeciążyć operator dodawania. Tam się jeszcze ++
, --
czasem. No i co i do czego? Na co komu? Po co? Ja muszę sobie chcieć przeciążyć taki operator?
Chcieć to sobie mogę, musieć mogę, bo mi prowadzący zajęcia zadał, że mam napisać program, który wykorzystuje przeciążanie operatorów i tyle. Ale na przykład operator ++
to jest operator, nie ten z nazwy języka, nie? Ale w Javie też go mamy ++
, --
. Minusy są operatorami inkrementacji i one występują w dwóch wersjach: pre i post. Czyli jak zamieścimy go przed zmienną, to najpierw następuje inkrementacja tej zmiennej, a później przypisanie, to znaczy inaczej – dostęp do tej… Najpierw następuje inkrementacja lub dekrementacja ++
, --
, a później dostęp do samej tej wartości. A jak postfix, to najpierw odczytujemy, czy zapisujemy, odczytujemy tą wartość, a później coś z nią robimy, zwiększamy o 1 lub zmniejszamy.
No i jak mamy typy arytmetyczne? To jest wszystko spoko, bo mamy liczbę 7. Zrobimy na niej ++
i mamy w tym miejscu 8.
Rewelacja. Nie to, co…
Ty jako licznik pętli.
Co oczywiście w Javie jest wspierane bardzo dobrze.
Bardzo dobrze wspiera od samego początku, tak dobrze, że aż w końcu zrezygnowano z tego i zrobiono pętlę taką for
typu foreach
, ale jest wspierana jak najbardziej i jest to akceptowalne dla typu arytmetycznego. Jak najbardziej te operacje są naturalne dla typów arytmetycznych, ale my tu mamy pojęcie arytmetyka wskaźników. Na przykład w C++ nie mamy arytmetyki referencji w Javie, na szczęście. Ale skoro mamy tą arytmetykę wskaźników, to znaczy, że do wskaźnika można dodać coś. Nie radziłbym dodawać drugiego wskaźnika, bo wtedy się spod ptaka. Ale trzeba jeszcze rzutować.
Ale co to jest?
Rzutuje oczywiście, ale na przykład można takiego wskaźnika… Można go zaimplementować sobie, zdeklarować ++
, --
, zrobić to przed, zrobić po i można do niego dodać skalar, czyli na przykład coś tam + 10
, nie?
No i kiedy to ma sens? No, średni będzie miało sens, jeżeli będziemy do void*
dodawać 17, bo wylądujemy 17 bajtów dalej. Słów nie pamiętam. Przepraszam najmocniej, bo szczerze powiedziawszy, pointer to chyba ma zawsze rozmiar co najmniej 4. Tak mi się wydaje.
Możliwe, ale musiałbym to doczytać, bo mimo że już teraz blisko od roku jestem z powrotem w C++, nie dodawałem do void*
ostatnio niczego. Sobie przeliczyć, także mi się zapomniało.
W każdym razie dodawanie do takiego wskaźnika na nic gołego to jest średni pomysł, ale już na przykład jak mamy wskaźnik na jakiś obiekt w kontenerze, czyli tenże właśnie iterator, to już wtedy ma to sens, bo na przykład dodamy sobie 3 i przeskakujemy na trzeci element, licząc od tego, do którego, na który byliśmy ustawieni teraz, nie? I de facto tak działają wszystkie algorytmy standardowe, które po prostu coś robią na kontenerach. Kontener to jest odpowiednik kolekcji w Javie. Tutaj jest, funkcjonuje od lat nazwa kontener. I biblioteka STL, czyli standardowa biblioteka szablonów. Do tego musimy nawiązać. Teraz, to wszystko w swoim czasie. To ta biblioteka bardzo mocno, przy okazji operacji na kolekcjach, właśnie bazuje na tym range’u, który jest parą iteratorów. Czyli tak naprawdę mamy wskaźniki na któryś tam element w kolekcji i wskaźnik na coś, co jest za ostatnim elementem. Czyli na taki pusty, wirtualny element, który jest już za kolekcją, żeby porównać, czy przypadkiem, jak inkrementowaliśmy ten wskaźnik, ten, który jedzie po kolekcji, to już nie wyskoczyliśmy poza… poza tenże.
Poza tego strażnika?
Poza tego strażnika? Tak jest. I ten… No i właściwie wszystkie, wszystkie kontenery udostępniają dwie metody: begin
i end
. begin
zwraca iterator do pierwszego elementu w kolekcji, a end
zwraca ten nieistniejący ostatni element i zawsze się robi, że coś jest różne… bieżący jest różny od end
. Podobnie zresztą w mapie, bo mapa nie może mieć końca. Jak najbardziej ma koniec. Więc jak robimy find
na mapie, dostajemy iterator do pary, która siedzi w tej mapie i ta para jest porównywana, na przykład z iteratorem końca mapy. Żeby się dowiedzieć, czy wcześniej wyskoczyliśmy.
Śmieszne to, ale przestawianie operatorów i oczywiście arytmetyka wskaźników jest niezmiernie ciekawa. Natomiast czas leci, a nie do tego przeciążania, bo tutaj akurat przeciążanie jest fajne, jeśli mówimy o naszych własnych obiektach, gdzie chcemy operator przeciążyć. Klasyczny przykład: modelujemy sobie pieniądze, walutę i możemy w łatwy sposób przeciążyć dodawanie, odejmowanie, czyli na przykład zaokrąglanie z takim samym braniem. To po prostu nasze obiekty.
To dobry przykład, bo właśnie wykorzystałeś coś, co ma jakiś sens arytmetyczny. To jest właśnie dalej mimo wszystko jakaś liczba. Ona jest tam, ona jeszcze czymś, ale ma to sens arytmetyczny.
No właśnie tak, bo takie pytanie: czy pod takie dodawanie arytmetyczne nie możemy sobie zamodelować pewnych rzeczy? Nie wiem. Załóżmy, obszar, mamy dwa profile i użytkownika. I chcemy przeprowadzić operację merge’a. Teoretycznie możemy sobie podciągnąć, jak zrobimy profile1 + profile2
. To jest merge. Tylko że tak naprawdę tutaj tracimy trochę chyba sedno tego problemu.
Do tego, dokładnie tego typu przeciążania, de facto tylko do…
Sztuka dla sztuki, sztuka dla sztuki. Sprawdza się on faktycznie tam, gdzie są działania matematyczne, natomiast wszelkie inne operacje de facto możemy modelować przez metody. I dlatego chyba nigdy w Javie albo prawie nigdy prawie nikomu nie brakowało.
Należy liczyć, że R mógłby mieć tego plusa. Przy tym ten zarzut. Troszkę to wygląda tak na bakcyla. Są metody.
Kontrprzykład w drugą stronę, bo add
albo update
dodaje tego void
tam trochę. Mogłoby być.
No, ale tylko pytanie, co byśmy tam dodawali? Drugi deck?
Update tak, ale z dolara byśmy dodawali, to już nie wiadomo jakie.
Pewnie trzeba było podać jednostkę, więc to jest takie troszkę niezdefiniowane i właśnie takie koszmarne. Może to jeszcze nie jest koszmarny przykład, ale jakieś takie mniejsze lub większe potworki powstają, jak ktoś się zachłyśnie tym przeciążeniem operatorów i przeciąży nie w tym kontekście operatora, który akurat mi się spodobał i już widziałem taki przykład, gdzie konstruujemy sobie query do bazy danych i parametry podajemy po operatorze procenta.
Po co?
A kto bogatemu zabroni przeciążyć? I tam operator procenta do tego, żeby tam dodawać? Jak sobie popatrzyłem w ten kodzik, to widzę stringa, nie? leci na SQL. A później go dzielimy przez parametry. Sens?
O żesz ty, grubo.
I szybki rzut oka na trzewia tego wynalazku i się okazało, że przeciążony operator jest addParam
po prostu.
No właśnie.
No czyli tam, tam akurat. No, może to jak już jest człowiek przyzwyczajony, już pierwszy szok minął. No to wtedy ok, no widzę to, czytam setny, tysięczny raz takie query. No to już widzę, że aha, no to parametry macie, w ogóle tym procentem nie przejmuję. Ale tak na pierwszy rzut oka to jest w ogóle totalnie nieczytelne.
Na studiach też bywały takie przykłady, że tam się robiło właśnie jakiegoś studenta, czy coś tam. Ma jakąś kartotekę studentów i trzeba było do niej dodać studenta. To był operator dodawania dla kartoteki, przeciążony, przyjmujący tego studenta. Coś jest w ogóle aberracją.
Dokładnie, bez znaczenia, bo metody też są po to, żeby miały nazwę i żeby oddawały nam troszkę tej esencji.
Semantyka.
Semantyka. A operatory matematyczne? No trudno, semantyka, jeśli mówimy o czymś innym niż działania matematyczne stricte.
Więc to jest takie troszkę… Tym bardziej, że tu w tym przykładzie ze studiów mieliśmy tą kartotekę i do niej dodawaliśmy coś. Czyli mamy pudełko. I dodajemy do niego element. Na matematyce coś takiego nie działa, bo dodajemy element do innego elementu tego samego typu albo podobnego jakiegoś konwertowanego.
No tak, tak to właśnie jest znowuż zrealizowane idealnie na typach arytmetycznych, tak jak pozostałe. Możemy to sobie oczywiście zamodelować struktury matematyczne. Tak jak na studiach przeciągało się jakieś pierścienie czy ciała, bo to oczywiście był też klasyczny przykład przeciążania, przeciążania operatorów. Oczywiście zaimplementować pierścień albo ciało. Waga podział ten pierścień nad ciałem K.
Tak, tak, jakieś takie konstrukcje. Bardzo ciekawe. Ja na tym się przeciążania operatorów uczyłem. Ale broń Boże! Broń! Nic nieprzyzwoitego z tym działem nie da. Przeciążyć operator, tylko przeciążyć.
Ale tak rzeczywiście w kontekście bardziej namacalnych obiektów tego typu rzeczy, no to tutaj troszkę ta arytmetyka nie ma chyba sensu.
Nie ma sensu.
No, ale są zastosowania dla przeciążania operatorów, które po prostu czynią wręcz ten język unikatowym, bo… No bo na przykład mamy taki iterator, to to jest też nawiązanie troszkę do porównania wzorców, bo iterator jest bardzo często wzorcem, bardzo często wykorzystywanym właśnie przy bibliotece algorytmów na kolekcjach. I jako że to jest wszystko pocięte na takie małe kawałeczki, czyli tutaj mamy jakąś kolekcję dowolnego typu, która jest rozumiana jako sekwencja obiektów, przy czym one nie muszą być sekwencyjnie poukładane tak jak w mapie na przykład, nie? Ale dalej jest to jakiś po prostu zbiór obiektów, gdzie też nie jest zbiorem, więc przepraszam za błądzenie w pojęciach. Czyli jest po prostu jakaś…
No, ale dobra, trzymajmy się tego pojęcia. Zbioru nie jest zbiór obiektów, czy też kolekcja. Niech będzie kolekcja, czy też kontener. Może dlatego kontener wymyślono? Nie, bo w kontenerze nie wiadomo, jak one leżą. W kolekcji mogą być poukładane, nie? A w kontenerze mogą być wyrzucone.
Więc jest ten kontener, jest iterator, który jedzie po tym kontenerze i co jeszcze jest? Jest na przykład operator „szufladki” taki, jak właśnie ten, ten wskaźnik. Mamy zmienną typu wskaźnik w C++. To żeby dobrać się do tego, co wskazuje, to tą szufladkę robimy, albo gwiazdka i wtedy wyłuskujemy ten obiekt, który tam jest pod spodem i do jego pól możemy się dobrać po kropce. Żeby nie robić tej gwiazdki, po to, bo mamy taki wskaźnik – siup strzałka i to on pokazuje na co pole, oczywiście na konkretne. I teraz taki iterator ma ten operator, te operatory przeciążone i wtedy ma to sens, bo ten iterator razu to jest wzorzec iterator, który jedzie po kolei po jakimś tam, po jakiejś kolekcji obiektów. A dwa, że jest wskaźnikiem jednocześnie na ten kolejny obiekt na przykład, czyli jako wskaźnik niczym się nie różni semantycznie od wskaźnika takiego gołego językowego, bo ma te same operatory na przykład.
Nie, tylko żeby, żeby taki iterator sobie napisać samemu, to trzeba właśnie te operatory poprzeciążać, bo jak się tego nie przeciąża, to kompilator nam wypluje takie chaszcze, że się po nocach śniły. A jak ktoś doczyta do końca, się dowie, że „one sorry, zabrakło tam operatora”. Może także operator gwiazdka, operator strzałka. Operatory ++
, --
. Podobnie +=
. To chyba nie są przeciążone, nie? Musiałbym sprawdzić, ale na przykład do… Na upartego można by sobie przeciążyć dla stringów, nie? Bo zwyczajnie, zwyczajowo przyjęto, że tam dodawanie łańcuchów znaków, konkatenacja po prostu jest robiona plusem. To co się może przydać to to jeszcze takie, jeszcze tam.
Stringi są o tyle jasne, że możemy, chociaż można. Minus tutaj, jakbyś zdefiniował? No nie.
Co minusa nie, ale + + +
? O, to mamy wybiórczość, albo minus taki łańcuch i to już jest dziura semantyczna.
Mnożenia i dzielenia też już nie zrobi. Nie zrobimy, więc to takie troszkę na siłę.
Dlatego w Javie nam tego nie brakuje. Tak, Javie tego nie brakuje w zupełności, podobnie jak wielokrotnego dziedziczenia też nie brakuje.
W Javie cały czas mamy interfejsy z metodami abstrakcyjnymi.
Operator new
i destruktory
No i mamy, mamy. Jest jeszcze jeden taki fajny operator. Właściwie dwa operatory w C++, które fajnie się przeciąża i one mają sens, to ich przeciążenie nawet duży sens mają, bo pierwszy z nich to jest operator new
. I teraz wracając do tego, do implementacji tego wzorca puli obiektów, jak byśmy chcieli sobie zrobić pulę obiektów. Albo inaczej, jak byśmy w ogóle chcieli zrobić sobie sprytny alokator, który nam zwraca… Tak jakbyśmy chcieli sobie zrobić sprytny alokator, który nam zwraca obiekty z naszej jakiejś sprytnej puli, bo mamy to wszystko tak optymalizowane, że w tej puli leżą już obiekty odpowiednio zainicjalizowane, przygotowane tylko po to, żeby je zwrócić. A jednak nie chcemy zaburzać tego normalnego sposobu. W sensie nie chcemy wprowadzać zmian do API, bo żeby to było sprytnie ukryte przed użytkownikiem, żeby nawet się nie domyślił, że jak dotknie nas ten operator na new
kontroler, to go zobaczy. Tak?
To zobaczy, że to idzie do tej implementacji nie systemowej, że w ogóle idzie do jakiejś implementacji, no i wtedy się może, może się połapać, że go trochę oszukali. Nie wszystko mu powiedzieliśmy, ale tak normalnie to ten kodzik, który by operował na jakichś tam obiektach zwykłych, on po prostu tak samo będzie działał na tych obiektach, nie? Więc przeciążenie… Po raz kolejny widać, że przeciążenie, sensowne przeciążenie operatora pozwala też wpisać się w jakiś standardowy algorytm. Gdybyśmy mieli jakiś algorytm, który po prostu zakłada alokację i niszczenie obiektów w trakcie, bo nie wiem, co tam robi, ale niech sobie robi jakieś skomplikowane obliczenia na jakichś dużych zbiorach danych, matematyczno-fizyczne, nie takie. Czyli mamy tam już abstrakcje na przykład fizyczne.
To pewnie w CERN by to mogli mieć albo na studiach byśmy to mogli mieć, jak byśmy tak fajniejsze tematy tych zajęć mieli, nie? Zresztą typ complex, czyli liczba zespolona, to jest przykład na coś takiego, że można by to było tak zrobić, tylko że akurat liczba zespolona to lepiej ją przez wartość przekazywać niż przez ten, niż alokować na stercie. Ale wyobraźmy sobie, że mamy jakieś duże pudełko na jakieś parametry do czegoś i to bierze udział w jakimś algorytmie. I jeszcze ten algorytm jest na tyle uniwersalny, że tych podtypów, tych, tych naszych parametrów jest po prostu ileś tam. Więc żeby tego wszystkiego nie zaburzać, możemy sobie ten algorytm napisać na wzór algorytmów standardowych, które zakładają i to jeszcze szablonowo, czyli korzystając z mechanizmu template’ów, które są po stronie Javy nazywanego generics, ale po stronie Javy dopiero teraz zyskują taką troszkę prawie pełnię możliwości względem tego, co C++ już miały u zarania samych template’ów. No to wtedy byśmy sobie napisali tak ogólnie, jak tylko to możliwe, bazując na podstawowych pojęciach i tych podstawowych operatorach. Ten algorytm i wtedy i każdy typ danych, na których ten algorytm by chciał pracować, musielibyśmy już dopasować do semantyki tego, czego oczekuje. Nie, to między innymi są te takie pokraczne komunikaty z kompilatora, że coś tam, coś tam nie znaleziono. Tylko że to ma gdzieś tak style i tam dopiero na końcu jest albo w środku zaszyte to, czego nie znaleziono i roi się od tych takich ukośnych nawiasów otwierających i zamykających.
Chociaż tutaj chyba też trzeba uważać, no bo potrafię sobie wyobrazić. Wrócę do tego new
i przeciążenia operatora. Określam zrobić z głową. Co Pan zrobi z głową, bo już widzę te implementacje singletonów, gdzie ktoś przeciąża sobie new
i pod spodem zwraca mi cały czas tą samą instancję. A przecież jednak semantyka new
trochę oznacza, że ja chcę nowy obiekt za każdym razem, a nie że ktoś mi to potem robi disabled, a ja nic o tym nie wiem. I sobie beztrosko na przykład robię coś, czego bym nie oczekiwał.
Tak, to zdecydowanie trzeba zwracać uwagę na to, że jest trochę niepewności, użytkownika w maliny. Przez ten przykład Singleton pokazał, że można by było tak zrobić, ale są lepsze sposoby, żeby zabronić konstruowania obiektu, na przykład ukrycie konstruktora w sekcji private.
Prywatny konstruktor to jest taka… To jest dość powszechnie spotykana.
Dobrze, wzorce jak najbardziej. Da się, to można zrobić. Tak, tak, tak.
Bardziej mi chodzi o takie…
Tak. Albo zaciemnienie obrazu.
Tak, można zaciemnić obraz bardzo łatwo. I to właśnie robiliśmy na studiach. Robiąc te dziwne przeciążenia operatorów. To to ma sens. Tylko zaciemnienie obrazu nie. Ale jak byśmy chcieli zrobić sprytny alokator, który na przykład z jakiegoś powodu chcemy od razu mieć zrealizowaną taką swoją własną stertę. Czasem są takie specyficzne zastosowania, nie? No to możemy sobie ten gigabajt pamięci od razu złapać, go trzymać. I te pule tych naszych obiektów, choćby były, mogą być dowolnego rozmiaru. Nieważne, że się dzieliły przez 4, to możemy sobie je tam tą, tą pulę porządkować i prowadzić swój własny wektor bitowy, który nam pokazuje co jest wolne, zajęte i wszystko idealnie, wszystko się spina, nie? I wtedy można sobie ten operator new
przeciążyć i co dostajemy? Dostajemy to, że język używa standardowego mechanizmu konstrukcji obiektu, alokacji pamięci przez new
. Ale w momencie, kiedy tworzymy obiekt tego typu, bo ten new
jest przeciążony właśnie dla tej klasy, której pulę byśmy chcieli sobie zrobić. Więc jeżeli robimy new
na czymkolwiek innym, to alokacja leci z takim standardowym operatorem ze sterty, o ile nie jest przeciążony.
Znowuż jakiś.
A jak robimy dla tego konkretnego typu danych, to alokacja leci z tej naszej puli, bo leci przez naszą implementację. Wtedy my sobie zwracamy ten obiekt, który akurat uważamy za stosowne. Ale gdy ten obiekt wychodzi z zakresu scope, to automatycznie jest wywoływany jego destruktor, więc on na przykład wraca nam do puli. Czyli mamy destruktor. A propos, ja to powinienem powiedzieć Ci na początku. No więc taki new
ma sens praktycznie w takich zastosowaniach, w innych nie za bardzo, bo on będzie mylący i ważne jest, że ten… Że jakoś trzeba ogarnąć tą, tą destrukcję też tych obiektów. Żeby jednak to sensownie działało, także to…
Kontrolujemy konstrukcję, to musimy też mieć możliwość kontrolowania destrukcji. Żeby jednak posprzątać po sobie. Robimy taki troszkę garbage collector…
…Gorączkowy.
Jakby troszkę.
Tylko życiodajny.
Tajniki. Bardzo. Dlaczego w ogóle kultura stosowania tych. Destruktor to już jest taki trochę nasz „na piechotę” robiony garbage collector, ale tak nie do końca, bo destruktor jest taki twór, którego nie mamy w Javie. Występuje on na przykład w Ruse, bo Ruse jest taki sobie, bardziej zbliżony do metalu. No i tam się też zarządza pamięcią z palca. Natomiast w Javie mamy garbage collector, który ma za zadanie nam odśmiecić tą pamięć, którą poalokowaliśmy, czego nie doświadczymy w C++, bo C++ nie jest zarządzany. My zarządzamy pamięcią w C++ i mamy to te wszystkie narzędzia właśnie po to, żeby tą pamięcią dobrze zarządzać, czyli między innymi jak już mamy ten obiekt działający, kiedy wychodzimy z danego zakresu, czyli z klamry info prosto w kodzie, to mamy pewność, że kompilator wstawi nam kodzik, który wywoła w tym momencie destruktor obiektu. I bardzo ładnie jest. I jeżeli mieliśmy jakieś zasoby zablokowane w tym obiekcie, trzymamy tam, nie wiem. Klasyczny przykład z otwieraniem połączenia do czegoś tam, gdzieś tam, nie? Albo alokowanie jakiejś struktury danych gdzieś tam grubszej, żeby w tym destruktorze to wszystko sobie wyczyścić, zwolnić tą pamięć i już nie. Jak tego nie zrobimy, to nam kompilator oczywiście krzyknie warning! Wiem, że tu nie mamy destruktora i wstawi nam tam standardowy destruktor, który nic nie robi, ewentualnie tam chyba wyczyści tak jakby była jakaś tablica. Tam chyba zwolni coś takiego, nie? Gdzieś niewłaściwie on zawody struktury innych obiektów. Ale jeżeli jakieś nietrywialne zasoby otwieraliśmy, tak jak te połączenia, to nam on tego nie zwolni. Ale jest na to sprytny sposób i też zaraz o to zahaczymy.
No, także wracając już i tak już uciekając z tego wątku operatorów, tu to ten new
jest takim dość przydatnym wynalazkiem, a oprócz niego jeszcze jest taki jeden, który zostawiłem na deser. Przynajmniej ja widzę. Chyba, że zapomniałem o czymś jeszcze, ale wydaje mi się, że więcej to się tam już nie zwykłe przeciążać.
Operator wywołania i wzorce projektowe
Operator wywołania, czyli nawias, taki pusty nawias bez niczego w środku. To jest po prostu operator, który pozwolił językowi C++ być językiem funkcyjnym. Bo możemy sobie przeciążyć dla naszego typu ten operator.
I wtedy był językiem funkcyjnym, zanim to było modne w…
Tak, zanim ludzie wiedzieli, co to są funkcje. Nie tylko wiedzieli z czasów C++, ale powstało coś takiego, co nosi miano funktorów. To jest taki prekursor Lambdy. Dzisiejszej Lambdy też są w C++. Teraz już od kilku lat, może już niedługo na standard będą. Ale zanim Lambdy weszły do języka, to były funktory. I funktor to jest właśnie taki obiekt, który ma przeciążony operator wywołania i wygląda w kodzie. Można, można się do takiego obiektu odnieść jak do funkcji po prostu ją, jego wywołując. I wtedy jest wywołany ten jego operator i tyle. I to się różni. No i tam Ci ten kodzik wywołuje. Nie różni się to od takiej funkcji, czy zwykłej metody wywołanej na obiekcie tym, że my sobie możemy taki funktor zainicjalizować jakimś stanem. Nie, czyli możemy sobie robić domknięcia w kodzie. Czymś takim możemy sobie funkcje przekazywać do funkcji. Po prostu paradygmat funkcji możemy sobie ten obiekt zwrócić, czyli zwracamy funkcje z funkcją na przykład, nie? Czyli da się pisać funkcje w C++ i to od bardzo dawna.
I ciekawostka przyrodnicza. Algorytmy z biblioteki standardowej korzystają z funktorów, bo czemuż by nie, na przykład predykaty są tak zrobione? To takie wczesne. Gdzieś tam do jakiegoś wyszukiwania trzeba było podać funktor, który zwracał boolean. No i ten funktor musiał sobie to to wszystko zrobić. Teraz oczywiście jak Lambdy już weszły, to mamy tam std::function
. To jest tak samo jak w Javie. Praktycznie te Lambdy nie różnią się za bardzo niczym od tych z Javowych. Co najwyżej składnia jest lekko dziwniejsza. Bo tam trzeba jeszcze podać taką klauzulę chwytającą, przechwytującą. Przepraszam, chyba tak to się nazywa, gdzie deklaruje się, co do tej Lambdy z zewnętrznego świata trafia. Bo na przykład, żeby taką Lambdę w metodzie wpyknąć, która by miała dostęp, chciała by mieć dostęp do stanu obiektu, w którego metodzie jest zdefiniowana, czy ogólnie obiektu gdzie, gdzie tam, na którym ona pracuje? Z którego się wywodzi? To trzeba by złapać w klamerki, znaczy w nawiasy prostokątne, i wtedy to ładnie działa. Oprócz tego, jak chcemy na przykład do jakiejś zmiennej lokalnej gdzieś tam ze scope’a się dobrać. Toteż musimy ją sobie wsadzić tam w te klauzule przechwytujące i wtedy wewnątrz możemy sobie na niej działać. A tak poza tym to to już jest tak samo. Jak chcielibyśmy to robić na poziomie faktora, to też można, bo wystarczy taki obiekt faktora po prostu zainteresować tymi zmiennymi. Można je tam przekazać przez referencję, poprzez referencję, referencję. To jest po prostu. Oprócz tego, że jest to ten uchwyt taki niemodyfikowany, to jeszcze jest to nazwa tej zmiennej, ale bardziej pasuje to pojęcie tego uchwytu. Czyli jeżeli mamy jakąś zmienną i przekazujemy ją przez referencję w głąb wywołania, to trafia tam właśnie zawartość tego obiektu przez tą referencję wskazywana. I tak można sobie na żywym organizmie wewnątrz takiej Lambdy na przykład pracować. Jeżeli by ktoś chciał.
Czyli wszystko się da, jak to się da.
Wszystko się da, tylko trzeba wielu konstruktów się nauczyć i troszkę ostrożności. Ale jak się to robiło w drugą stronę, to jest łatwiej, bo jak się już nauczyło tego struktur w C++, to później na przykład jak się patrzyło na taką Javę i nie było słowa virtual
i się czytało, że każda metoda jest wirtualna, to już wszystko wiadomo. Zawsze, nawet programując w czymś wyższego poziomu, dobrze znać chociażby nawet pobieżnie zasady, które panują na niższym poziomie i dlaczego coś działa tak a nie inaczej, bo to jednak zawsze daje troszkę lepsze zrozumienie. Wiadomo, jednak abstrakcja idzie coraz wyżej i czasami, czasami już nawet nie wiemy, czemu, czemu coś wygląda tak a nie inaczej albo działa tak a nie inaczej.
Widać, że w C++ widać te szczegóły na wierzchu, bardzo czasami…
Musi. Czasem, że aż boli.
Aż boli. Dokładnie. No, bo to one są szczegóły takie dość, dość niskopoziomowe, jak widać, ale cenowo to jest jakaś tam też wiedza.
Ale żeby nie było, to język C++ nie jest wcale aż tak niskopoziomowy, jak tutaj go skalowaniem, bo da się pisać wysokopoziomowo w nim i da się robić to czysto obiektowo. Polimorfizm nie da się z pojedynczym dziedziczeniem, na przykład nie ma interfejsów, ale jest pojęcie metody wirtualnej i jest pojęcie metody czysto wirtualnej. Metoda czysto wirtualna to jest taka, która nie ma implementacji i robi się to w taki sposób lekko śmieszny, bo w deklaracji metody wirtualnej na końcu pisze się = 0
, czyli że to nie ma, nie ma, jest tylko nazwa. Ale wtedy ta klasa zyskuje właściwości klasy abstrakcyjnej, czyli nie możemy jej stworzyć, nie możemy jej powołać do życia. Bo kompilator jeszcze by przymknął na to oko, ale biedny linker już się popłacze, jak nie znajdzie implementacji tejże właśnie metody.
No bo tam było 0
, więc mu nie skompilowała. Tej metody musiałem odziedziczyć i dostarczyć im dokładnie.
Czyli można powiedzieć, że taki interfejs to jest taki interfejs, tylko że ze słowem kluczowym class
na początku, bo nikt nie wpadł na to, żeby dodać w ogóle słowo „interfejs” przez lata. To wystarczy dodać do nazwy klasy literkę I
, jak to się zwykle robiło w Javie i wszystko było IcosTam
, a później było C
reszta.
I teraz co? Bo co to chyba zasady, co to właśnie w C było, ale i to jakoś trafiło do C.
Ale i… Aha, że I
w Javie też interfejs.
No tak, w Javie już nie było tego C. Natomiast cała biblioteka MFC, to są klasy, które się od C zaczynają.
No tak, tak, tak. Żeby każdy wiedział, co to jest klasa, żeby nikomu się przypadkiem nie pomyliło, że to struktura, że to są jeszcze struktury w ogóle. Ale zaraz do tego może to obiektowo jakoś dojdziemy.
Wzorce projektowe
Ale jeszcze tutaj o wzorcach warto by było powiedzieć. No bo język, jak już wspomnieliśmy, jest wieloparadygmatowy, czyli po prostu wzorce z tych snów, praktycznie z każdego paradygmatu są tutaj dostępne. Nie, i da, czyli wzorce obiektowe to są wzorce, które są przydatne w programowaniu obiektowym i jak najbardziej wzorce jakieś tam funkcyjne, czyli na przykład tniemy. Nie wiem, czy to, czy to bardziej wzorcem nazwać, czy, czy tego, czy po prostu konstrukcją językową. Takie domknięcie na przykład to jest chyba coś, co jest bardziej konstrukcją. W programowaniu funkcyjnym, gdzie po prostu mamy częściowo zdefiniowaną funkcję i tam ją sobie przekazujemy gdzieś w głąb i dotykamy ją jakąś, jakimś szczegółem implementacji. Jeszcze troszkę bardziej nie. No to tutaj ten operator, ten funktor, który byłby implementacją, byłby przykładem, jak można zrobić taki wzorzec. Iterator to jest taki bardzo mocno używany wzorzec właśnie w tych kontenerach.
No i oprócz tego to możemy praktycznie każdy, każdy wzorzec sobie dopasować do tego, co byśmy chcieli. To jest to, to jest wzorzec, to jest wciąż schemat, to jest schemat, więc jakby kompletny język, to nie ma przeszkód. Tak w sensie Turinga, tak w języku jako takim, tak żeby coś z poprawnym mu zrobić. No i… ale ma też takie, no, przynajmniej jeden wzorzec ma unikatowy. W odniesieniu do do tego, do takich języków zarządzanych, które nie mają destruktora. Ten destruktor to jest taka fajna rzecz, która, jakkolwiek by nie była upierdliwa, to właśnie w tym wzorcu się przydaje. I to znakomicie.
Wzorzec się nazywa RAII . I nazwa jest jak wszystko w tej dziedzinie co najmniej dziwna, bo się tłumaczy jako Resource Acquisition Is Initialization. Już nie mogę od tego, ale chodzi o to, że pozyskanie zasobu jest jego inicjalizacja. To jest takie sedno tego wzorca. I doskonale to będzie widać np. na tych smart pointerach.
Smart pointery to są takie sprytne wrappery na gołe wskaźniki. One są już bezpiecznie typowane. Z nimi nie można zrobić takich krzaków, jakie można było z gołymi wskaźnikami. To znaczy, można dalej, bo jeszcze mamy całą gamę, całe naręcze operatorów rzutowania na różne okazje. Więc możemy dopisać naprawdę jeszcze raz liczby, wszystko co tylko potrzebujemy, ale nie musimy tego robić, bo jesteśmy tak podziurawieni, że w tym momencie już nam nie robi różnicy. Ale mimo wszystko smart pointery jak weszły, to chyba był duży skok taki w zarządzaniu pamięcią jakościowy w C++, bo nagle się okazało, że te wszystkie wycieki pamięci albo większość stają się bardziej kontrolowane, bo np. pojawiły się takie rzeczy jak unique_ptr
.
Czyli taki wskaźnik na obiekt, który jeżeli zostanie przypisany do innej zmiennej, przekazuje własność tego obiektu, a tamten poprzedni się zeruje, więc już nie wskazuje na nic. Więc nie może być tak, że nagle zostają jakieś wiszące wskaźniki w programie, gdzie zmienna została poddana operatorowi delete
. No bo oprócz new
jest też operator delete
. Można sobie zawołać delete cosTam
. I to jest właśnie niszczenie tego obiektu. A propos tego właśnie wzorca puli, to tam trzeba przeciążyć nie tylko new
, ale też i delete
. O tym właśnie wspominaliśmy. Także to ten delete
jest tutaj kluczowy, bo to on mówi, że w tym momencie właśnie zasób musi być zniszczony.
I teraz wyobraźmy sobie sytuację taką klasyczną dla tego wzorca RAII. No więc właśnie jedną z takich sytuacji jest ten, ten wskaźnik, czyli ten, ten sprytny wskaźnik. Czyli jeżeli sobie alokujemy jakiś obiekt na stercie i przypisujemy go do, inicjalizujemy tym, że właśnie wskazaniem. Nasz smart pointer to, w zależności od typu tego smart pointera, dostajemy taką sprytną implementację, która już prawie nam daje zarządzaną pamięć i to jeszcze bez garbage collectora.
Bo ten smart pointer ma, najczęściej każdy smart pointer niszczy… Wróć, nie każdy, bo unique_ptr
akurat to robi, ale shared_ptr
tego, tego nie robi w pewnych okolicznościach. On ten wskaźnik jest na tyle sprytny, że wie, czy powinien zniszczyć obiekt, czy nie. Więc jeżeli wie, że nie wskazuje na obiekt współdzielony, to może go zniszczyć, kiedy jest wywołany operator delete
. Natomiast jeżeli wskazuje na którąś z kopii, znaczy, jest którąś z kopii wskazujących na ten obiekt, to jeżeli licznik referencji nie jest wyzerowany w momencie, kiedy jest wywołany operator delete
, to obiekt nie jest zwalniany. Czyli jeżeli mamy obiekt zaalokowany i robimy sobie ileś tam kopii tego wskaźnika i później chcielibyśmy wszystkie, to po prostu dopiero ostatnia, kiedy licznik referencji w „masterze” faktycznie nam usunie ten obiekt. To jest taki jeden przykład.
No to już prawie, prawie że zarządzane. Już to jest.
No to już jest tak trochę zarządzane. No, okej, ale już trochę nie.
Ale jest jeszcze fajniejsze zastosowanie tego wzorca, bo wyobraźmy sobie np. zablokowanie sekcji krytycznej mutexem. W Javie dopiero niedawno zamknęliśmy to w ósemce, bo cały resources
. Ma… No to trafiliśmy na resources, to tu coś takiego by było, bo tam był jasno pokazany, w jakim ten resource, który pozyskujemy, jest dostępny, a później to on miał być zamknięty, bo tam wszedł interfejs AutoCloseable
i te wszystkie resources, które wpychamy w try-with-resources, jeżeli implementują AutoCloseable
, to są automatycznie zwalniane. Więc tu już mamy ten wzorzec w Javie dostępny od pewnego czasu, ale też tak nie za darmo, a w C++ od samego początku, bo od czasu kiedy istnieją struktury, mogliśmy sobie to zrobić.
Bo jeżeli np. chcielibyśmy taki mutex sobie zablokować na początku kodu, to nawet semantycznie to wygląda dobrze, bo po prostu otwieramy sobie klamerki i to jest nasza sekcja krytyczna, czyli tworzymy nowy scope. Wewnątrz tego scope’u tworzymy na stosie, bez żadnej tam alokacji gdzieś tam stertowej, zmienną tego typu mutex i on w momencie tworzenia nam się lokuje. Jeżeli może, jak nie, to pewnie wyjątek. To zależy od implementacji, ale jeżeli może się zablokować, to się blokuje i wiemy, że w tym scope’ie, w tych klamerkach, w których jesteśmy, siedzimy sobie właśnie w tej bezpiecznej sekcji krytycznej, bo weszliśmy poprzez właśnie tego lock’a
. Natomiast gdy wychodzimy z tego scope’u, to język nam gwarantuje, że destruktor tego lock’a
zostanie zawołany, a on po prostu zwolni tą blokadę. Więc to jest takie czyściutkie i od niepamiętnych czasów dostępne.
Dobrze pokazuje intencje. Rzeczywiście te wysokie elementy języka, które już mają pewne, pewne wzorce wbudowane w swoje struktury, rzeczywiście trzeba tylko umiejętnie nimi dysponować. I co powiedzieć?
Że są, że tak trzeba i że są?
Ale akurat o tym wzorcu się książki rozpisują już od bardzo dawna i ten wzorzec jest wykorzystywany np. na smart pointery, takie właśnie mutexy czy jakieś tam połączenia do czegokolwiek, jakieś klienty bazodanowe bądź testowe czy cokolwiek byśmy chcieli. Możemy sobie robić właśnie w tym duchu i po prostu w konstruktorze takiego obiektu nawiązywać połączenie. Jak nam się udało, to to jedziemy, jak nie, to no to tutaj wypada rzucić wyjątek, bo po prostu żeby wyjść z tego scope’u, żeby nam się to wszystko zniszczyło i wszystkie zasoby, które jeszcze gdzieś tam przy okazji zablokowaliśmy, zdążyły nam się zwolnić, też korzystając z tego automatycznego mechanizmu sprytnego. No a jak się już udało, to możemy bezpiecznie na tym obiekcie dalej działać i wiemy, że on nam się zniszczy, jak wyjdziemy z scope’u.
Zakończenie
No to tak, to chyba w skrócie telegraficznym dwugodzinnym… Półtora godzinnym skróceniu.
Półtora wyszło?
Godzina dwadzieścia wyszło.
A to jeszcze nie wszystko chyba. Coś pewnie zapomniałem.
No to co? To pewnie skończymy na dzisiaj.
Po wprowadzeniu. Troszkę jeszcze rzeczy zostało. W międzyczasie można by zostało rozciągnąć na mini odcinek.
Żeby nie było, że tak kończymy, bez sensu. To zostało np. porównanie, jak właściwie się tego używa. Po prostu C++. Czy ja wiem, czy całego kawałka? Z tego co miałem okazję dotknąć, to mogę go odnieść do Javy. Jak to się tam takie, czy inne konstrukcje po prostu realizuje w C++, a jakie tam w Javie i ciekawe. Mam taki przykład z tego projektu 30-letniego. Jeśli, jak to się kod legacy zachowuje w C++, C i jak w Javie? Bo to są jakieś tam subtelności, to właśnie takie drobne. Jeśli język i struktura jest trudniejsza, to legacy w takim czymś musi też być trudniejsze do opanowania.
No, ale jest ciekawsze.
Po przeprosinach. To znak, że musimy kończyć.
Zdecydowanie.
To dzięki Wojtek. Do następnego odcinka.
Dzięki, do następnego. Cześć.
Cześć.