Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00265 005557 13070460 na godz. na dobę w sumie
Skuteczny nowoczesny C++ - ebook/pdf
Skuteczny nowoczesny C++ - ebook/pdf
Autor: Liczba stron:
Wydawca: Promise Język publikacji: polski
ISBN: 978-83-7541-219-2 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie
Porównaj ceny (książka, ebook, audiobook).

Do opanowania języków C++11 i C++14 nie wystarcza zapoznanie się z wprowadzonymi w nich funkcjonalnościami (np. deklaracjami typu auto, semantyką operacji move, wyrażeniami lambda i obsługą współbieżności). Sztuką jest nauczenie się korzystania z tych funkcjonalności skutecznie – tak, aby programy były poprawne, wydajne, łatwe w utrzymaniu i przenośne. Pomocna w tym będzie niniejsza książka, w której został opisany sposób tworzenia wspaniałego oprogramowania przy użyciu standardów C++11 i C++14 – tj. nowoczesnego języka C++.
Tematy:
Zalety i wady inicjalizacji klamrowej, specyfikacji noexcept, technik doskonałego przekazywania argumentów oraz funkcji make służących do tworzenia wskaźników inteligentnych
Związki między std::move, std::forward, odwołaniami do r-wartości i odwołaniami uniwersalnymi
Techniki pisania czytelnych, poprawnych i skutecznych wyrażeń lambda
Różnice między std::atomic a volatile, zalecane sposoby użycia każdej z tych deklaracji oraz ich związek z interfejsem API współbieżności w języku C++
Zmiany najlepszych rozwiązań znanych ze „starego” języka C++ (tj. C++98) wymagane podczas tworzenia oprogramowania w nowoczesnym języku C++
Książka Skuteczny nowoczesny C++ jest napisana zgodnie z wypróbowanym, opartym na wskazówkach i przykładach stylem wcześniejszych książek Scotta Meyersa, ale dotyczy całkowicie nowego materiału. Jest ważną pozycją dla każdego nowoczesnego twórcy oprogramowania w języku C++.
Przed ponad 20 laty książki Scotta Meyersa z serii Effective C++ (Effective C++, More Effective C++ i Effective STL) wyznaczyły poziom odniesienia dla wskazówek dotyczących programowania w języku C++. Jego jasne, ujmujące wyjaśnienie skomplikowanego technicznego materiału przyniosły mu światową sławę, dzięki której stał się popularnym trenerem, konsultantem i prezenterem konferencyjnym. Scott Meyers zyskał tytuł doktora informatyki na uniwersytecie Brown.
„Kiedy już zapoznałem się z podstawami języka C++, sposobów programowania kodu produkcyjnego w tym języku nauczyłem się z serii książek Meyersa Effective C++. Książka Skuteczny nowoczesny C++ jest najważniejszym podręcznikiem dotyczącym istotnych wytycznych, stylów i idiomów przydatnych do skutecznego i dobrego stosowania nowoczesnego języka C++. Nie masz jej jeszcze? Kup ją. Teraz.”
—Herb Sutter
Przewodniczący komitetu standaryzacyjnego ISO C++ oraz architekt oprogramowania C++ w firmie Microsoft t

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Recenzje książki Skuteczny nowoczesny C++ I co, nadal interesujesz się językiem C++? Warto! Nowoczesny C++ (czyli C++11/C++14) to znacznie więcej niż odnowienie standardu. Nowe funkcjonalności sprawiają, że wydaje się wynalezieniem tego języka na nowo. Szukasz wskazówek i pomocy? Zatem ta książka na pewno Ci się przyda. Gdy chodzi o język C++, Scott Meyers był i nadal jest synonimem dokładności, jakości i entuzjazmu. Gerhard Kreuzer Inżynier w dziale badań i rozwoju, Siemens AG Osiągnięcie najwyższej wiedzy eksperckiej jest dość trudne. Osiągnięcie perfekcji w naucza- niu — doskonałej strategii i umiejętności upraszczania wyjaśnień – również jest niełatwe. Wiesz, że natrafi łeś na skarb, gdy spotkasz jedną osobę mającą te dwie cechy. Skuteczny nowoczesny C++ to nadzwyczajne osiągnięcie wytrawnego autora technicznego. Jasne, zrozumiałe i ustawione w kolejności wyjaśnienia zostały ułożone na podstawach złożonych i łączących się tematów, a wszystko w rzeczowym, literackim stylu. Znalezienie błędu tech- nicznego, nudnego fragmentu lub niedopracowanego zdania jest równie nieprawdopodobne w książce Skuteczny nowoczesny C++. Andrei Alexandrescu doktor, naukowiec, pracownik Facebooka i autor książki Modern C++ Design Wszystkim, który chcą zyskać jak najwięcej z nowoczesnego języka C++ (zarówno najlepsze rozwiązania, jak i pułapki do uniknięcia), szczerze polecam nabycie tej książki, przeczyta- nie jej dokładnie i częste odwoływanie się do niej. Chociaż mam ponad dwudziestoletnie doświadczenie w języku C++, na pewno nauczyłem się z niej nowych rzeczy! Nevin Liber Starszy inżynier oprogramowania, DRW Trading Group Bjarne Stroustrup – twórca języka C++ – powiedział, „C++11 sprawia wrażenie nowego języka”. Skuteczny nowoczesny C++ sprawia, że podzielamy to wrażenie dzięki jasnemu wytłumaczeniu, jak programiści mogą korzystać z nowych funkcjonalności i idiomów standardów C++11 i C++14 w swojej codziennej pracy. Jest to jeszcze jedna wspaniała książka Scotta Meyersa. Cassio Neri Analityk jakości FX, Lloyds Banking Group ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Scott ma dryg do sprowadzania technicznej złożoności do zrozumiałego sedna. Jego książki Eff ective C++ pomogły podnieść styl kodowania poprzedniego pokolenia programistów C++. Nowa książka wydaje się mieć podobne znaczenie dla tych, którzy korzystają z no- woczesnego języka C++. Roger Orr OR/2 Limited, członek komitetu standaryzacyjnego ISO C++ Skuteczny nowoczesny C++ jest wspaniałym narzędziem do poprawienia umiejętności programowania w nowoczesnym języku C++. Nie tylko uczy, jak, kiedy i gdzie używać nowoczesnego języka C++ w skuteczny sposób, ale także wyjaśnia dlaczego. Bez wątpliwości jasna i przenikliwa książka Scotta podzielona na 42 dobrze przemyślane punkty umożliwia programistom znacznie lepiej zrozumieć ten język. Bart Vandewoestyne Inżynier w dziale badań i rozwoju, entuzjasta języka C++ Kocham język C++, od wielu dekad stanowi moje narzędzie pracy zawodowej. A dzięki ostatniemu mnóstwu funkcjonalności stał się jeszcze bardziej potężny i ekspresywny, niż mogłem sobie wcześniej wyobrazić. Jak zawsze różnorodne możliwości wywołują pytanie „kiedy i jak je stosować?” I jak zawsze książki Eff ective C++ napisane przez Scotta są naj- lepszą odpowiedzią na to pytanie. Damien Watkins Computation Soft ware Engineering Team Lead, CSIRO Wspaniale jest czytać o przejściu do nowoczesnego języka C++ – nowe funkcje standardów C++11/14 są opisane obok tych pochodzących z C++98, punkty tematyczne są łatwe do odwoływania, a każdy z nich jest podsumowany radami. Przyjemne i przydatne zarówno dla dorywczych, jak i zaawansowanych programistów C++. Rachel Cheng F5 Networks Jeśli migrujesz z języka C++98/03 do C++11/14, potrzebujesz bardzo praktycznych i jasnych informacji, jakie Scott dostarcza w książce Nowoczesny skuteczny C++. Jeśli już piszesz kod C++11, prawdopodobnie odkryjesz zagadnienia związane z ważnymi nowymi funkcjo- nalnościami dzięki dokładnemu omówieniu ich przez Scotta. W każdym razie tej książce warto poświęcić czas. Rob Stewart Członek komisji nadzorczej Boost (boost.org) ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Skuteczny nowoczesny C++ Scott Meyers przekład: Maria Chaniewska APN Promise Warszawa 2015 ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Skuteczny nowoczesny C++ © 2015 APN PROMISE SA Authorized translation of English edition of Effective Modern C++ ISBN 978-1-491-90399-5 Copyright © 2015 Scott Meyers. All rights reserved. This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls of all rights to publish and sell the same. APN PROMISE SA, biuro: ul. Kryniczna 2, 03-934 Warszawa tel. +48 22 35 51 600, fax +48 22 35 51 699 e-mail: mspress@promise.pl Wszystkie prawa zastrzeżone. Żadna część niniejszej książki nie może być powielana ani rozpowszechniana w jakiejkolwiek formie i w jakikolwiek sposób (elektroniczny, mechaniczny), włącznie z fotokopiowaniem, nagrywaniem na taśmy lub przy użyciu innych systemów bez pisemnej zgody wydawcy. Logo O’Reilly jest zarejestrowanym znakiem towarowym O’Reilly Media, Inc. Effective Modern C++, ilustracja z okładki i powiązane elementy są znakami towarowymi O’Reilly Media, Inc. Wszystkie inne nazwy handlowe i towarowe występujące w niniejszej publikacji mogą być znakami towarowymi zastrzeżonymi lub nazwami zastrzeżonymi odpowiednich firm odnośnych właścicieli. Przykłady firm, produktów, osób i wydarzeń opisane w niniejszej książce są fikcyjne i nie odnoszą się do żadnych konkretnych firm, produktów, osób i wydarzeń. Ewentualne podobieństwo do jakiejkolwiek rzeczywistej firmy, organizacji, produktu, nazwy domeny, adresu poczty elektronicznej, logo, osoby, miejsca lub zdarzenia jest przypadkowe i niezamierzone. APN PROMISE SA dołożyła wszelkich starań, aby zapewnić najwyższą jakość tej publikacji. Jednakże nikomu nie udziela się rękojmi ani gwarancji. APN PROMISE SA nie jest w żadnym wypadku odpowiedzialna za jakiekolwiek szkody będące następstwem korzystania z informacji zawartych w niniejszej publikacji, nawet jeśli APN PROMISE została powiadomiona o możliwości wystąpienia szkód. ISBN: 978-83-7541-155-3 Przekład: Maria Chaniewska Redakcja: Marek Włodarz Korekta: Ewa Swędrowska Projekt okładki: Ellie Volkhausen Ilustracje: Rebecca Demarest Skład i łamanie: MAWart Marek Włodarz ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Spis treści Podziękowania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .ix Wprowadzenie. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Terminologia i konwencje. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Raportowanie błędów i sugerowanie poprawek . . . . . . . . . . . . . . . . . . . . . . . 8 1 Dedukcja typów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Punkt 1: Dedukcja typów w szablonach. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Przypadek 1: ParamType to odwołanie lub wskaźnik, ale nie odwołanie uniwersalne . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Przypadek 2: ParamType jest odwołaniem uniwersalnym . . . . . . . . . . . 13 Przypadek 3: ParamType nie jest ani wskaźnikiem, ani odwołaniem . . 14 Argumenty tablicowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Argumenty funkcyjne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Punkt 2: Dedukcja typu auto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Punkt 3: decltype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 Punkt 4: Jak wyświetlać wydedukowane typy . . . . . . . . . . . . . . . . . . . . . . . 34 Edytory IDE 34 Diagnostyka kompilatora. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 Wyniki czasu wykonania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 2 auto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Punkt 5: Preferuj deklarację auto zamiast jawnych deklaracji typów. . . . 41 Punkt 6: Stosuj idiom jawnego inicjatora typu, gdy deklaracja auto powoduje dedukcję niepożądanych typów . . . . . . . . . . . . . . . . . 48 3 Droga do nowoczesnego języka C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Punkt 7: Rozróżniaj między () a {} podczas tworzenia obiektów . . . . . . 55 Punkt 8: Preferuj nullptr zamiast 0 i NULL . . . . . . . . . . . . . . . . . . . . . . . . . . 67 ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== v Punkt 9: Preferuj deklaracje aliasów zamiast typedef. . . . . . . . . . . . . . . . . 72 Punkt 10: Preferuj wyliczenia enum z zasięgiem zamiast bez zasięgu . . . . . 77 Punkt 11: Preferuj funkcje usunięte zamiast prywatnych niezdefiniowanych. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 Punkt 12: Deklaruj funkcje nadpisujące jako override. . . . . . . . . . . . . . . . . 91 Punkt 13: Preferuj iteratory const_iterator zamiast iterator. . . . . . . . . . . 99 Punkt 14: Deklaruj funkcje jako noexcept, jeśli nie zgłaszają wyjątków. . 104 Punkt 15: Stosuj constexpr, kiedy to tylko możliwe . . . . . . . . . . . . . . . . . . 112 Punkt 16: Dbaj o bezpieczeństwo wątkowe funkcji składowych const . . 120 Punkt 17: Generowanie specjalnych funkcji składowych . . . . . . . . . . . . . 127 4 Wskaźniki inteligentne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .137 Punkt 18: Stosuj wskaźniki std::unique_ptr do zarządzania zasobami posiadanymi wyłącznie . . . . . . . . . . . . . . . . . . . . . . . . 139 Punkt 19: Stosuj wskaźniki std::shared_ptr w przypadku zarządzania zasobami o współdzielonym posiadaniu . . . . . . . 146 Punkt 20: Stosuj typ std::weak_ptr dla wskaźników przypominających std::shared_ptr, które mogą zawisnąć . . . 157 Punkt 21: Preferuj funkcje std::make_unique i std::make_shared zamiast bezpośredniego używania instrukcji new . . . . . . . . . . . 163 Punkt 22: Podczas używania idiomu Pimpl definiuj specjalne funkcje składowe w pliku implementacji . . . . . . . . . . . . . . . . . . 173 5 Odwołania do r-wartości, semantyka przenoszenia i przekazywanie doskonałe. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .185 Punkt 23: std::move i std::forward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 Punkt 24: Odróżniaj odwołania uniwersalne od odwołań do r-wartości 193 Punkt 25: Stosuj std::move w przypadku odwołań do r-wartości, a std::forward w przypadku odwołań uniwersalnych . . . . . . . 199 Punkt 26: Unikaj przeciążania w przypadku odwołań uniwersalnych. . . 209 Punkt 27: Zapoznaj się z alternatywami przeciążania odwołań uniwersalnych . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 Rezygnacja z przeciążania . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 Przekazywanie przez const T . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 Przekazywanie przez wartość . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 Technika tag dispatch. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 Ograniczanie szablonów przyjmujących odwołania uniwersalne . . . 223 vi | Spis treści ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Kompromisy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 Punkt 28: Zwijanie odwołań. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 Punkt 29: Załóż, że operacje przenoszenia nie są ani obecne, ani tanie, ani używane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 Punkt 30: Zapoznaj się z przypadkami niepowodzeń przenoszenia doskonałego . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 Inicjatory klamrowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 0 lub NULL jako wskaźniki null . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 Sama deklaracja całkowitoliczbowych danych składowych o kwalifikatorach static const . . . . . . . . . . . . . . . . . . . . . 249 Przeciążone nazwy funkcji i nazwy szablonów. . . . . . . . . . . . . . . . . . . 251 Pola bitowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 6 Wyrażenia lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .257 Punkt 31: Unikaj domyślnych trybów przechwytywania. . . . . . . . . . . . . . 259 Punkt 32: Stosuj przechwytywanie inicjujące do przenoszenia obiektów do domknięć . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 Punkt 33: Stosuj decltype do parametrów auto , aby je przekazywać za pomocą std::forward. . . . . . . . . . . . . . . . . . . 275 Punkt 34: Preferuj wyrażenia lambda zamiast std::bind. . . . . . . . . . . . . . 278 7 Interfejs API współbieżności . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .289 Punkt 35: Preferuj programowanie oparte na zadaniach zamiast opartego na wątkach . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289 Punkt 36: Określ zasadę std::launch::async, jeśli asynchoniczność jest istotna. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294 Punkt 37: Doprowadź wątki std::thread do stanu nieprzyłączalnego na wszystkich ścieżkach . . . . . . . . . . . . . . . . 300 Punkt 38: Uważaj na różnorodne działanie destruktorów uchwytów wątków . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309 Punkt 39: Rozważ obiekty future typu void do komunikacji zdarzeń jednorazowych . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314 Punkt 40: Stosuj std::atomic dla współbieżności, a volatile dla pamięci specjalnej . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325 ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Spis treści | vii 8 Szlify. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .335 Punkt 41: Rozważ przekazywanie przez wartość parametrów, które można kopiować i tanio przenosić – o ile są zawsze kopiowane. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335 Punkt 42: Rozważ umieszczanie zamiast wstawiania. . . . . . . . . . . . . . . . . 348 Indeks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .367 O autorze. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .361 viii | Spis treści ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Podziękowania Badania nad językiem, który był wówczas nazywany C++0x (powstającym C++11), rozpocząłem w roku 2009. Umieściłem wiele pytań na grupie dyskusyjnej Usenet comp.std.c++ i jestem wdzięczny członkom tej społeczności (szczególnie Danielowi Krüglerowi) za ich pomocne wpisy. W późniejszych latach swoje pytania dotyczące C++11 i C++14 zadawałem na portalu Stack Overfl ow i jestem równie zobowiązany tej społeczności za pomoc w zrozumieniu szczegółowych zagadnień nowoczesnego języka C++. W roku 2010 przygotowałem materiały do szkolenia na temat języka C++0x (osta- tecznie opublikowanego jako Overview of the New C++, Artima Publishing, 2010). Zarówno te materiały, jak i moja wiedza odniosły korzyści z technicznego sprawdze- nia, które przeprowadzili Stephan T. Lavavej, Bernhard Merkle, Stanley Friesen, Leor Zolman, Hendrik Schober i Anthony Williams. Bez ich pomocy prawdopodobnie nigdy nie byłbym w stanie podjąć się napisania książki Skuteczny nowoczesny C++. Ten tytuł przypadkowo został zasugerowany lub poparty przez wielu czytelników odpowiadających na mój wpis na blogu z 18 lutego 2014 „Help me name my book” (pomóżcie mi nazwać książkę), a Andrei Alexandrescu (autor książki Modern C++ Design, Addison-Wesley, 2001) był tak uprzejmy, że zgodził się na użycie tego tytułu. Nie jestem w stanie zidentyfi kować wszystkich źródeł informacji w tej książce, ale pewne z nich miały względnie bezpośredni wpływ. Użycie w punkcie 4 niezdefi nio- wanego szablonu do wydobycia informacji o typie z kompilatorów zostało zasugero- wane przez Stephana T. Lavaveja, a Matt P. Dziubinski zwrócił mi uwagę na Boost. TypeIndex. W punkcie 5 przykład unsigned-std::vector int ::size_type pochodzi z artykułu Andrey’a Karpova z 28 lutego 2010, „In what way can C++0x standard help you eliminate 64-bit errors” (W jaki sposób standard C++0x pomaga elimino- wać błędy w oprogramowaniu 64-bitowym). Przykład std::pair std::string, int / std::pair const std::string, int w tym samym punkcie pochodzi z wykładu Step- hana T. Lavaveja w Going Native 2012, „STL11: Magic Secrets”. Punkt 6 został zainspirowany artykułem Herba Suttera z 12 sierpnia 2013 „GotW #94 Solution: AAA Style (Almost Always Auto)” (Prawie zawsze auto). Punkt 9 był umotywowany ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== ix przez wpis blogu Martinho Fernandesa z 27 maja 2012 „Handling dependent names” (Obsługa nazw zależnych). Przykład w punkcie 12 demonstrujący przeciążanie na kwalifi katorach odwołań jest oparty na odpowiedzi Caseya na pytanie „What’s a use case for overloading member functions on reference qualifi ers?” (Jaki jest przypadek użycia funkcji składowych przeciążonych na kwalifi katorach odwołań?) zadane na portalu Stack Overfl ow 14 stycznia 2014. Moje zalecenia z punktu 15 dotyczące rozległego wsparcia funkcji constexpr w języku C++14 uwzględniają informacje, które dał mi Rein Halbersma. Punkt 16 jest oparty na prezentacji Herba Suttera C++ and Beyond 2012, „You don’t know const and mutable” (Nie znasz const ani mutable). Rada z punktu 18, aby mieć funkcje fabrykujące zwracające wskaźniki std::unique_ptr, jest oparta na artykule Herba Suttera z 30 maja 2013, „GotW# 90 Solution: Factories” (Fabryki). W punkcie 19, funkcja fastLoadWidget pochodzi z prezentacji Herba Suttera Going Native 2013, „My Favorite C++ 10-Liner”. Moje zalecenie dotyczące std::unique_ptr i typów niekompletnych w punkcie 22 wy- korzystuje artykuł Herba Suttera z 27 listopada 2011, „GotW #100: Compilation Firewalls” (Zapory kompilacji), a także odpowiedzi Howarda Hinnanta z 22 maja 2011 na pytanie Stack Overfl ow „Is std::unique_ptr T required to know the full defi nition of T?” (Czy std::unique_ptr T wymaga znajomości pełnej defi nicji typu T?). Przykład dodawania macierzy Matrix w punkcje 25 pochodzi z zapisków Davida Abrahamsa. Komentarz JoeArgonne z 8 grudnia 2012 dotyczący wpisu na blogu z 30 listopada 2012 „Another alternative to lambda move capture” (Inna alternaty- wa przechwytywania przenoszącego wyrażenia lambda) był źródłem zapisanego w punkcie 32 podejścia opartego na std::bind do emulowania przechwytywania inicjującego w C++11. Wyjaśnienie w punkcie 37 dotyczące problemu z niejawnym odłączeniem w destruktorze wątku std::thread pochodzi z dokumentu Hansa-J. Boehma z 4 grudnia 2008, „N2802: A plea to reconsider detach-on-destruction for thread objects” (Apel o ponowne rozważenie odłączania przy niszczeniu obiektów wątków). Punkt 41 oryginalnie został zmotywowany dyskusjami pod wpisem na blogu Davida Abrahamsa z 15 sierpnia 2009 „Want speed? Pass by value” (Potrze- bujesz szybkości? Przekazuj przez wartość). Pomysł, że typy obsługujące tylko przenoszenie zasługują na specjalne traktowanie, pochodzi od Matthew Fioravante, natomast analiza kopiowania opartego na przypisywaniu wynika z komentarzy napisanych przez Howarda Hinnanta. W punkcie 42 Stephan T. Lavavej i Howard Hinnant pomogli mi zrozumieć względne profi le wydajności związane z funkcja- mi umieszczania i wstawiania, a Michael Winterberg zwrócił mi uwagę na to, jak umieszczanie może prowadzić do wycieków zasobów. (Michael uznaje za swoje źródło prezentację Seana Parenta Going Native 2013, „C++ Seasoning”). Michael x | Podziękowania ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== wskazał także, jak funkcje umieszczania mogą używać inicjowania bezpośredniego, podczas gdy funkcje wstawiania używają inicjowania kopiującego. Sprawdzanie wersji roboczych książki technicznej jest wymagającym, czasochłon- nym i bardzo ważnym zadaniem. Dlatego mam szczęście, że tak wiele osób chcia- ło to zrobić dla mnie. Ofi cjalną korektą pełnych lub częściowych wersji robo- czych książki Skuteczny nowoczesny C++ zajmowali się Cassio Neri, Nate Kohl, Gerhard Kreuzer, Leor Zolman, Bart Vandewoestyne, Stephan T. Lavavej, Nevin „ :-)” Liber, Rachel Cheng, Rob Stewart, Bob Steagall, Damien Watkins, Bradley E. Needham, Rainer Grimm, Fredrik Winkler, Jonathan Wakely, Herb Sutter, Andrei Alexandrescu, Eric Niebler, Th omas Becker, Roger Orr, Anthony Williams, Michael Winterberg, Benjamin Huchley, Tom Kirby-Green, Alexey A Nikitin, William Dealtry, Hubert Matthews i Tomasz Kamiński. Otrzymałem także opinie od wielu czytelników przez O’Reilly’s Early Release EBooks i Safari Books Online’s Rough Cuts, komentarze na moim blogu (Th e View from Aristeia) i pocztą e-mail. Jestem wdzięczny wszystkim tym osobom. Ta książka jest znacznie lepsza niż byłaby bez tej pomocy. Szczególnie jestem zobowiązany względem Stephana T. Lavaveja i Roba Stewarta, których niezwykle szczegółowe i wyczerpujące uwagi doprowadziły mnie do zmartwienia, że spędzili nad tą książką prawie tyle czasu, co ja. Specjalnie dziękuję także Leorowi Zolmanowi, który oprócz przejrzenia rękopisu, dokładnie sprawdził wszystkie przykłady kodu. Dedykowane korekty wersji cyfrowych książki przeprowadzili Gerhard Kreuzer, Emyr Williams i Bradley E. Needham. Moja decyzja, aby ograniczyć długość wierszy kodu do 64 znaków (maksimum, które prawdopodobnie pozwoli na właściwy wygląd w druku, a także na różnych urządzeniach cyfrowych, przy różnych orientacjach urządzeń i konfi guracjach czcionek) została oparta na podstawie informacji uzyskanej od Michaela Mahera. Po przeszło dwudziestu latach życia ze mną moja żona Nancy L. Urbano kolejny raz tolerowała miesiące nieuważnych rozmów z mieszaniną rezygnacji, wściekłości, a czasami wybuchami zrozumienia i wsparcia. W tym samym czasie nasza suka Darla była przeważnie zadowolona z drzemki, gdy spędzałem godziny przy ekranie komputera, ale nigdy nie pozwoliła mi zapomnieć, że istnieje życie poza klawiaturą. ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Podziękowania | xi ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Wprowadzenie Jeśli jesteś doświadczonym programistą C++ i choć trochę mnie przypominasz, po- czątkowo pomyślisz o języku C++11 „Tak, tak, wiem. To C++, tylko trochę bardziej”. Jednak, gdy dowiesz się więcej, będziesz zaskoczony zasięgiem zmian. Deklaracje auto, pętle for oparte na zakresie, wyrażenia lambda i odwołania do r-wartości zmieniają oblicze języka C++, nie mówiąc nic o nowych funkcjach współbieżności. Następnie pojawiają się zmiany idiomatyczne. Znikają wartości 0 i defi nicje typów typedef, pojawiają się wskaźniki nullptr i deklaracje aliasów. Wyliczenia powinny teraz mieć określony zasięg. Wskaźniki inteligentne są teraz preferowane względem wbudowanych. Przenoszenie obiektów jest zwykle lepsze niż ich kopiowanie. Jest dużo do nauczenia się o C++11, nie wspominając o C++14. Ważniejsze jest to, że jest dużo do nauczenia się o skutecznym korzystaniu z no- wych możliwości. Jeśli potrzebujesz podstawowych informacji o „nowoczesnych” funkcjonalnościach języka C++, zasoby są obfi te, ale jeżeli szukasz wskazówek, jak użyć tych funkcjonalności do tworzenia oprogramowania, które będzie poprawne, wydajne, łatwe w utrzymaniu i przenośne, znalezienie ich będzie znacznie trudniej- sze. Tu przyda się niniejsza książka. Nie jest poświęcona opisaniu funkcjonalności C++11 i C++14, ale ich skutecznemu stosowaniu. Informacje w tej książce są podzielone na wskazówki nazywane punktami. Chcesz zrozumieć różne formy dedukcji typów? Albo dowiedzieć się, kiedy (a kiedy nie) używać deklaracji auto? Interesuje Cię, dlaczego funkcje składowe const powinny być bezpieczne wątkowo, jak zaimplementować idiom Pimpl przy użyciu wskaźni- ka std::unique_ptr, dlaczego należy unikać domyślnych trybów przechwytywania w wyrażeniach lambda lub czym różnią się typy std::atomic i volatile? Znajdziesz tu wszystkie odpowiedzi. Co więcej, są one niezależne od platformy i zgodne ze standardami. Jest to książka o przenośnym języku C++. Punkty w tej książce są wskazówkami, a nie regułami, ponieważ wskazówki mają wyjątki. Najważniejszą częścią każdego punktu nie jest oferowana rada, ale jej uzasadnienie. Po przeczytaniu, będziesz móc wyznaczyć, czy okoliczności danego ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== 1 projektu uzasadniają naruszenie wskazówki z danego punktu. Prawdziwym celem tej książki nie jest nakazanie, co robić, a czego unikać, ale przekazanie głębokiej wiedzy na temat sposobu działania języków C++11 i C++14. Terminologia i konwencje W celu zapewnienia, że się rozumiemy, ważne jest, abyśmy ustalili pewną termi- nologię, zaczynając ironicznie od samego „C++”. Istnieją cztery ofi cjalne wersje języka C++, każda nazwana od roku przyjęcia odpowiedniego standardu ISO: C++98, C++03, C++11 i C++14. C++98 i C++03 różnią się jedynie szczegółami technicznymi, więc w tej książce będę się odnosił do nich jako do C++98. Gdy od- noszę się do C++11, mam na myśli zarówno C++11, jak i C++14, ponieważ C++14 jest faktycznie nadzbiorem C++11. Gdy piszę C++14, mam na myśli dokładnie C++14. A gdy po prostu piszę o C++, jest to zdanie ogólne dotyczące wszystkich wersji języka. Używany termin C++ C++98 C++11 C++14 Wersja języka Wszystkie C++98 i C++03 C++11 i C++14 C++14 W efekcie mogę powiedzieć, że język C++ stawia na pierwszym miejscu wydajność (prawdziwe dla wszystkich wersji), że w języku C++98 brakuje obsługi współbież- ności (prawdziwe tylko dla C++98 i C++03), że język C++11 obsługuje wyrażenia lambda (prawdziwe dla C++11 i C++14) oraz że język C++14 oferuje uogólnioną dedukcję typu zwracanego przez funkcję (prawdziwe tylko dla C++14). Najbardziej rozpowszechnioną funkcjonalnością C++11 jest prawdopodobnie semantyka przenoszenia, a podstawą semantyki przenoszenia jest rozróżnianie wyrażeń, które są r-wartościami, od tych, które są l-wartościami. Jest tak, ponieważ r-wartości wskazują obiekty podatne na operacje przenoszenia, a l-wartości – za- sadniczo nie. Koncepcyjne (chociaż w praktyce nie zawsze) r-wartości odpowia- dają tymczasowym obiektom zwracanym przez funkcje, a l-wartości odpowiadają obiektom, do których możemy się odwoływać przez nazwę, wskaźnik lub odwołanie do l-wartości. 2 | Wprowadzenie ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Przydatną heurystyką pozwalającą wyznaczyć, czy wyrażenie jest l-wartością, jest zapytanie, czy możemy wziąć jej adres. Jeśli możemy, zwykle jest. Jeśli nie możemy, zwykle jest r-wartością. Przyjemną cechą tej heurystyki jest to, że pozwala zapa- miętać, że typ wyrażenia jest niezależny od tego, czy wyrażenie jest l-wartością, czy r-wartością. Oznacza to, że dla danego typu T możemy mieć l-wartości typu T, a także r-wartości typu T. Jest to szczególnie ważne, aby pamiętać o tym, gdy mamy do czynienia z parametrem, którego typem jest odwołanie do r-wartości, ponieważ sam parametr jest l-wartością: class Widget { public: Widget(Widget rhs); // rhs jest l-wartością, chociaż ma … // typ odwołania do r-wartości }; Tutaj byłoby doskonale poprawne wzięcie adresu parametru rhs wewnątrz kon- struktora przenoszącego obiektu Widget, więc rhs jest l-wartością, nawet jeśli jego typem jest odwołanie do r-wartości. (Dzięki podobnemu rozumowaniu wszystkie parametry są l-wartościami). Ten fragment kodu demonstruje kilka używanych przeze mnie konwencji: 폷 Nazwą klasy jest Widget. Używam nazwy Widget zawsze, kiedy chcę określić arbitralny typ defi niowany przez użytkownika. O ile nie potrzebuję pokazać konkretnych szczegółów klasy, używam nazwy Widget bez deklaracji. 폷 Używam nazwy parametru rhs („right-hand side” – prawostronny). Jest to pre- ferowana przeze mnie nazwa parametru dla operacji przenoszenia (czyli kon- struktora przenoszącego i przenoszącego operatora przypisania) i operacji kopiowania (czyli konstruktora kopiującego o kopiującego operatora przypi- sania). Korzystam z niej także dla parametrów prawostronnych operatorów binarnych: Matrix operator+(const Matrix lhs, const Matrix rhs); Mam nadzieję, że nie jest zaskakujące, że lhs oznacza lewostronny („left -hand side”). 폷 Stosuję specjalne formatowanie do części kodu lub części komentarzy, aby przyciągnąć do nich uwagę. W powyższym konstruktorze przenoszącym kla- sy Widget wyróżniłem deklarację rhs i część komentarza wskazującą, że rhs ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Wprowadzenie | 3 to l-wartość. Wyróżniony kod nie jest z założenia dobry ani zły. Jest to po pro- stu kod, na który chcę zwrócić szczególną uwagę. 폷 Używam znaku „ …”, aby pokazać, że „tu może znajdować się inny kod”. Ten wąski wielokropek jest odmienny od szerokiego wielokropka („ ...”), uży- wanego w języku C++11 w kodzie źródłowym szablonów o zmiennej liczbie parametrów. Brzmi to mętnie, ale tak nie jest. Na przykład: template typename... Ts // to są wielokropki void processVals(const Ts ... params) // w kodzie źródłowym { // języka C++ … // to oznacza // „tu znajduje się inny kod” } Deklaracja funkcji processVals pokazuje, że używam typename, gdy deklaruję parametry typu w szablonach, ale jest to zaledwie osobista preferencja. Słowo kluczowe class działałoby równie dobrze. W tych sytuacjach, gdy pokazuję urywki kodu ze standardu C++, deklaruję parametry typu przy użyciu słowa kluczowego class, ponieważ jest ono stosowane w standardzie. Gdy obiekt jest inicjowany innym obiektem tego samego typu, nowy obiekt jest nazywany kopią obiektu inicjującego, nawet jeśli ta kopia została utworzona przy użyciu konstruktora przenoszącego. Niestety nie ma żadnej terminologii w C++, która rozróżniałaby między obiektem, który jest kopią skonstruowaną za pomocą kopiowania, a kopią skonstruowaną za pomocą przenoszenia: void someFunc(Widget w); // parametr w funkcji someFunc // jest przekazywany przez wartość Widget wid; // wid to pewien obiekt klasy Widget someFunc(wid); // w tym wywołaniu funkcji someFunc, // w jest kopią obiektu wid, która // została utworzona za pomocą // konstrukcji kopiującej someFunc(std::move(wid)); // w tym wywołaniu funkcji someFunc, // w jest kopią obiektu wid, która 4 | Wprowadzenie ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== // zostałautworzona za pomocą // konstrukcji przenoszącej Kopie r-wartości są zasadniczo konstruowane przez przenoszenie, a kopie l-wartości są zwykle konstruowane przez kopiowanie. Wynika z tego, że jeśli wiemy tylko, że obiekt jest kopią innego obiektu, nie możemy określić, jak kosztowne było skon- struowanie tej kopii. Na przykład w powyższym kodzie nie ma sposoby określenia, jak kosztowne jest utworzenie parametru w bez wiedzy, czy do funkcji someFunc zostały przekazane r-wartości, czy l-wartości. (Konieczna byłaby także znajomość kosztu przenoszenia i kopiowania obiektów Widget). W wywołaniu funkcji wyrażenia przekazywane po stronie wywołującej są argumen- tami funkcji. Argumenty są używane do inicjowania parametrów funkcji. W pierw- szym wywołaniu funkcji someFunc (powyżej) argumentem jest wid. W drugim wy- wołaniu argumentem jest std::move(wid). W obu wywołaniach parametrem jest w. Rozróżnianie miedzy argumentami a parametrami jest ważne, ponieważ parametry są l-wartościami, ale argumenty, którymi są inicjowane, mogą być r-wartościami lub l-wartościami. Jest to szczególnie ważne w procesie przekazywania doskonałe- go, gdzie argument przekazywany do funkcji jest przekazywany do drugiej funkcji w taki sposób, że r-wartościowość lub l-wartościowość oryginalnego argumentu jest zachowywana. (Doskonałe przekazywanie jest szczegółowo opisane w punkcie 30). Dobrze zaprojektowane funkcje są bezpieczne wątkowo, co oznacza, że oferują przynajmniej podstawową gwarancję bezpieczeństwa wątkowego (czyli gwarancję podstawową). Takie funkcje zapewniają w kodzie wywołującym, że nawet w przy- padku wywołania wyjątku niezmienniki programu pozostają nietknięte (czyli żadne struktury danych nie zostaną uszkodzone), a żadne zasoby nie wyciekną. Funkcje oferujące silną gwarancję bezpieczeństwa wątkowego (czyli gwarancję silną) zapew- niają kod wywołujący, że w przypadku powstania wyjątku stan programu pozostaje taki jak przed wywołaniem. Gdy odwołują się do obiektu funkcyjnego, zwykle mam na myśli obiekt typu ob- sługującego funkcję składową operator(). Innymi słowy, obiekt, który działa jak funkcja. Czasami używam tego terminu w nieco bardziej ogólnym sensie, aby określić cokolwiek, co może być wywołane przy użyciu składni wywołania funkcji globalnej (czyli „functionName(arguments)”). Ta szersza defi nicja obejmuje nie tylko obiekty obsługujące operator(), ale także funkcje i wskaźniki funkcji w stylu języka C. (Węższa defi nicja pochodzi z C++98, szersza z C++11). Dalsze uogólnienie przez dodanie wskaźników funkcji prowadzi do tego, co jest nazywane obiektami wywoły- walnymi. Możemy zasadniczo zignorować dokładnie rozróżnienie i po prostu myśleć ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Wprowadzenie | 5 o obiektach funkcyjnych i obiektach wywoływalnych jako elementach języka C++, które mogą być wywoływane przy użyciu pewnego rodzaju składni wywoływania funkcji. Obiekty funkcyjne tworzone przez wyrażenia lambda są nazywane domknięciami. Rzadko konieczne jest rozróżnianie między wyrażeniami lambda a tworzonymi przez nie domknięciami, więc często będę określał jedno i drugie jako wyraże- nia lambda. Analogicznie rzadko rozróżniam miedzy szablonami funkcji (czyli szablonami, które generują funkcje) a funkcjami szablonowymi (czyli funkcjami generowanymi na podstawie szablonów funkcji). Podobnie w przypadku szablonów klas i klas szablonowych. Wiele rzeczy w języku C++ może być zarówno deklarowane, jak i defi niowane. Deklaracje wprowadzają nazwy i typy bez podawania szczegółów, takich jak to, gdzie jest umieszczana pamięć lub jaka jest implementacja: extern int x; // deklaracja obiektu class Widget; // deklaracja klasy bool func(const Widget w); // deklaracja funkcji enum class Color; // deklaracja wyliczenia z zasięgiem // (patrz punkt 10) Defi nicje dostarczają informacje o lokalizacji w pamięci lub implementacji: int x; // definicja obiektu class Widget { // definicja klasy … }; bool func(const Widget w) { return w.size() 10; } // definicja funkcji enum class Color { Yellow, Red, Blue }; // definicja wyliczenia z zasięgiem 6 | Wprowadzenie ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Defi nicja także zalicza się do deklaracji, dlatego, o ile nie jest naprawdę ważne, że coś jest defi nicją, zwykle odwołuję się do deklaracji. Defi niuję sygnaturę funkcji jako część jej deklaracji, która określa typy parame- trów i wartości zwracanych. Nazwy funkcji i parametrów nie są częścią sygnatury. W powyższym przykładzie sygnaturą funkcji func jest bool(const Widget ). Elementy deklaracji funkcji inne niż typy parametrów i wartości zwracanych (np. noexcept lub constexpr, jeśli są obecne) są wykluczane (noexcept i constexpr są opisane w punktach 14 i 15). Ofi cjalna defi nicja „sygnatury” jest trochę inna od mojej, ale w tej książce moja defi nicja jest przydatna. (Ofi cjalna defi nicja czasami pomija typy zwracane). Nowe standardy języka C++ zasadniczo zachowują poprawność kodu zapisanego w starszych standardach, ale czasami komitet standaryzacyjny określa funkcjonal- ności jako przestarzałe (deprecate). Takie funkcjonalności oczekują w celi śmierci i zostaną usunięte z przyszłych standardów. Kompilatory mogą, ale nie muszą ostrzegać o używaniu przestarzałych funkcjonalności, jednak lepiej ich unikać. Nie tylko mogą prowadzić do późniejszych kłopotów z przenoszeniem, ale zasadniczo są gorsze od funkcjonalności, którymi zostały zastąpione. Na przykład wskaźnik std::auto_ptr jest przestarzały w C++11, ponieważ std::unique_ptr robi to samo, tylko lepiej. Czasami standard mówi, że wynikiem operacji jest niezdefi niowane działanie. Oznacza to, że działanie w trybie wykonania jest nieprzewidywalne i powinno być oczywiste, że chcesz uniknąć takiej niepewności. Przykłady czynności o niezdefi nio- wanym działaniu obejmują użycie nawiasów kwadratowych („[]”) do indeksowania poza granicami wektora std::vector, wyłuskiwanie niezainicjowanego iteratora lub spowodowanie wyścigu danych (czyli występowanie co najmniej dwóch wątków, z których przynajmniej jeden zapisuje, równocześnie uzyskujących dostęp do tej samej lokalizacji w pamięci). Wbudowane wskaźniki, takie jak zwracane przez instrukcję new, nazywam wskaź- nikami surowymi. Przeciwieństwem wskaźnika surowego jest wskaźnik inteligentny. Wskaźniki inteligentne zwykle przeciążają operatory wyłuskiwania wskaźników (operator- i operator*), chociaż, jak wyjaśnię w punkcie 20, wskaźnik std::weak_ptr jest wyjątkiem. ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Wprowadzenie | 7 Raportowanie błędów i sugerowanie poprawek Dołożyłem wszelkich starań, aby wypełnić tę książkę jasnymi, dokładnymi i przy- datnymi informacjami, ale na pewno można ją jeszcze poprawić. Jeśli znajdziesz błędy dowolnego typu (techniczne, logiczne, gramatyczne, typografi czne itp.) lub jeśli masz sugestie dotyczące sposobów udoskonalenia tej książki, napisz do mnie na adres emc++@aristeia.com. Nowe dodruki dają mi możliwość zmodyfi kowania książki Skuteczny nowoczesny C++, a nie mogę rozwiązać problemów, o których nie wiem! Aby wyświetlić listę problemów, o których już wiem, zajrzyj na stronę erraty książki pod adresem http://www.aristeia.com/BookErrata/emc++-errata.html. 8 | Wprowadzenie ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Rozdział 1 Dedukcja typów W języku C++98 istniał jeden zestaw reguł dedukcji typów używany w szablonach funkcji. W standardzie C++11 zmodyfi kowano go odrobinę i dodano dwa kolejne zestawy reguł, które dotyczą deklaracji auto i instrukcji decltype. W standardzie C++14 konteksty użycia technik auto i decltype zostały rozszerzone. Dzięki coraz obszerniejszym zastosowaniom dedukcji typów nie musimy pisać ich nazw wielo- krotnie ani w oczywistych sytuacjach. Powoduje to, że oprogramowanie C++ lepiej się adaptuje, ponieważ zmiana typu w jednym miejscu kodu źródłowego auto- matycznie propaguje się za pomocą dedukcji typów do innych lokalizacji. Jednak utworzony kod może być trudniejszy do zrozumienia, ponieważ typy dedukowane przez kompilator mogą nie być tak oczywiste, jak tego oczekujemy. Bez solidnej wiedzy na temat sposobu działania dedukcji typów skuteczne pro- gramowanie w nowoczesnym języku C++ jest prawie niemożliwe. Po prostu tech- nika ta występuje w zbyt wielu kontekstach: w wywołaniach szablonów funkcji, w większości sytuacji, gdzie występuje deklaracja auto, w wyrażeniach decltype, a w standardzie C++14 również tam, gdzie jest używana enigmatyczna konstrukcja decltype(auto). W tym rozdziale zawarte zostały informacje na temat dedukcji typów potrzebne każdemu programiście C++. Zawiera on objaśnienie dedukcji typów w szablonach, opartego na niej działania deklaracji auto, a także odmiennego sposobu działania instrukcji decltype. Dowiemy się także, jak zmusić kompilatory do uwidocznienia wyników dedukcji typów, abyśmy mogli upewnić się, że odbywa się ona zgodnie z oczekiwaniami. Punkt 1: Dedukcja typów w szablonach Kiedy użytkownicy złożonego systemu nie mają pojęcia, jak on działa, ale są z niego zadowoleni, mówi to wiele o projekcie systemu. Pod tym względem dedukcja ty- pów w szablonach w języku C++ jest ogromnym sukcesem. Miliony programistów ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== 9 przekazują argumenty do funkcji szablonowych z całkowicie satysfakcjonującymi wynikami, chociaż większość z nich czułaby się przyciśnięta do muru, gdyby mieli dać więcej niż mglisty opis sposobu dedukcji typów używanych przez te funkcje. Jeśli należysz do tej grupy, mam dobre i złe wiadomości. Dobra wiadomość to ta, że dedukcja typów dla szablonów jest podstawą jednej z najbardziej interesujących funkcjonalności nowoczesnego języka C++: auto. Osoby zadowolone z tego, jak zachodziła dedukcja typów w C++98 dla szablonów, będą na pewno zadowolone ze sposobu dedukcji typów C++11 dla deklaracji auto. Zła wiadomość jest taka, że gdy reguły dedukcji typów w szablonach są stosowane w kontekście auto, czasami działają mniej intuicyjnie niż przy stosowaniu ich do szablonów. Dlatego ważne jest, aby dobrze zrozumieć aspekty dedukcji typów w szablonach, na których oparta jest deklaracja auto. Wszystkie potrzebne informacje zawarte są w tym Sposobie. Jeśli zechcemy przyjrzeć się fragmentowi pseudokodu, szablon funkcji możemy przedstawić tak: template typename T void f(ParamType param); Wywołanie może wyglądać tak: f(expr); // wywołanie funkcji f z pewnym wyrażeniem Podczas kompilacji kompilatory używają wyrażenia expr do dedukcji dwóch typów: jednego dla T, a drugiego dla ParamType. Te typy są często różne, ponieważ ParamType często zawiera dodatkowe określenia, np. const lub kwalifi katory odwołań. Jeśli na przykład szablon jest zadeklarowany w taki sposób template typename T void f(const T param); // ParamType to const T i mamy takie wywołanie int x = 0; f(x); // wywołanie funkcji f z argumentem int wydedukowany typ T to int, ale wydedukowany typ ParamType to const int . Oczekiwanie, że wydedukowany typ T jest taki sam jak typ argumentu przekaza- nego do funkcji (czyli że T ma typ wyrażenia expr), jest naturalne. W powyższym 10 | Rozdział 1: Dedukcja typów ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== przykładzie tak jest: x ma typ int i T ma wydedukowany typ int. Ale to nie zawsze działa w ten sposób. Wydedukowany typ T zależy nie tylko od typu wyrażenia expr, ale także od formy ParamType. Istnieją tu trzy przypadki: 폷 ParamType to wskaźnik lub typ referencyjny, ale nie jest odwołaniem uniwersal- nym. (Odwołania uniwersalne są opisane w punkcie 24. Teraz wystarczy wie- dzieć, że istnieją i różnią się od odwołań do l-wartości i odwołań do r-wartości). 폷 ParamType to odwołanie uniwersalne. 폷 ParamType nie jest ani wskaźnikiem, ani odwołaniem. Dlatego mamy do zbadania trzy przypadki dedukcji typów. Każdy z nich będzie oparty na ogólnej formie szablonów i ich wywołań: template typename T void f(ParamType param); f(expr); // dedukcja T i ParamType na podstawie expr Przypadek 1: ParamType to odwołanie lub wskaźnik, ale nie odwołanie uniwersalne Najprostsza sytuacja występuje, gdy ParamType ma typ referencyjny lub wskaźni- kowy, ale nie jest odwołaniem uniwersalnym. W takim przypadku dedukcja typu działa następująco: 1. Jeśli typ wyrażenia expr to odwołanie, zignoruj jego referencyjność. 2. Następnie dopasuj do wzorca typ wyrażenia expr względem ParamType, aby wyznaczyć T. Jeśli weźmiemy taki przykład template typename T void f(T param); // param to odwołanie i takie deklaracje danych int x = 27; // x to int const int cx = x; // cx to const int const int rx = x; // rx to odwołanie do x jako const int wydedukowane typy param i T w różnych wywołaniach będą następujące: ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Punkt 1 | 11 f(x); // T to int, typ param to int f(cx); // T to const int, // typ param to const int f(rx); // T to const int, // to param to const int W drugim i trzecim wywołaniu można zauważyć, że ponieważ cx i rx oznaczają wartości const, typ T jest dedukowany jako const int, co powoduje, że typem pa- rametru jest const int . Jest to ważne podczas wywoływania. Gdy przekazujemy obiekt const do parametru odwołania, oczekujemy, że obiekt pozostanie niezmody- fi kowany, czyli parametr będzie odwołaniem do stałej (const). Dlatego przekazanie obiektu const do szablonu przyjmującego parametr T jest bezpieczne: stałość (const) obiektu staje się częścią wydedukowanego typu T. W trzecim przykładzie wprawdzie typ rx to odwołanie, jednak typ T jest dedukowany jako niereferencyjny. Dzieje się tak, ponieważ referencyjność rx jest ignorowana podczas dedukcji typu. Wszystkie te przykłady pokazały parametry odwołań do l-wartości, ale dedukcja typów działa dokładnie tak samo w przypadku parametrów odwołań do r-wartości. Oczywiście tylko argumenty r-wartości mogą być przekazywane przez parametry odwołań do r-wartości, ale to ograniczenie nie ma nic wspólnego z dedukcją typów. Jeśli zmienimy typ parametru funkcji f z T na const T , działanie zmieni się odro- binę, ale w niezbyt zaskakujący sposób. Stałość (const) danych cx i rx nadal będzie uwzględniana, ale ponieważ teraz zakładamy, że param jest odwołaniem do stałej (const), nie ma już potrzeby dedukcji deklaracji const jako części typu T: template typename T void f(const T param); // param to teraz odwołanie do const int x = 27; // jak poprzednio const int cx = x; // jak poprzednio const int rx = x; // jak poprzednio f(x); // T to int, typ param to const int f(cx); // T to int, typ param to const int 12 | Rozdział 1: Dedukcja typów ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== f(rx); // T to int, typ param to const int Jak poprzednio, referencyjność rx jest ignorowana podczas dedukcji typu. Jeżeli argument param byłby wskaźnikiem (lub wskaźnikiem do stałej, czyli const), zamiast odwołaniem, działanie byłoby dokładnie takie samo: template typename T void f(T* param); // param to teraz wskaźnik int x = 27; // jak poprzednio const int *px = x; // px to wskaźnik do x jako const int f( x); // T to int, typ param to int* f(px); // T to const int, // typ param to const int* Na pewno już ziewasz i przysypiasz, ponieważ reguły dedukcji typów w języku C++ działają tak naturalnie dla parametrów będących odwołaniami i wskaźnikami, że widziane w formie pisemnej są naprawdę nudne. Wszystko jest tak oczywiste! Tego właśnie oczekujemy w systemie dedukcji typów. Przypadek 2: ParamType jest odwołaniem uniwersalnym Mniej oczywiste jest działanie szablonów przyjmujących parametry odwołań uni- wersalnych. Takie parametry są deklarowane jako odwołania do r-wartości (np. w szablonie funkcji przyjmującym parametr T, typem deklarowanym odwołania uniwersalnego jest T ), ale działają inaczej, gdy przekażemy do nich argumenty będące l-wartościami. Dokładny opis znajduje się w punkcie 24, a jego wersja skrócona jest taka: 폷 Jeżeli expr to l-wartość, zarówno T, jak i ParamType są dedukowane jako odwo- łania do l-wartości. Jest to podwójnie niezwykłe. Po pierwsze jest to jedyna sytuacja dotycząca dedukcji typów szablonu, gdzie T jest dedukowane jako odwołanie. Po drugie, chociaż ParamType jest deklarowany przy użyciu skład- ni dla odwołania do r-wartości, jego dedukowanym typem jest odwołanie do l-wartości. Jeżeli expr to r-wartość, stosowane są „normalne” reguły (czyli Przypadek 1). 폷 ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Punkt 1 | 13 Na przykład: template typename T void f(T param); // param jest teraz odwołaniem uniwersalnym int x = 27; // jak poprzednio const int cx = x; // jak poprzednio const int rx = x; // jak poprzednio f(x); // x to l-wartość, więc T to int , // typ param to również int f(cx); // cx to l-wartość, więc T to const int , // typ param to także const int f(rx); // rx to l-wartość, więc T to const int , // typ param to także const int f(27); // 27 to r-wartość, więc T to int, // dlatego typ param to int W punkcie 24 zostało wyjaśnione dokładnie, dlaczego te przykłady tak działają. Najistotniejsze tutaj jest to, że reguły dedukcji typów dla parametrów odwołań uni- wersalnych są inne od tych dla parametrów, które są odwołaniami do l-wartości lub odwołaniami do r-wartości. W szczególności, gdy są używane odwołania uniwer- salne, dedukcja typów rozróżnia argumenty l-wartości od argumentów r-wartości. Nie dzieje się tak nigdy w przypadku odwołań innych niż uniwersalne. Przypadek 3: ParamType nie jest ani wskaźnikiem, ani odwołaniem Gdy ParamType nie jest ani wskaźnikiem, ani odwołaniem, mamy do czynienia z przekazywaniem przez wartość: template typename T void f(T param); // param jest teraz przekazywany przez wartość Oznacza to, że param będzie kopią tego, co zostanie przekazane – całkowicie no- wym obiektem. Fakt, że param będzie nowym obiektem, wpływa na reguły rządzące sposobem dedukcji T na podstawie wyrażenia expr: 14 | Rozdział 1: Dedukcja typów ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== 1. Jak poprzednio, jeśli typ expr jest odwołaniem, ignorujemy część referencyjną. 2. Jeżeli po zignorowaniu referencyjności w wyrażeniu expr, wyrażenie expr ma właściwość const, ignorujemy również ten fakt. Jeżeli jest obiektem volatile, również to ignorujemy. (Obiekty volatile są bardzo rzadko spotykane. Za- sadniczo wykorzystuje się je tylko przy implementacji sterowników urządzeń. Szczegółowy opis znajdziesz w punkcie 40). Stąd: int x = 27; // jak poprzednio const int cx = x; // jak poprzednio const int rx = x; // jak poprzednio f(x); // oba typy T i param to int f(cx); // ponownie oba typy T i param to int f(rx); // nadal oba typy T i param to int Zauważmy, że chociaż cx i rx reprezentują wartości const, param nie ma właściwości const. Ma to sens. param jest obiektem całkowicie niezależnym od cx i rx – kopią cx lub rx. Fakt, że cx i rx nie mogą być modyfi kowane, nie ma wpływu na to, czy można modyfi kować param. Dlatego określenia const (i ewentualnie volatile) wyrażenia expr są ignorowane podczas dedukcji typu parametru param: po prostu dlatego, że niemożność modyfi kacji wyrażenia expr nie oznacza, że nie można modyfi kować jego kopii. Ważne jest, aby zauważyć, że określenie const (i volatile) jest ignorowane tylko w przypadku parametrów przekazywanych przez wartość. Jak zobaczyliśmy, w przy- padku parametrów, które są odwołaniami lub wskaźnikami do stałych (const), właściwość const wyrażenia expr jest zachowywana. Rozważmy jednak przypadek, gdy expr jest stałym (const) wskaźnikiem do stałego (const) obiektu, a wyrażenie expr jest przekazywane do parametru param przez wartość: template typename T void f(T param); // param jest nadal przekazywany przez wartość const char* const ptr = // ptr to stały wskaźnik do stałego obiektu Fun with pointers ; ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Punkt 1 | 15 f(ptr); // przekazanie argumentu typu const char * const Tutaj const po prawej stronie gwiazdki powoduje deklarację wskaźnika ptr jako const: ptr nie może wskazać na inną lokalizację, ani nie może mieć przypisanej wartości null. (const po lewej stronie od gwiazdki oznacza, że to, na co wskazuje wskaźnik ptr – ciąg znaków – jest również const, więc nie może być zmodyfi kowa- ny). Gdy argument ptr jest przekazywany do funkcji f, bity stanowiące wskaźnik są kopiowane do parametru param. W ten sposób sam wskaźnik (ptr) jest przekazy- wany przez wartość. W połączeniu z regułą dedukcji typów dla parametrów prze- kazywanych przez wartość, właściwość const wskaźnika ptr zostanie zignorowana, a wydedukowanym typem parametru param będzie const char*, czyli modyfi kowalny wskaźnik do stałego (const) ciągu znaków. Właściwość const tego, na co wskazu- je wskaźnik ptr, jest zachowywana podczas dedukcji typu, ale właściwość const samego wskaźnika ptr jest ignorowana podczas kopiowania w celu utworzenia nowego wskaźnika param. Argumenty tablicowe Mniej więcej opisałem główne zasady dedukcji typów szablonowych, ale istnieje niszowy przypadek, o którym warto wiedzieć. Chodzi o to, że typy tablicowe różnią się od typów wskaźnikowych, chociaż czasami wydaje się, że można je wymieniać. Główną przyczyną tej iluzji jest to, że w wielu kontekstach tablicę można sprowadzić do wskaźnika na jej pierwszy element. To sprowadzenie sprawia, że da się skom- pilować taki kod, jak poniższy: const char name[] = J. P. Briggs ; // typ name to // const char[13] const char * ptrToName = name; // tablica jest sprowadzana // do wskaźnika Tutaj wskaźnik const char* o nazwie ptrToName jest inicjowany za pomocą zmiennej name typu const char[13]. Te typy (const char* i const char[13]) nie są takie same, ale z powodu reguły sprowadzania tablicy do wskaźnika kod się kompiluje. Co jednak stanie się wtedy, kiedy przekażemy tablicę do szablonu przyjmującego parametr przez wartość? template typename T void f(T param); // szablon z parametrem przez wartość 16 | Rozdział 1: Dedukcja typów ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== f(name); // jakie typy są dedukowane dla T i param? Zaczniemy od spostrzeżenia, że nie ma czegoś takiego jak parametr funkcji, który jest tablicą. Tak, tak, poniższa składnia jest dozwolona: void myFunc(int param[]); ale deklaracja funkcji jest traktowana jako deklaracja wskaźnika, co oznacza, że funkcja myFunc mogłaby być równoważnie zadeklarowana tak: void myFunc(int* param); // ta sama funkcja, co wyżej Ta równoważność parametrów tablicowych i wskaźnikowych wynika z zakorzenie- nia języka C++ w języku C i powoduje mylne wrażenie, że typy tablicowe i wskaź- nikowe są takie same. Ponieważ deklaracje parametrów tablicowych są traktowane tak, jak gdyby były parametrami wskaźnikowymi, typ tablicy przekazywany do funkcji przez wartość jest dedukowany jako typ wskaźnikowy. Oznacza to, że w wywołaniu szablonu f jego parametr typu T jest dedukowany jako const char*: f(name); // name to tablica, ale T jest dedukowane jako const char* Ale w tym tkwi haczyk. W funkcjach nie można deklarować parametrów, które są prawdziwymi tablicami, natomiast można w nich deklarować parametry, które są odwołaniami do tabel! Dlatego, jeżeli zmodyfi kujemy szablon f, aby przyjmował argument przez odwołanie template typename T void f(T param); // szablon z parametrem przez odwołanie i przekażemy do niego tablicę f(name); // przekazanie tablicy do f typem wydedukowanym dla T jest faktyczny typ tablicy! Ten typ zawiera rozmiar tablicy, więc w tym przykładzie T jest dedukowany jako const char [13], a typem parametru funkcji f (odwołania do tablicy) jest const char ( )[13]. Tak, składnia wygląda toksycznie, ale jej znajomość pozwala zapunktować u tych nielicznych osób, które o to dbają. ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Punkt 1 | 17 Interesujące jest oto, że możliwość deklarowania odwołań do tablic umożliwia tworzenie szablonu służącego do dedukcji liczby elementów zawartych w tablicy: // zwraca rozmiar tablicy jako stałą czasu kompilacji. // (Parametr tablicowy nie ma nazwy, ponieważ interesuje nas // tylko liczba zawartych w tablicy elementów). template typename T, std::size_t N // zobacz poniższe constexpr std::size_t arraySize(T ( )[N]) noexcept // informacje { // o constexpr return N; // i noexcept } Zgodnie z opisem zawartym w punkcie 15 zadeklarowanie tej funkcji jako constexpr sprawia, że jej wynik jest dostępny w czasie kompilacji. Dlatego możliwe jest zade- klarowanie, powiedzmy, tablicy z taką samą liczbą elementów, jaką ma inna tablica, której rozmiar jest obliczony na podstawie inicjatora klamrowego: int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals ma // 7 elementów int mappedVals[arraySize(keyVals)]; // tyle samo ma // mappedVals Oczywiście, jako nowoczesny programista C++, wolisz typ std::array od wbudo- wanego typu tablicowego: std::array int, arraySize(keyVals) mappedVals; // rozmiar mappedVals // wynosi 7 Zadeklarowanie arraySize jako noexcept pomaga kompilatorom generować lepszy kod. Szczegółowe informacje znajdziesz w punkcie 14. Argumenty funkcyjne Tablice nie są jedynymi elementami języka C++, które można sprowadzić do wskaź- ników. Typy funkcji można sprowadzić do wskaźników funkcji, a wszystko, co omó- wiliśmy odnośnie dedukcji typów dla tablic, dotyczy dedukcji typów dla funkcji oraz ich sprowadzania do wskaźników funkcji. W efekcie otrzymujemy: void someFunc(int, double); // someFunc to funkcja; 18 | Rozdział 1: Dedukcja typów ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== // jej typ to void(int, double) template typename T void f1(T param); // w f1 param jest przekazywany przez wartość template typename T void f2(T param); // w f2 param jest przekazywany przez odwołanie f1(someFunc); // param jest dedukowany jako wskaźnik do funkcji; // typ to void (*)(int, double) f2(someFunc); // param jest dedukowany jako odwołanie do funkcji; // typ to void ( )(int, double) W praktyce różnica jest prawie nieistotna, ale jeżeli chcesz wiedzieć o sprowadza- niu tablicy do wskaźnika, możesz także dowiedzieć się o sprowadzaniu funkcji do wskaźnika. I tu dochodzimy do sedna: reguł związanych z deklaracją auto dotyczących dedukcji typów w szablonach. Wspomniałem na początku, że są one dość proste, i prze- ważnie takie są. Specjalne traktowanie uzgodnionych l-wartości podczas dedukcji typów dla odwołań uniwersalnych wprowadza odrobinę zamieszania, a reguły sprowadzania do wskaźnika dotyczące tablic i funkcji wprowadzają jeszcze większy zamęt. Czasami po prostu chcemy zażądać od kompilatora „Powiedz mi, jaki typ dedukujesz!” Wtedy warto skorzystać z punktu 4, w którym pokażę, jak skłonić kompilatory do takiego działania. Warto zapamiętać 폷 Podczas dedukcji typu w szablonie argumenty będące odwołaniami nie są traktowane jako odwołania, czyli ich referencyjność jest ignorowana. 폷 Podczas dedukcji typów dla parametrów odwołań uniwersalnych argumenty l-wartości są traktowane w specjalny sposób. 폷 Podczas dedukcji typów dla parametrów przekazywanych przez wartość argumenty const i/lub volatile są traktowane jako pozbawione tych cech. 폷 Podczas dedukcji typu w szablonie argumenty, które są nazwami tablic lub funkcji, są sprowadzane do wskaźników, o ile nie użyjemy ich do inicjo- wania odwołań. ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== Punkt 1 | 19 Punkt 2: Dedukcja typu auto Po przeczytaniu punktu 1 dotyczącego dedukcji typów w szablonach, wiesz już prawie wszystko, co trzeba wiedzieć na temat dedukcji typów auto, ponieważ poza tylko jednym osobliwym wyjątkiem dedukcja typów auto to jest dedukcja typów w szablonach. Jak to możliwe? Dedukcja typów w szablonach obejmuje szablony, funkcje i parametry, natomiast auto nie ma z nimi nic do czynienia. To prawda, ale nie ma znaczenia. Istnieje bezpośrednie odwzorowanie dedukcji typów w szablonach na dedukcję typów auto. Istnieje dokładna algorytmiczna transformacja jednego rodzaju w drugi. W punkcie 1 dedukcja typów w szablonach została wyjaśniona na przykładzie tego ogólnego szablonu funkcji: template typename T void f(ParamType param); i tego ogólnego wywołania: f(expr); // wywołanie f z pewnym wyrażeniem W wywołaniu funkcji f kompilatory korzystają z wyrażenia expr do dedukcji typów dla T i ParamType. Gdy zmienne są deklarowane przy użyciu auto, auto odgrywa rolę T w szablonie, a określenie typu dla zmiennej pełni rolę ParamType. Jest to łatwiej pokazać niż opisać, więc rozważmy ten przykład: auto x = 27; Tutaj określeniem typu zmiennej x jest po prostu samo auto. Z drugiej strony w tej deklaracji const auto cx = x; określeniem typu jest const auto. Natomiast tutaj const auto rx = x; 20 | Rozdział 1: Dedukcja typów ##7#52#aSUZPUk1BVC1WaXJ0dWFsbw== określeniem typu jest const auto . W celu dedukcji typów dla x, cx i rx w tych przy- kładach kompilatory działają tak, jakby dla każdej deklaracji był podany szablon, a także wywołanie do tego szablonu z odpowiednim wyrażeniem inicjując
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Skuteczny nowoczesny C++
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: