Nowoczesny C++ Zbiór praktycznych zadań dla przyszłych ekspertów Marius Bancila Tytuł oryginału: The Modern C++ Challenge Tłumaczenie: Jacek Janusz ISBN: 978-83-283-5212-4 Copyright © Packt Publishing 2018. First published in the English language under the title ‘The Modern C++ Challenge – (9781788993869)’ Polish edition copyright © 2019 by Helion SA All rights reserved. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Helion SA dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Helion SA nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Helion SA ul. Kościuszki 1c, 44-100 Gliwice tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/nowcpp_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Poleć książkę na Facebook.com Księgarnia internetowa Kup w wersji papierowej Lubię to! » Nasza społeczność Oceń książkę ecb84badecb8c394873734f1e9bfb90f e Spis treści O autorze 9 O recenzentach 10 Przedmowa 11 Rozdział 1. Zadania matematyczne 19 Zadania 1. Suma liczb naturalnych podzielnych przez 3 lub 5 2. Największy wspólny dzielnik 3. Najmniejsza wspólna wielokrotność 4. Największa liczba pierwsza mniejsza od podanej 5. Liczby pierwsze szóstkowe 6. Liczby obfite 7. Liczby zaprzyjaźnione 8. Liczby Armstronga 9. Czynniki pierwsze liczby 10. Kod Graya 11. Przekształcanie liczb arabskich na rzymskie 12. Najdłuższy ciąg Collatza 13. Wyznaczanie liczby 14. Sprawdzanie numerów ISBN Rozwiązania 1. Suma liczb naturalnych podzielnych przez 3 lub 5 2. Największy wspólny dzielnik 3. Najmniejsza wspólna wielokrotność 4. Największa liczba pierwsza mniejsza od podanej 5. Liczby pierwsze szóstkowe 6. Liczby obfite 7. Liczby zaprzyjaźnione 8. Liczby Armstronga ecb84badecb8c394873734f1e9bfb90f 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 21 21 21 22 23 24 24 25 26 e Spis treści 9. Czynniki pierwsze liczby 10. Kod Graya 11. Przekształcanie liczb arabskich na rzymskie 12. Najdłuższy ciąg Collatza 13. Wyznaczanie liczby 14. Sprawdzanie numerów ISBN Rozdział 2. Funkcje języka 27 28 29 31 32 33 35 Zadania 15. Typ danych IPv4 16. Wyliczanie zakresu adresów IPv4 17. Utworzenie dwuwymiarowej tablicy z podstawowymi operacjami 18. Funkcja wyznaczająca minimum dla dowolnej liczby argumentów 19. Dodawanie zakresu wartości do kontenera 20. Dowolny, wszystkie lub żaden argument w kontenerze 21. Klasa opakowująca dla uchwytu systemowego 22. Wyświetlanie różnych skal temperatur Rozwiązania 15. Typ danych IPv4 16. Wyliczanie zakresu adresów IPv4 17. Utworzenie dwuwymiarowej tablicy z podstawowymi operacjami 18. Funkcja wyznaczająca minimum dla dowolnej liczby argumentów 19. Dodawanie zakresu wartości do kontenera 20. Dowolny, wszystkie lub żaden argument w kontenerze 21. Klasa opakowująca dla uchwytu systemowego 22. Wyświetlanie różnych skal temperatur Rozdział 3. Łańcuchy i wyrażenia regularne 35 35 35 35 36 36 36 36 36 37 37 38 40 42 43 44 45 49 53 Zadania 23. Zamiana typu binarnego na łańcuch 24. Zamiana typu łańcuchowego na binarny 25. Wielkie litery w tytule artykułu 26. Łączenie łańcuchów oddzielanych separatorem 27. Dzielenie łańcucha na tokeny z listą możliwych separatorów 28. Najdłuższy podciąg palindromiczny 29. Sprawdzanie tablic rejestracyjnych 30. Wyodrębnianie elementów adresu URL 31. Przekształcanie dat w łańcuchach Rozwiązania 23. Zamiana typu binarnego na łańcuch 24. Zamiana typu łańcuchowego na binarny 25. Wielkie litery w tytule artykułu 26. Łączenie łańcuchów oddzielanych separatorem 27. Dzielenie łańcucha na tokeny z listą możliwych separatorów 28. Najdłuższy podciąg palindromiczny 29. Sprawdzanie tablic rejestracyjnych 30. Wyodrębnianie elementów adresu URL 31. Przekształcanie dat w łańcuchach 4 ecb84badecb8c394873734f1e9bfb90f 53 53 53 54 54 54 54 54 55 55 56 56 57 58 59 60 61 63 64 65 e Spis treści Rozdział 4. Strumienie i systemy plików 67 Zadania 32. Trójkąt Pascala 33. Lista procesów w postaci tabeli 34. Usuwanie pustych wierszy z pliku tekstowego 35. Obliczanie rozmiaru katalogu 36. Usuwanie plików starszych od określonej daty 37. Wyszukiwanie w katalogu plików, które pasują do wyrażenia regularnego 38. Tymczasowe pliki logów Rozwiązania 32. Trójkąt Pascala 33. Lista procesów w postaci tabeli 34. Usuwanie pustych wierszy z pliku tekstowego 35. Obliczanie rozmiaru katalogu 36. Usuwanie plików starszych od określonej daty 37. Wyszukiwanie w katalogu plików, które pasują do wyrażenia regularnego 38. Tymczasowe pliki logów 67 67 67 68 68 68 68 68 69 69 70 72 73 73 75 76 Rozdział 5. Data i czas 79 Zadania 39. Pomiar czasu wykonania funkcji 40. Liczba dni zawartych między dwiema datami 41. Dzień tygodnia 42. Numer dnia i tygodnia w roku 43. Czasy spotkań dla wielu stref czasowych 44. Kalendarz miesięczny Rozwiązania 39. Pomiar czasu wykonania funkcji 40. Liczba dni zawartych między dwiema datami 41. Dzień tygodnia 42. Numer dnia i tygodnia w roku 43. Czasy spotkań dla wielu stref czasowych 44. Kalendarz miesięczny 79 79 79 79 79 80 80 81 81 82 83 83 84 86 Rozdział 6. Algorytmy i struktury danych 89 Zadania 45. Kolejka priorytetowa 46. Bufor cykliczny 47. Podwójne buforowanie 48. Najczęściej występujący element w zbiorze danych 49. Histogram tekstu 50. Filtrowanie listy numerów telefonów 51. Przekształcanie listy numerów telefonów 52. Generowanie wszystkich permutacji ciągu znaków 53. Średnia ocena filmów 54. Algorytm tworzenia par 55. Algorytm scalania 56. Algorytm wyboru 57. Algorytm sortowania 89 89 90 90 90 90 91 91 91 91 91 92 92 92 5 ecb84badecb8c394873734f1e9bfb90f e Spis treści 58. Najkrótsza ścieżka między węzłami 59. Program Weasel 60. Gra w życie Rozwiązania 45. Kolejka priorytetowa 46. Bufor cykliczny 47. Podwójne buforowanie 48. Najczęściej występujący element w zbiorze danych 49. Histogram tekstu 50. Filtrowanie listy numerów telefonów 51. Przekształcanie listy numerów telefonów 52. Generowanie wszystkich permutacji ciągu znaków 53. Średnia ocena filmów 54. Algorytm tworzenia par 55. Algorytm scalania 56. Algorytm wyboru 57. Algorytm sortowania 58. Najkrótsza ścieżka między węzłami 59. Program Weasel 60. Gra w życie Rozdział 7. Współbieżność 92 93 93 95 95 97 100 102 103 105 106 107 109 110 111 112 113 116 120 122 127 Zadania 61. Algorytm przekształcania współbieżnego 62. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu wątków 63. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu funkcji asynchronicznych 64. Algorytm sortowania współbieżnego 65. Wyświetlanie komunikatów w konsoli w sposób bezpieczny dla wątków 66. System obsługi klienta Rozwiązania 61. Algorytm przekształcania współbieżnego 62. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu wątków 63. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu funkcji asynchronicznych 64. Algorytm sortowania współbieżnego 65. Wyświetlanie komunikatów w konsoli w sposób bezpieczny dla wątków 66. System obsługi klienta Rozdział 8. Wzorce projektowe 127 127 127 128 128 128 128 129 129 130 132 134 136 137 141 Zadania 67. Sprawdzanie poprawności haseł 68. Generowanie losowych haseł 69. Generowanie numerów ubezpieczenia socjalnego 70. System zatwierdzania 71. Obserwowany kontener typu wektorowego 72. Obliczanie ceny zamówienia z rabatami 6 ecb84badecb8c394873734f1e9bfb90f 141 141 141 141 142 142 143 e Spis treści Rozwiązania 67. Sprawdzanie poprawności haseł 68. Generowanie losowych haseł 69. Generowanie numerów ubezpieczenia socjalnego 70. System zatwierdzania 71. Obserwowany kontener typu wektorowego 72. Obliczanie ceny zamówienia z rabatami 144 144 147 151 155 158 163 Rozdział 9. Serializacja danych 169 Zadania 73. Serializacja danych do pliku XML i deserializacja ich z niego 74. Pobieranie danych z pliku XML przy użyciu języka XPath 75. Serializacja danych do formatu JSON 76. Deserializacja danych z formatu JSON 77. Tworzenie pliku PDF z listą filmów 78. Tworzenie pliku PDF na podstawie zbioru obrazów Rozwiązania 73. Serializacja danych do pliku XML i deserializacja ich z niego 74. Pobieranie danych z pliku XML przy użyciu języka XPath 75. Serializacja danych do formatu JSON 76. Deserializacja danych z formatu JSON 77. Tworzenie pliku PDF z listą filmów 78. Tworzenie pliku PDF na podstawie zbioru obrazów 169 169 170 170 170 171 171 172 172 175 177 178 180 183 Rozdział 10. Archiwa, obrazy i bazy danych 187 Zadania 79. Wyszukiwanie plików w archiwum ZIP 80. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum 81. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum z zastosowaniem hasła 82. Tworzenie pliku PNG z flagą narodową 83. Tworzenie obrazu PNG zawierającego tekst weryfikacyjny 84. Generator kodów kreskowych EAN-13 85. Odczytywanie informacji o filmach z bazy SQLite 86. Wstawianie w sposób transakcyjny informacji o filmach do bazy danych SQLite 87. Obsługa multimediów w bazie danych SQLite Rozwiązania 79. Wyszukiwanie plików w archiwum ZIP 80. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum 81. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum z zastosowaniem hasła 82. Tworzenie pliku PNG z flagą narodową 83. Tworzenie obrazu PNG zawierającego tekst weryfikacyjny 84. Generator kodów kreskowych EAN-13 85. Odczytywanie informacji o filmach z bazy SQLite 86. Wstawianie w sposób transakcyjny informacji o filmach do bazy danych SQLite 87. Obsługa multimediów w bazie danych SQLite 7 ecb84badecb8c394873734f1e9bfb90f 187 187 187 188 188 188 189 189 189 190 191 191 192 196 198 199 202 207 212 216 e Spis treści Rozdział 11. Kryptografia 225 Zadania 88. Szyfr Cezara 89. Szyfr Vigenère’a 90. Kodowanie i dekodowanie base64 91. Sprawdzanie poprawności uwierzytelniania użytkowników 92. Wyznaczanie skrótów dla plików 93. Szyfrowanie i deszyfrowanie plików 94. Podpisywanie plików Rozwiązania 88. Szyfr Cezara 89. Szyfr Vigenère’a 90. Kodowanie i dekodowanie base64 91. Sprawdzanie poprawności uwierzytelniania użytkowników 92. Wyznaczanie skrótów dla plików 93. Szyfrowanie i deszyfrowanie plików 94. Podpisywanie plików Rozdział 12. Praca w sieci i usługi 225 225 225 225 226 226 226 226 227 227 228 231 236 239 240 242 247 Zadania 95. Znajdowanie adresu IP dla hosta 96. Gra Fizz-Buzz klient-serwer 97. Kursy wymiany bitcoinów 98. Pobieranie wiadomości e-mailowych przy użyciu protokołu IMAP 99. Tłumaczenie tekstu na dowolny język 100. Wykrywanie twarzy na obrazie Rozwiązania 95. Znajdowanie adresu IP dla hosta 96. Gra Fizz-Buzz klient-serwer 97. Kursy wymiany bitcoinów 98. Pobieranie wiadomości e-mailowych przy użyciu protokołu IMAP 99. Tłumaczenie tekstu na dowolny język 100. Wykrywanie twarzy na obrazie 247 247 247 248 248 248 248 249 249 250 255 258 263 267 Bibliografia 277 Skorowidz 281 8 ecb84badecb8c394873734f1e9bfb90f e O autorze Marius Bancila jest inżynierem oprogramowania z 15-letnim doświadczeniem w zakresie opracowywania rozwiązań dla sektorów przemysłowego i finansowego. Jest autorem książki Modern C++ Programming Cookbook. Koncentruje się na technologiach firmy Microsoft i rozwija głównie aplikacje desktopowe przy użyciu języków C++ oraz C#. Lubi dzielić się swoją wiedzą techniczną z innymi, dlatego też od ponad dekady posiada tytuł Microsoft MVP. Można się z nim skontaktować na Twitterze, używając adresu @mariusbancila. Moje podziękowania otrzymują: Nikhil Borkar, Jijo Maliyekal, Chaitanya Nair, Nitin Dasan, a także osoby z wydawnictwa Packt, które przyczyniły się do powstania tej książki. Chciałbym również podziękować recenzentom, którzy pozytywnie zaopiniowali książkę i sprawili, że stała się jeszcze lepsza. Wreszcie przekazuję specjalne podziękowania mojej żonie i rodzinie, która wspierała mnie w pracy nad tym projektem. ecb84badecb8c394873734f1e9bfb90f e O recenzentach Aivars Kalvāns jest głównym architektem oprogramowania w firmie Tieto Latvia. Od ponad 16 lat pracuje nad systemem kart płatniczych Card Suite i zarządza wieloma podstawowymi bibliotekami i programami języka C++. Jest również odpowiedzialny za wskazówki programistyczne dla języka C++, szkolenia dotyczące kodowania odpornego na błędy, a także za przeglądy kodu. Organizuje wewnętrzne spotkania programistów C++ i zabiera na nich głos. Chciałbym podziękować mojej uroczej żonie Anete oraz synom Kārlisowi, Gustavsowi i Leo za uczynienie mojego życia znacznie ciekawszym. Arun Muralidharan jest projektantem oprogramowania systemowego, sprawnie poruszającym się po wszystkich warstwach stosu technologicznego. Posiada ponad 8-letnie doświadczenie programistyczne. Projektowanie systemów rozproszonych, architektura, systemy zdarzeń, skalowalność, wydajność i języki programowania to tylko kilka spośród zagadnień, które go najbardziej interesują. Jest zagorzałym fanem języka C++ i jego metaprogramowania za pomocą szablonów. Lubi duże wyzwania i większość czasu spędza, zajmując się zagadnieniami języka C++. Chciałbym w tym miejscu podziękować całej społeczności C++, od której wiele się nauczyłem przez wszystkie minione lata. Nibedit Dey to nowoczesny przedsiębiorca z multidyscyplinarnym zapleczem technologicznym. Posiada tytuł licencjacki w dziedzinie inżynierii biomedycznej i magistra projektowania cyfrowego oraz systemów wbudowanych. Przed rozpoczęciem swojej działalności jako przedsiębiorcy pracował przez kilka lat w firmach L&T i Tektronix na różnych stanowiskach związanych z badaniami oraz rozwojem. Przez ostatnie 8 lat używał języka C++ do tworzenia złożonych systemów opartych na oprogramowaniu. ecb84badecb8c394873734f1e9bfb90f e Przedmowa C++ jest językiem programowania ogólnego przeznaczenia, który łączy różne paradygmaty, takie jak programowanie obiektowe, imperatywne, generyczne i funkcyjne. C++ został zaprojektowany pod kątem wydajności i jest podstawowym wyborem dla aplikacji, w których jest ona kluczowa. W ciągu ostatnich kilku dekad C++ był jednym z najczęściej używanych języków programowania w przemyśle, środowisku akademickim i gdziekolwiek indziej. Został ustandaryzowany przez Międzynarodową Organizację Normalizacyjną (ISO), która obecnie pracuje nad kolejną wersją standardu, nazwaną C++ 20. Ma ona zostać zaprezentowana w 2020 roku. Ponieważ definicja standardu mieści się na prawie 1500 stronach, C++ nie jest najprostszym językiem do nauczenia się i opanowania. Umiejętności z nim związane nie są nabywane jedynie poprzez czytanie dokumentacji lub obserwowanie innych osób, które wykonują określone działania, lecz przez ciągłe ćwiczenia. Podobnie jest z programowaniem. Będąc programistami, nie uczymy się nowych języków ani technologii jedynie poprzez czytanie książek czy artykułów lub oglądanie samouczków wideo. Zamiast tego potrzebujemy praktyki, aby zapamiętać i wzmocnić nową wiedzę, którą staramy się przyswoić, w celu jej ostatecznego opanowania. Często jednak znalezienie dobrych ćwiczeń służących do testowania naszej wiedzy jest trudnym zadaniem. Choć na wielu stronach internetowych prezentowane są zadania związane z różnymi językami programowania, to większość z nich dotyczy problemów matematycznych, algorytmów lub zagadnień związanych z konkursami dla studentów. Tego rodzaju zadania nie pozwalają wykonywać ćwiczeń, w których wykorzystuje się różnorodne funkcjonalności danego języka programowania. W tym właśnie miejscu pojawia się ta książka. Jest ona zbiorem 100 przykładowych zadań dotyczących świata rzeczywistego, zdefiniowanych w taki sposób, abyś mógł się zapoznać z dużą różnorodnością języka C++ i jego standardowej biblioteki, a także wieloma zewnętrznymi bibliotekami wieloplatformowymi. Niektóre z tych zadań są specyficzne dla C++, lecz na ogół można je rozwiązać w wielu językach programowania. Oczywiście celem tej książki jest pomóc Ci opanować C++, a zatem oczekuje się, że zadania rozwiążesz w tym języku. Wszystkie rozwiązania zawarte w książce dotyczą ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów więc języka C++. Ucząc się innych języków programowania, możesz wykorzystać tę książkę jako źródło przykładowych zadań, chociaż w tym przypadku nie będziesz czerpać korzyści z dostępnych rozwiązań. Zadania w tej książce zostały umieszczone w 12 rozdziałach. Każdy rozdział zawiera zadania dotyczące podobnych lub pokrewnych tematów. Mają one różne poziomy trudności; niektóre z nich są łatwe, inne umiarkowane, a jeszcze inne trudne. Książka ma względnie równą liczbę zadań na każdym poziomie trudności. Każdy rozdział rozpoczyna się od opisu proponowanych zagadnień, a sposób rozwiązania zadań wynika z zaleceń, wyjaśnień i kodu źródłowego. Chociaż w książce możesz znaleźć gotowe rozwiązania, zaleca się, abyś spróbował je najpierw wdrożyć samodzielnie. Dopiero później (lub jeśli masz trudności z ukończeniem zadań) przyjrzyj się proponowanym rozwiązaniom. W kodzie źródłowym przedstawionym w książce brakuje tylko jednej rzeczy — nagłówków, które musisz uwzględnić. Zostały one celowo pominięte, abyś sam je zdefiniował. Z drugiej strony kod źródłowy dostarczony wraz z książką jest kompletny i możesz w nim znaleźć wszystkie wymagane nagłówki. W chwili pisania tej książki wersja standardu C++ 20 jest w trakcie tworzenia, a proces ten będzie kontynuowany przez następne kilka lat. Jednak niektóre funkcje zostały już poddane pod głosowanie, a jedną z nich jest rozszerzenie biblioteki chrono o kalendarze i strefy czasowe. W rozdziale 5. znajdziesz kilka zadań związanych z tym zagadnieniem i chociaż żaden kompilator nie wspiera jeszcze tych rozszerzeń, same zadania można rozwiązać przy użyciu biblioteki date, na podstawie której zaprojektowano nowe dodatki standardu. Do rozwiązywania zadań użyto w tej książce także wielu innych bibliotek. Są to: Asio, Crypto++, Curl, NLohmann/json, PDF-Writer, PNGWriter, pugixml, SQLite i ZipLib. Ponadto alternatywą dla wykorzystywanych w książce bibliotek std::optional i filesystem może być biblioteka Boost z kompilatorami. Możesz jej użyć w tych przypadkach, w których wspomniane wcześniej biblioteki nie są dostępne. Wszystkie one mają otwarty kod i działają na wielu platformach. Zostały wybrane z powodów, wśród których można wymienić wydajność, dobrą dokumentację i szerokie zastosowanie w społeczności. W celu rozwiązania zadań możesz jednak użyć dowolnych innych bibliotek. Dla kogo jest przeznaczona ta książka? Czy chcesz się nauczyć języka C++ i szukasz wyzwań, aby przećwiczyć zdobywaną wiedzę? Jeśli tak, ta książka jest dla Ciebie. Książka jest przeznaczona dla osób uczących się języka C++, niezależnie od ich doświadczenia z innymi językami programowania. Ma ona stać się cennym zasobem praktycznych ćwiczeń dotyczących rozwiązywania zadań związanych ze światem rzeczywistym. W tej książce nie zaprezentujemy opcji języka ani standardowej biblioteki. Taką wiedzę powinieneś zdobyć z innych źródeł, takich jak dokumentacja, artykuły lub samouczki wideo. Niniejsza pozycja ma być Twoim towarzyszem nauki i rzucać Ci wyzwania polegające na rozwiązywaniu zadań o różnym poziomie trudności z wykorzystaniem umiejętności, które wcześniej zdobyłeś dzięki innym źródłom. Niemniej jednak wiele zadań zaproponowanych w tej książce jest uniwersalnych i można z nich korzystać przy uczeniu się innych języków programowania, chociaż w tej sytuacji nie będziesz czerpać korzyści z dostępnych rozwiązań. 12 ecb84badecb8c394873734f1e9bfb90f e Przedmowa Jakie zagadnienia omówiono w tej książce? Rozdział 1. „Zadania matematyczne” zawiera szereg ćwiczeń matematycznych, które pomogą Ci sprostać trudniejszym problemom pojawiającym się w następnych rozdziałach. Rozdział 2. „Funkcje języka” proponuje zagadnienia związane z przeciążaniem operatorów, semantyką przenoszenia, literałami definiowanymi przez użytkownika oraz aspektami metaprogramowania za pomocą szablonów, takimi jak funkcje wariadyczne, wyrażenia fold i cechy typu. Rozdział 3. „Łańcuchy i wyrażenia regularne” zawiera kilka zadań związanych z manipulowaniem łańcuchami, takich jak konwertowanie pomiędzy łańcuchami i innymi typami danych, dzielenie i łączenie łańcuchów, a także zadań dotyczących obsługi wyrażeń regularnych. Rozdział 4. „Strumienie i systemy plików” obejmuje manipulowanie strumieniem wyjściowym oraz obsługę plików i katalogów przy użyciu biblioteki filesystem języka C++ 17. Rozdział 5. „Data i czas” przygotowuje Cię do nadchodzących rozszerzeń C++ 20 związanych z biblioteką chrono, a także zapoznaje Cię z kilkoma zadaniami dotyczącymi kalendarza i strefy czasowej, możliwymi do rozwiązania za pomocą biblioteki date, na której zostały oparte nowe dodatki standardu. Rozdział 6. „Algorytmy i struktury danych” jest jednym z najbardziej obszernych i omawia wiele zagadnień, w przypadku których należy wykorzystać istniejące algorytmy standardowe. Inaczej jest wówczas, gdy trzeba wdrożyć własne algorytmy ogólnego przeznaczenia lub struktury danych, takie jak bufor cykliczny i kolejka priorytetowa. Rozdział kończy się dwoma zadaniami związanymi z grami: programem Weasel (łasica), opartym na symulacji autorstwa Richarda Dawkinsa, oraz programem Gra w życie autorstwa Johna Conwaya, dzięki którym można poznać algorytmy ewolucyjne i automaty komórkowe. Rozdział 7. „Współbieżność” to miejsce, w którym nie tylko wykorzystujemy wątki i funkcje asynchroniczne do implementacji algorytmów równoległych ogólnego zastosowania, ale także rozwiązujemy niektóre zadania dotyczące współbieżności pochodzące ze świata rzeczywistego. Rozdział 8. „Wzorce projektowe” proponuje szereg zadań możliwych do rozwiązania za pomocą wzorców projektowych, takich jak dekorator, kompozyt, łańcuch odpowiedzialności czy metoda szablonowa. Rozdział 9. „Serializacja danych” omawia najczęściej spotykane formaty serializowania danych, czyli JSON i XML, wraz ze związanymi z nimi zagadnieniami. Zachęcamy tu także do tworzenia plików PDF z wykorzystaniem bibliotek niezależnych, o otwartych źródłach i wieloplatformowych. Rozdział 10. „Archiwa, obrazy i bazy danych” uczy rozwiązywania zadań związanych z obsługą archiwów zip i tworzeniem plików PNG w powiązaniu z zagadnieniami dotyczącymi świata rzeczywistego, takimi jak systemy Captcha i kody kreskowe oraz osadzanie i wykorzystywanie baz danych SQLite we własnych aplikacjach. 13 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Rozdział 11. „Kryptografia” omawia przede wszystkim użycie biblioteki Crypto++, służącej do szyfrowania danych i wykorzystywania podpisów cyfrowych. Zachęca się w nim także do wdrażania własnych narzędzi umożliwiających kodowanie i dekodowanie w standardzie base64. Rozdział 12. „Praca w sieci i usługi” to miejsce, w którym musisz wdrożyć własną aplikację klient-serwer komunikującą się za pomocą protokołu TCP/IP, a także korzystać z różnych usług REST, takich jak kursy wymiany bitcoinów lub funkcje API do tłumaczenia tekstu. Jak efektywnie korzystać z tej książki? Jak już wspomniano, do opanowania zagadnień wymienionych w tej książce potrzebna jest podstawowa znajomość języka C++ i biblioteki standardowej — tę wiedzę możesz jednak również zdobyć w trakcie studiowania niniejszej pozycji. W każdym razie ta książka nauczy Cię, jak rozwiązywać określone zadania, ale nie nauczy Cię samego języka i funkcji użytych w rozwiązaniach. Będziesz potrzebował kompilatora z obsługą C++ 17. Pełen zestaw wymaganych bibliotek, a także dostępne kompilatory, których można użyć, zostały zaprezentowane na liście sprzętu i oprogramowania znajdującej się w pakiecie kodu. W poniższych sekcjach znajdziesz szczegółowe instrukcje dotyczące pobierania i tworzenia kodu zawartego w tej książce. Pobieranie przykładowych plików z kodami Do książki dołączony jest zestaw plików, które pomogą Ci w poznaniu zagadnień w niej omawianych. Możesz je pobrać z serwera FTP wydawnictwa Helion pod adresem ftp://ftp.helion.pl/przyklady/nowcpp.zip. Pliki udostępnione są w formie archiwum, które po pobraniu należy rozpakować na swoim dysku. Tworzenie kodu Chociaż w całej książce jest używana duża liczba bibliotek zewnętrznych, działają one razem ze wszystkimi zawartymi tutaj rozwiązaniami na wszystkich platformach. Jednakże kod został opracowany i przetestowany w środowisku Visual Studio 2017 v15.6/7 w systemie operacyjnym Windows 10 oraz w środowisku Xcode 9.3 w systemie Mac OS 10.13.x. Jeśli pracujesz na komputerze Mac i korzystasz ze środowiska Xcode, pamiętaj, że w książce wykorzystano dwie funkcje, które nie są dostępne z zestawem narzędzi LLVM zawartym w Xcode. Są to biblioteki filesystem i std::optional. Zostały one jednak zaprojektowane na podstawie bibliotek Boost.Filesystem i Boost.Optional, dlatego wymienione standardowe biblioteki użyte w proponowanych rozwiązaniach można łatwo zamienić na biblioteki Boost. W rzeczywistości dołączony kod jest napisany w taki sposób, aby działał z każdą z wybranych opcji. Zdefiniowanie, których rodzajów bibliotek należy użyć, wykonuje się za pomocą kilku makr. Instrukcje dotyczące budowania kodu przy użyciu jednego z powyższych rozwiązań zostały podane poniżej, chociaż są one również dostępne w archiwum plików źródłowych. 14 ecb84badecb8c394873734f1e9bfb90f e Przedmowa Aby obsługiwać większość środowisk programistycznych i systemów kompilacji, których można używać na różnych platformach, kod został dostarczony razem ze skryptami CMake. Są one używane do generowania projektów lub tworzenia skryptów dla preferowanego zestawu narzędzi. Jeśli nie masz zainstalowanej aplikacji CMake w swoim komputerze, możesz ją pobrać ze strony https://cmake.org/. Poniżej znajdują się instrukcje umożliwiające użycie narzędzia CMake do wygenerowania skryptów Visual Studio i Xcode. Jeśli będzie to konieczne, w przypadku innych narzędzi zapoznaj się z dokumentacją CMake. Jak wygenerować projekty w środowisku Visual Studio 2017? Aby wygenerować projekty Visual Studio 2017 w celu tworzenia kodu na platformie x86, wykonaj następujące czynności: 1. Otwórz wiersz poleceń, przejdź do głównego katalogu kodu źródłowego, utwórz folder build, a następnie wejdź do niego. 2. Wykonaj następujące polecenie CMake: cmake -G "Visual Studio 15 2017" .. -DCMAKE_USE_WINSSL=ON -DCURL_WINDOWS_SSPI= ON -CURL_LIBRARY=libcurl -DCURL_INCLUDE_DIR=..\libs\curl\include -DBUILD_ TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL=OFF 3. Po wykonaniu polecenia gotowy projekt Visual Studio powinien być dostępny w pliku build/cppchallenger.sln. Jeśli zamiast tego chcesz kompilować programy na platformie x64, użyj generatora o nazwie „Visual Studio 15 2017 Win64”. Środowisko Visual Studio 2017 15.4 obsługuje zarówno bibliotekę filesystem (jako bibliotekę eksperymentalną), jak i bibliotekę std::optional. Jeśli korzystasz z poprzedniej wersji środowiska lub po prostu chcesz zastosować bibliotekę Boost, możesz po ich prawidłowym zainstalowaniu wygenerować projekty za pomocą następującego polecenia: cmake -G "Visual Studio 15 2017" .. -DCMAKE_USE_WINSSL=ON -DCURL_WINDOWS_SSPI=ON DCURL_LIBRARY=libcurl -DCURL_INCLUDE_DIR=..\libs\curl\include -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL=OFF -DBOOST_FILESYSTEM=ON -DBOOST_OPTIONAL=ON DBOOST_INCLUDE_DIR=<ścieżka_do_nagłówków> -DBOOST_LIB_DIR=<ścieżka_do_bibliotek> Upewnij się, że ścieżki do nagłówków i plików bibliotek statycznych nie zawierają na końcu ukośników odwrotnych (znaków \). Jak wygenerować projekty w środowisku Xcode? Kilka rozwiązań w ostatnim rozdziale wykorzystuje bibliotekę libcurl. W przypadku obsługi SSL biblioteka ta musi zostać połączona z biblioteką OpenSSL. Aby zainstalować bibliotekę OpenSSL, wykonaj następujące czynności: 1. Pobierz bibliotekę spod adresu https://www.openssl.org/. 2. Rozpakuj archiwum, a następnie w oknie wiersza poleceń przejdź do jego głównego katalogu. 3. Zbuduj i zainstaluj bibliotekę przy użyciu poniższych poleceń (wykonywanych w podanej kolejności): 15 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów ./Configure darwin64-x86_64-cc shared enable-ec_nistp_64_gcc_128 no-ssl2 nossl3 no-comp --openssldir=/usr/local/ssl/macos-x86_64 make depend sudo make install Do czasu, gdy biblioteki std::optional oraz filesystem staną się dostępne w kompilatorze Clang środowiska Xcode, powinieneś używać biblioteki Boost. Aby zainstalować i zbudować bibliotekę Boost, wykonaj poniższe działania: 1. Zainstaluj menedżer pakietów Homebrew ze strony https://brew.sh/. 2. Wykonaj następujące polecenie, aby automatycznie pobrać i zainstalować Boost: brew install boost 3. Biblioteka Boost po instalacji będzie dostępna w lokalizacji /usr/local/Cellar/boost/1.65.0. Aby w środowisku Xcode wygenerować projekty z dostępnych plików źródłowych, musisz wykonać poniższe czynności: 1. Otwórz wiersz poleceń, a następnie przejdź do głównego katalogu kodu źródłowego. Utwórz folder build i wejdź do niego. 2. Wykonaj następujące polecenie CMake: cmake -G Xcode .. -DOPENSSL_ROOT_DIR=/usr/local/bin -DOPENSSL_INCLUDE_DIR= /usr/local/include/ -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL= OFF -DBOOST_FILESYSTEM=ON -DBOOST_OPTIONAL=ON -DBOOST_INCLUDE_DIR=/usr/local/ Cellar/boost/1.65.0 -DBOOST_LIB_DIR=/usr/local/Cellar/boost/1.65.0/lib 3. Po wykonaniu polecenia gotowy projekt Xcode powinien być dostępny w pliku build/cppchallenger.xcodeproj. Wykorzystywane konwencje W tej książce wykorzystywanych jest wiele konwencji tekstowych. KodWTekście: oznacza fragmenty kodu w tekście oraz dane wprowadzane i wyświetlane w wierszu poleceń. Oto przykład: „Wykonaj polecenie brew install boost”. W taki sposób został zdefiniowany listing kodu: int main() { std::cout << "Witaj, świecie!\n"; } Gdy będziemy chcieli zwrócić Twoją uwagę na określony fragment listingu, odpowiednie wiersze lub elementy zostaną pogrubione: template<typename C, typename... Args> void push_back(C& c, Args&&... args) { (c.push_back(args), ...); } 16 ecb84badecb8c394873734f1e9bfb90f e Przedmowa Każda informacja wprowadzana lub wyświetlana w wierszu poleceń jest prezentowana w następujący sposób: $ mkdir build $ cd build Czcionka pogrubiona: wskazuje nowe pojęcie lub ważne słowo. Przykład: „W projekcie Visual Studio dodaj ścieżkę curl\include do listy dodatkowych katalogów plików nagłówkowych”. Kursywa: wykorzystywana w przypadku komunikatów, nazw plików i ścieżek, adresów internetowych oraz twitterowych, nazw bibliotek. Oto przykład: „Zamontuj pobrany plik obrazu WebStorm-10*.dmg w postaci kolejnego dysku w Twoim systemie operacyjnym”. Ta ramka oznacza ostrzeżenie, wskazówkę lub ważną uwagę. 17 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 18 ecb84badecb8c394873734f1e9bfb90f e 1 Zadania matematyczne Zadania 1. Suma liczb naturalnych podzielnych przez 3 lub 5 Napisz program, który oblicza sumę wszystkich liczb naturalnych podzielnych przez 3 lub 5 aż do podanej wartości granicznej wprowadzonej przez użytkownika. 2. Największy wspólny dzielnik Napisz program, który obliczy i wyświetli największy wspólny dzielnik dwóch dodatnich liczb całkowitych. 3. Najmniejsza wspólna wielokrotność Napisz program, który obliczy i wyświetli najmniejszą wspólną wielokrotność dla dwóch lub więcej dodatnich liczb całkowitych. 4. Największa liczba pierwsza mniejsza od podanej Napisz program, który obliczy i wyświetli największą liczbę pierwszą mniejszą od liczby podanej przez użytkownika, która powinna być dodatnią liczbą całkowitą. 5. Liczby pierwsze szóstkowe Napisz program, który wyświetli wszystkie liczby pierwsze szóstkowe aż do limitu wprowadzonego przez użytkownika. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 6. Liczby obfite Napisz program, który wyświetli wszystkie liczby obfite oraz ich obfitość aż do wartości wprowadzonej przez użytkownika. 7. Liczby zaprzyjaźnione Napisz program, który wyświetli listę wszystkich par liczb zaprzyjaźnionych mniejszych niż milion. 8. Liczby Armstronga Napisz program, który wypisze wszystkie liczby Armstronga zawierające trzy cyfry. 9. Czynniki pierwsze liczby Napisz program, który wyświetla czynniki pierwsze liczby wprowadzonej przez użytkownika. 10. Kod Graya Napisz program wyświetlający naturalne reprezentacje binarne, reprezentacje kodu Graya i dekodowane wartości kodu Graya dla wszystkich liczb 5-bitowych. 11. Przekształcanie liczb arabskich na rzymskie Napisz program, który biorąc pod uwagę liczbę wprowadzoną przez użytkownika, wyświetla jej odpowiednik w postaci liczby rzymskiej. 12. Najdłuższy ciąg Collatza Napisz program, który ustali i wyświetli, jaka liczba mniejsza od miliona stworzy najdłuższy ciąg Collatza oraz jaka będzie jego długość. 13. Wyznaczanie liczby Napisz program, który obliczy wartość π z dokładnością do dwóch cyfr dziesiętnych. 14. Sprawdzanie numerów ISBN Napisz program, który potwierdzi, że 10-cyfrowa wartość wprowadzona przez użytkownika reprezentuje poprawny identyfikator ISBN-10. 20 ecb84badecb8c394873734f1e9bfb90f e Rozdział 1. • Zadania matematyczne Rozwiązania 1. Suma liczb naturalnych podzielnych przez 3 lub 5 Rozwiązanie tego zadania polega na przetwarzaniu wszystkich liczb, począwszy od 3 (1 i 2 nie są podzielne przez 3, więc nie ma sensu ich sprawdzanie) aż do limitu wprowadzonego przez użytkownika. Zastosuj operację modulo, aby sprawdzić, czy reszta z dzielenia liczby przez 3 i 5 wynosi 0. Jednak sztuczka w przypadku wyższego limitu polega na użyciu dla sumy typu long long, a nie int lub long, co skutkowałoby przepełnieniem podczas sumowania do wartości 100 000: int main() { setlocale(LC_ALL, "polish"); unsigned int limit = 0; std::cout << "Ograniczenie górne:"; std::cin >> limit; unsigned long long sum = 0; for (unsigned int i = 3; i < limit; ++i) { if (i % 3 == 0 || i % 5 == 0) sum += i; } std::cout << "suma=" << sum << std::endl; } 2. Największy wspólny dzielnik Największy wspólny dzielnik (w skrócie NWD; ang. gcd) dwóch lub więcej niezerowych liczb całkowitych, znany również jako największy wspólny podzielnik, to największa dodatnia liczba całkowita dzieląca każdą z nich. Istnieje kilka sposobów obliczania NWD — skuteczną metodą jest algorytm Euklidesa. W przypadku dwóch liczb całkowitych algorytm ten ma taką postać: gcd(a,0) = a gcd(a,b) = gcd(b, a mod b) Powyższy wzór może zostać w bardzo prosty sposób zaimplementowany w języku C++ jako funkcja rekurencyjna: unsigned int gcd(unsigned int const a, unsigned int const b) { return b == 0 ? a : gcd(b, a % b); } 21 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Nierekurencyjna implementacja algorytmu Euklidesa przedstawia się następująco: unsigned int gcd(unsigned int a, unsigned int b) { while (b != 0) { unsigned int r = a % b; a = b; b = r; } return a; W języku C++ 17 istnieje funkcja constexpr, nazwana gcd() i zdefiniowana w pliku nagłówkowym <numeric>, która oblicza największy wspólny dzielnik dwóch liczb. 3. Najmniejsza wspólna wielokrotność Najmniejsza wspólna wielokrotność (NWW — ang. lcm) dwóch lub więcej niezerowych liczb całkowitych to najmniejsza dodatnia liczba całkowita, której dzielnikiem jest każda z nich. Możliwym sposobem obliczenia najmniejszej wspólnej wielokrotności jest zredukowanie problemu do obliczenia największego wspólnego dzielnika. W tym przypadku używany jest następujący wzór: lcm(a, b) = abs(a, b) / gcd(a, b) Funkcja obliczająca najmniejszą wspólną wielokrotność może wyglądać tak: int lcm(int const a, int const b) { int h = gcd(a, b); return h ? (a * (b / h)) : 0; } Aby obliczyć NWW dla więcej niż dwóch liczb całkowitych, możesz użyć algorytmu std::accumulate, zdefiniowanego w pliku nagłówkowym <numeric>: template<class InputIt> int lcmr(InputIt first, InputIt last) { return std::accumulate(first, last, 1, lcm); } W języku C++ 17 istnieje funkcja constexpr, nazwana lcm() i zdefiniowana w pliku nagłówkowym <numeric>, która oblicza najmniejszą wspólną wielokrotność dwóch liczb. 22 ecb84badecb8c394873734f1e9bfb90f e Rozdział 1. • Zadania matematyczne 4. Największa liczba pierwsza mniejsza od podanej Liczba pierwsza ma tylko dwa dzielniki: 1 i siebie samą. Aby znaleźć największą liczbę pierwszą mniejszą od podanej wartości, należy najpierw napisać funkcję, która sprawdza, czy dana liczba jest liczbą pierwszą, a następnie wywołać tę funkcję, zaczynając od wprowadzonej wartości. W dalszej kolejności trzeba się przemieszczać w kierunku jedynki aż do napotkania pierwszej liczby pierwszej. Istnieją różne algorytmy ustalania, czy liczba jest liczbą pierwszą. Typowa implementacja służąca do sprawdzania liczb pierwszych jest następująca: bool is_prime(int const num) { if (num <= 3) { return num > 1; } else if (num % 2 == 0 || num % 3 == 0) { return false; } else { for (int i = 5; i * i <= num; i += 6) { if (num % i == 0 || num % (i + 2) == 0) { return false; } } return true; } } Powyższa funkcja może zostać użyta w taki sposób: int main() { setlocale(LC_ALL, "polish"); int limit = 0; std::cout << "Ograniczenie górne:"; std::cin >> limit; for (int i = limit; i > 1; i--) { if (is_prime(i)) { std::cout << "Największa liczba pierwsza:" << i << std::endl; return 0; } } } 23 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 5. Liczby pierwsze szóstkowe Liczby pierwsze szóstkowe to dwie liczby pierwsze, które różnią się od siebie o wartość sześć (na przykład 5 i 11 lub 13 i 19). Istnieją również bliźniacze liczby pierwsze, które różnią się o wartość dwa, a także liczby pokrewne (lub stryjeczne) różniące się o wartość cztery. W poprzednim zadaniu zaimplementowaliśmy funkcję, która sprawdza, czy dana liczba całkowita jest liczbą pierwszą. Ta funkcja zostanie ponownie użyta w tym ćwiczeniu. Musisz sprawdzić, czy jeśli liczba n jest liczbą pierwszą, również liczba n + 6 jest liczbą pierwszą — jeżeli tak, wówczas wyświetlisz te dwie liczby w konsoli: int main() { setlocale(LC_ALL, "polish"); int limit = 0; std::cout << "Ograniczenie górne:"; std::cin >> limit; for (int n = 2; n <= limit; n++) { if (is_prime(n) && is_prime(n + 6)) { std::cout << n << "," << n + 6 << std::endl; } } } Dodatkowym ćwiczeniem mogłoby być obliczanie i wyświetlanie ciągów liczb pierwszych szóstkowych o długości trzy, cztery i pięć. 6. Liczby obfite Liczba obfita jest liczbą, dla której suma jej dzielników właściwych jest większa od niej samej. Dzielnikami właściwymi liczby są dodatnie czynniki pierwsze różniące się od niej. Wartość, o jaką suma dzielników właściwych przekracza liczbę, nazywa się obfitością. Na przykład liczba 12 ma dzielniki właściwe 1, 2, 3, 4 i 6. Ich suma wynosi 16, co czyni liczbę 12 obfitą. Jej obfitość wynosi 4 (czyli 16 – 12). Aby określić sumę dzielników właściwych, próbujemy wszystkie liczby od 2 do pierwiastka kwadratowego liczby (wszystkie czynniki pierwsze są mniejsze od tej wartości lub jej równe). Jeżeli bieżąca wartość (nazwijmy ją i) podzieli liczbę, wówczas i oraz num / i są dzielnikami. Jeśli jednak są one równe (na przykład jeżeli i = 3, a n = 9, wówczas i dzieli 9, lecz n / i = 3), dodajemy tylko i, ponieważ dzielniki właściwe mogą zostać dodane tylko raz. W przeciwnym razie dodajemy zarówno i, jak i num / i i kontynuujemy algorytm: int sum_proper_divisors(int const number) { int result = 1; 24 ecb84badecb8c394873734f1e9bfb90f e Rozdział 1. • Zadania matematyczne for (int i = 2; i <= std::sqrt(number); i++) { if (number%i == 0) { result += (i == (number / i)) ? i : (i + number / i); } } return result; } Wyświetlanie liczb obfitych jest proste — polega na wykonaniu iteracji aż do osiągnięcia określonej wartości granicznej, obliczeniu sumy dzielników właściwych i porównaniu jej z liczbą: void print_abundant(int const limit) { for (int number = 10; number <= limit; ++number) { auto sum = sum_proper_divisors(number); if (sum > number) { std::cout << number << ", obfitość=" << sum - number << std::endl; } } } int main() { setlocale(LC_ALL, "polish"); int limit = 0; std::cout << "Ograniczenie górne:"; std::cin >> limit; print_abundant(limit); } 7. Liczby zaprzyjaźnione Uważa się, że dwie liczby są zaprzyjaźnione, jeśli suma dzielników właściwych jednej liczby jest równa takiej samej sumie obliczonej dla drugiej z nich. Dzielniki właściwe danej liczby są jej dodatnimi czynnikami pierwszymi różnymi od niej. Liczby zaprzyjaźnione nie powinny być mylone z liczbami przyjacielskimi[JJ1]. Na przykład liczba 220 ma dzielniki właściwe 1, 2, 4, 5, 10, 11, 20, 22, 44, 55 i 110, których suma wynosi 284. Dzielniki właściwe liczby 284 to 1, 2, 4, 71 i 142; ich suma wynosi 220. Dlatego też liczby 220 i 284 są uznawane za zaprzyjaźnione. 25 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Rozwiązanie tego zadania polega na przetwarzaniu wszystkich liczb aż do określonego limitu. Dla każdej liczby oblicz sumę jej dzielników właściwych. Nazwijmy ją sum1. Powtórz proces i wyznacz sumę dzielników właściwych. Jeżeli wynik będzie równy liczbie pierwotnej, wówczas ona oraz sum1 utworzą liczby zaprzyjaźnione: void print_amicables(int const limit) { for (int number = 4; number < limit; ++number) { auto sum1 = sum_proper_divisors(number); if (sum1 < limit) { auto sum2 = sum_proper_divisors(sum1); if (sum2 == number && number != sum1) { std::cout << number << "," << sum1 << std::endl; } } } } W powyższym przykładzie sum_proper_divisors() jest funkcją utworzoną w celu rozwiązania zadania wyznaczania liczb obfitych. Powyższa funkcja wyświetla pary liczb dwukrotnie, na przykład 220,284 i 284,220. Zmodyfikuj tę implementację w taki sposób, aby wyświetlać tylko jedną parę. 8. Liczby Armstronga Liczba Armstronga (nazwana tak na cześć Michaela F. Armstronga), zwana również liczbą narcystyczną, to liczba, która jest sumą swoich cyfr podniesionych do potęgi równej ich liczbie. Przykładowo, najmniejsza liczba Armstronga to 153, która jest równa 13 + 53 + 33. Aby ustalić, czy liczba z trzema cyframi jest liczbą narcystyczną, musisz najpierw określić te cyfry, aby zsumować ich potęgi. Jednak wymaga to dzielenia i operacji modulo, które są kosztowne. Znacznie szybszym sposobem obliczenia jest poleganie na tym, że liczba jest sumą cyfr pomnożonych przez wartość 10 podniesioną do potęgi zależnej od położenia danej cyfry. Innymi słowy, dla liczb do wartości 1000 używamy wzoru a * 10 ^ 2 + b * 10 ^ 1 + c. Ponieważ musisz tylko ustalać liczby trzycyfrowe, oznacza to, że wartość a powinna się zaczynać od 1. Ten sposób obliczeń jest lepszy niż inne metody, ponieważ mnożenie jest szybsze niż dzielenie i operacja modulo. Implementacja funkcji wyglądałaby tak: void print_narcissistics() { for (int a = 1; a <= 9; a++) { for (int b = 0; b <= 9; b++) { 26 ecb84badecb8c394873734f1e9bfb90f e Rozdział 1. • Zadania matematyczne for (int c = 0; c <= 9; c++) { auto abc = a * 100 + b * 10 + c; auto arm = a * a * a + b * b * b + c * c * c; if (abc == arm) { std::cout << arm << std::endl; } } } } } Dodatkowym ćwiczeniem mogłoby być napisanie funkcji, która wyznacza liczby narcystyczne aż do podanego limitu, niezależnie od liczby jej cyfr. Taka funkcja działałaby wolniej, ponieważ najpierw musiałbyś określić zestaw cyfr, zapisać go w jakimś kontenerze, a następnie zsumować cyfry podniesione do odpowiedniej potęgi (równej liczbie cyfr). 9. Czynniki pierwsze liczby Czynnikami pierwszymi dodatniej liczby całkowitej są liczby pierwsze, które dokładnie dzielą tę liczbę całkowitą. Na przykład czynniki pierwsze liczby 8 to 2 · 2 · 2, a czynniki pierwsze liczby 42 to 2 · 3 · 7. Aby określić czynniki pierwsze, należy zastosować następujący algorytm: 1. Gdy liczba n jest podzielna przez 2, oznacza to, że wartość 2 jest jej czynnikiem pierwszym i musi zostać dodana do listy, natomiast n powinna stać się równa n / 2. Po wykonaniu tego kroku n będzie liczbą nieparzystą. 2. Rozpocznij szereg iteracji, zaczynając od wartości 3 aż do pierwiastka kwadratowego z liczby n. Jeżeli bieżąca wartość (nazwijmy ją i) dzieli liczbę n, wówczas jest jej czynnikiem pierwszym i musi zostać dodana do listy, natomiast n powinna stać się równa n / i. Gdy i nie dzieli już n, zwiększ jej wartość o 2 (aby uzyskać następną liczbę nieparzystą). 3. Gdy n będzie liczbą pierwszą większą niż 2, powyższe kroki nie spowodują, że stanie się ona równa 1. Jeśli więc pod koniec kroku 2. liczba n jest wciąż większa niż 2, oznacza to, że jest czynnikiem pierwszym. std::vector<unsigned long long> prime_factors(unsigned long long n) { std::vector<unsigned long long> factors; while (n % 2 == 0) { factors.push_back(2); n = n / 2; } for (unsigned long long i = 3; i <= std::sqrt(n); i += 2) { while (n%i == 0) { 27 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów factors.push_back(i); n = n / i; } } if (n > 2) factors.push_back(n); return factors; } int main() { setlocale(LC_ALL, "polish"); unsigned long long number = 0; std::cout << "liczba:"; std::cin >> number; auto factors = prime_factors(number); std::copy( std::begin(factors), std::end(factors), std::ostream_iterator<unsigned long long>(std::cout, " ")); } W ewentualnym następnym ćwiczeniu określ największy czynnik pierwszy dla liczby 600 851 475 143. 10. Kod Graya Kod Graya, znany również pod nazwą kodu refleksyjnego lub odzwierciedlonego binarnie, jest formą kodowania binarnego, w którym dwie kolejne liczby różnią się od siebie tylko jednym bitem. Aby przeprowadzić kodowanie do kodu Graya, musimy użyć następującego wzoru: if b[i-1] = 1 then g[i] = not b[i] else g[i] = b[i] Jest to równoznaczne następującemu zapisowi: g = b xor (wartość b jeden raz logicznie przesunięta w prawo) W celu zdekodowania kodu Graya używany jest poniższy wzór: b[0] = g[0] b[i] = g[i] xor b[i-1] Dla liczb całkowitych nieujemnych można go zapisać w języku C++ w postaci poniższego programu: 28 ecb84badecb8c394873734f1e9bfb90f e Rozdział 1. • Zadania matematyczne unsigned int gray_encode(unsigned int const num) { return num ^ (num >> 1); } unsigned int gray_decode(unsigned int gray) { for (unsigned int bit = 1U << 31; bit > 1; bit >>= 1) { if (gray & bit) gray ^= bit >> 1; } return gray; } Aby wyświetlić wszystkie 5-bitowe liczby całkowite, ich binarną reprezentację, zakodowany kod Graya i zdekodowaną wartość, możemy wykorzystać poniższy listing: std::string to_binary(unsigned int value, int const digits) { return std::bitset<32>(value).to_string().substr(32-digits, digits); } int main() { setlocale(LC_ALL, "polish"); std::cout << "Liczba\tWart. binarna\tKod Graya\tWart. zdekodowana\n"; std::cout << "------\t-------------\t----------\t-----------------\n"; for (unsigned int n = 0; n < 32; ++n) { auto encg = gray_encode(n); auto decg = gray_decode(encg); std::cout << n << "\t" << to_binary(n, 5) << "\t\t" << to_binary(encg, 5) << "\t\t" << decg << "\n"; } } 11. Przekształcanie liczb arabskich na rzymskie Liczby rzymskie w formie takiej, jaką znamy obecnie, używają siedmiu znaków: I = 1, V = 5, X = 10, L = 50, C = 100, D = 500 i M = 1000. System wykorzystuje dodawanie i odejmowanie w celu tworzenia symboli liczbowych. Symbole od 1 do 10 są takie: I, II, III, IV, V, VI, VII, VIII, IX i X. Rzymianie nie używali symbolu zera i w celu jego reprezentacji pisali słowo nulla. W tym systemie największe symbole znajdują się po lewej stronie, a najmniej znaczące — po prawej. Przykładowo, liczbą rzymską reprezentującą rok 1994 jest MCMXCIV. Jeśli nie znasz reguł dotyczących liczb rzymskich, poszukaj dodatkowych informacji w internecie. 29 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Aby zdefiniować liczbę rzymską dla danej liczby, użyj następującego algorytmu: 1. Weź pod uwagę każdy podstawowy symbol liczb rzymskich, zaczynając od największego (M), a kończąc na najmniejszym (I). 2. Jeśli wartość bieżąca jest większa niż wartość symbolu, dodaj symbol do liczby rzymskiej i odejmij jego wartość od bieżącej. 3. Powtarzaj proces, aż wartość bieżąca będzie równa zero. Jako przykładu użyjmy liczby 42: pierwszy rzymski symbol podstawowy mniejszy niż 42 to XL, który wynosi 40. Dodajemy go do docelowej liczby i otrzymujemy XL, a jednocześnie odejmujemy od bieżącej liczby, w wyniku czego uzyskujemy 2. Pierwszy rzymski symbol podstawowy mniejszy od 2 to I, czyli 1. Dodajemy I do liczby, w wyniku czego uzyskujemy XLI, a następnie odejmujemy 1 od bieżącej liczby, co daje nam 1. Dodajemy jeszcze jeden symbol I do liczby, która staje się równa XLII, i odejmujemy ponownie 1 od bieżącej liczby, osiągając 0, a zatem kończymy algorytm: std::string to_roman(unsigned int value) { std::vector<std::pair<unsigned int, char const*>> roman { { 1000, "M" },{ 900, "CM" }, { 500, "D" },{ 400, "CD" }, { 100, "C" },{ 90, "XC" }, { 50, "L" },{ 40, "XL" }, { 10, "X" },{ 9, "IX" }, { 5, "V" },{ 4, "IV" }, { 1, "I" }}; std::string result; for (auto const & kvp : roman) { while (value >= kvp.first) { result += kvp.second; value -= kvp.first; } } return result; } Powyższa funkcja może zostać użyta w następujący sposób: int main() { for(int i = 1; i <= 100; ++i) { std::cout << i << "\t" << to_roman(i) << std::endl; } int number = 0; std::cout << "liczba:"; std::cin >> number; std::cout << to_roman(number) << std::endl; } 30 ecb84badecb8c394873734f1e9bfb90f e Rozdział 1. • Zadania matematyczne 12. Najdłuższy ciąg Collatza Problem Collatza, znany również jako problem Ulama, problem 3x + 1, problem Kakutaniego lub problem syrakuzański, jest nieudowodnioną hipotezą, która stwierdza, że ciąg zdefiniowany w sposób, który opisano poniżej, zawsze uzyskuje wartość 1. Definicja ciągu jest następująca: rozpocznij od dowolnej całkowitej liczby dodatniej n i uzyskaj każdy nowy element za pomocą poprzedniego: jeśli poprzedni składnik będzie parzysty, następny powinien być równy jego połowie — w przeciwnym razie musi zostać zdefiniowany jako 3 razy większy od poprzedniego oraz dodatkowo zwiększony o 1. Zadanie, które musisz rozwiązać, polega na wygenerowaniu ciągów Collatza dla wszystkich dodatnich liczb całkowitych mniejszych od miliona, określeniu, który z nich jest najdłuższy, a następnie wyświetleniu jego długości oraz liczby początkowej, z której powstał. Chociaż w celu utworzenia ciągu dla każdej z liczb oraz wyznaczenia elementów pozwalających na osiągnięcie wartości 1 możemy zastosować metodę „na siłę” (ang. brute-force), szybszym rozwiązaniem będzie zapamiętanie długości wszystkich ciągów, które już zostały wygenerowane. Gdy bieżący element ciągu rozpoczynającego się od wartości n stanie się mniejszy od niej, oznacza to, że jest liczbą, dla której ciąg został już określony. Możemy więc po prostu pobrać jego zapamiętaną długość i dodać do bieżącej, aby określić długość ciągu rozpoczynającego się od n. Takie podejście ogranicza jednak maksymalną liczbę wyznaczanych ciągów Collatza, ponieważ w pewnym momencie zajętość pamięci podręcznej przekroczy ilość pamięci, którą system może przydzielić: std::pair<unsigned long long, long> longest_collatz( unsigned long long const limit) { long length = 0; unsigned long long number = 0; std::vector<int> cache(limit + 1, 0); for (unsigned long long i = 2; i <= limit; i++) { auto n = i; long steps = 0; while (n != 1 && n >= i) { if ((n % 2) == 0) n = n / 2; else n = n * 3 + 1; steps++; } cache[i] = steps + cache[n]; if (cache[i] > length) { length = cache[i]; number = i; } } return std::make_pair(number, length); } 31 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 13. Wyznaczanie liczby Właściwym rozwiązaniem zadania przybliżonego określenia wartości π jest użycie symulacji Monte Carlo. Jest to metoda wykorzystująca losowe próbki danych wejściowych do badania zachowania złożonych procesów lub systemów. Ta metoda ma wiele różnych zastosowań i jest wykorzystywana w rozmaitych obszarach, w tym w fizyce, inżynierii, informatyce, finansach czy biznesie. Aby zaimplementować rozwiązanie, wykorzystujemy następujące założenia: powierzchnia koła o średnicy d wynosi PI * d ^ 2 / 4. Pole kwadratu o długości boków równych d wynosi d ^ 2. Jeśli podzielimy te dwa wzory przez siebie, otrzymamy PI / 4. Gdy umieścimy koło wewnątrz kwadratu, a następnie wygenerujemy losowe liczby rozmieszczone w nim równomiernie, ich liczba w kole powinna być wprost proporcjonalna do powierzchni koła, a liczba wewnątrz kwadratu powinna być wprost proporcjonalna do powierzchni kwadratu. Oznacza to, że podzielenie całkowitej liczby trafień zawartych w kwadracie i kole powinno dać wynik PI / 4. Im więcej punktów zostanie wygenerowanych, tym dokładniejszy będzie rezultat. Do generowania liczb pseudolosowych użyjemy algorytmu Mersenne Twister i rozkładu jednostajnego ciągłego: template <typename E = std::mt19937, typename D = std::uniform_real_distribution<>> double compute_pi(E& engine, D& dist, int const samples = 1000000) { auto hit = 0; for (auto i = 0; i < samples; i++) { auto x = dist(engine); auto y = dist(engine); if (y <= std::sqrt(1 - std::pow(x, 2))) hit += 1; } return 4.0 * hit / samples; } int main() { std::random_device rd; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); auto eng = std::mt19937{ seq }; auto dist = std::uniform_real_distribution<>{ 0, 1 }; for (auto j = 0; j < 10; j++) std::cout << compute_pi(eng, dist) << std::endl; } 32 ecb84badecb8c394873734f1e9bfb90f e Rozdział 1. • Zadania matematyczne 14. Sprawdzanie numerów ISBN Międzynarodowy Znormalizowany Numer Książki (ISBN) to unikatowy numeryczny identyfikator książek. Obecnie używany jest format 13-cyfrowy. W przypadku naszego zadania należy jednak zweryfikować poprzedni format, w którym używano 10 cyfr. Ostatnia z 10 cyfr to suma kontrolna. Ta cyfra musi być wybrana w taki sposób, by suma wszystkich 10 cyfr, z których każda została pomnożona przez swoją wagę (liczbę całkowitą) zmniejszającą się od 10 do 1, była wielokrotnością 11. Przedstawiona poniżej funkcja validate_isbn_10 używa numeru ISBN w postaci ciągu znaków i zwraca wartość true, jeśli długość łańcucha wynosi 10, wszystkie jego elementy są cyframi, a ich suma pomnożona przez odpowiednią wagę (lub pozycję) jest wielokrotnością 11: bool validate_isbn_10(std::string_view isbn) { auto valid = false; if (isbn.size() == 10 && std::count_if(std::begin(isbn), std::end(isbn), isdigit) == 10) { auto w = 10; auto sum = std::accumulate( std::begin(isbn), std::end(isbn), 0, [&w](int const total, char const c) { return total + w-- * (c - '0'); }); valid = !(sum % 11); } return valid; } Twoim kolejnym ćwiczeniem mogłoby być ulepszenie powyższej funkcji w taki sposób, aby poprawnie weryfikowała numery ISBN-10, które zawierają łączniki (na przykład 3-16-148410-0). Możesz również napisać funkcję, która sprawdza poprawność numerów ISBN-13. 33 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 34 ecb84badecb8c394873734f1e9bfb90f e 2 Funkcje języka Zadania 15. Typ danych IPv4 Utwórz klasę reprezentującą adres IPv4. Zaimplementuj funkcje wymagane do wyświetlania takich adresów w konsoli lub wprowadzania ich w niej. Użytkownik powinien mieć możliwość podawania wartości w formacie z kropkami, na przykład 127.0.0.1 lub 168.192.0.100. Jest to również format, w jakim adresy IPv4 powinny zostać przesłane do strumienia wyjściowego. 16. Wyliczanie zakresu adresów IPv4 Napisz program, który pozwala użytkownikowi wprowadzić dwa adresy IPv4 reprezentujące zakres, a następnie wyświetla listę wszystkich zawartych w nim adresów. Rozszerz strukturę zdefiniowaną dla poprzedniego zadania, aby zaimplementować pożądaną funkcjonalność. 17. Utworzenie dwuwymiarowej tablicy z podstawowymi operacjami Utwórz szablon klasy reprezentujący dwuwymiarowy kontener tablicy z metodami dostępu do elementu (at() i data()), a także zdefiniuj kwerendę dotyczącą pojemności, iteratory, jak również metody wypełniania tablicy i zamiany jej elementów. Powinno być możliwe przenoszenie obiektów utworzonego typu. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 18. Funkcja wyznaczająca minimum dla dowolnej liczby argumentów Napisz szablon funkcji, która może przyjmować dowolną liczbę argumentów i zwracać minimalną wartość spośród nich wszystkich, używając operatora < dla porównania. Następnie stwórz wariant tego szablonu, który to wariant można sparametryzować za pomocą funkcji porównania binarnego użytej zamiast operatora <. 19. Dodawanie zakresu wartości do kontenera Utwórz funkcję ogólnego przeznaczenia, która może umieścić dowolną liczbę elementów na końcu kontenera zawierającego metodę push_back (T&& value). 20. Dowolny, wszystkie lub żaden argument w kontenerze Napisz zestaw funkcji ogólnego przeznaczenia, które umożliwiają sprawdzenie, czy dowolny argument, wszystkie argumenty występują w danym kontenerze, czy też żaden z podanych argumentów w nim nie występuje. Przykładowy kod z użyciem tych funkcji powinien mieć taką postać: std::vector<int> v{ 1, 2, 3, 4, 5, 6 }; assert(contains_any(v, 0, 3, 30)); std::array<int, 6> a{ { 1, 2, 3, 4, 5, 6 } }; assert(contains_all(a, 1, 3, 5, 6)); std::list<int> l{ 1, 2, 3, 4, 5, 6 }; assert(!contains_none(l, 0, 6)); 21. Klasa opakowująca dla uchwytu systemowego Weź pod uwagę uchwyt systemu operacyjnego, taki jak uchwyt pliku. Napisz interfejs, który zajmuje się rezerwowaniem i zwalnianiem uchwytu, a także innymi operacjami, takimi jak sprawdzanie ważności uchwytu i przenoszenie własności uchwytu z jednego obiektu do drugiego. 22. Wyświetlanie różnych skal temperatur Utwórz niewielką bibliotekę, która umożliwia wyrażanie temperatur w trzech najczęściej używanych skalach: Celsjusza, Fahrenheita i Kelvina, a także konwersję między nimi. Biblioteka ta musi umożliwiać wprowadzanie wartości temperatury dla każdej ze skal, na przykład w postaci 36.5_deg dla stopni Celsjusza, 97.7_f dla stopni Fahrenheita i 309.65_K dla skali Kelvina. Powinny być także dostępne operacje, które można realizować przy użyciu tych wartości, jak również powinny być możliwe konwersje między skalami. 36 ecb84badecb8c394873734f1e9bfb90f e Rozdział 2. • Funkcje języka Rozwiązania 15. Typ danych IPv4 Rozwiązanie zadania polega na utworzeniu klasy reprezentującej adres IPv4. Taki adres jest wartością 32-bitową, zwykle reprezentowaną w formacie dziesiętnym z kropkami, na przykład 168.192.0.100; każda jego część jest wartością 8-bitową z zakresu od 0 do 255. W celu uproszczenia reprezentacji i obsługi danych do przechowywania wartości adresu możemy użyć czterech zmiennych typu unsigned char lub unsigned long. Aby mieć możliwość wczytywania wartości adresu bezpośrednio z konsoli (lub dowolnego innego strumienia wejściowego), a także wyświetlania go w niej (lub dowolnym innym strumieniu wyjściowym), musimy przeciążyć operatory >> i <<. Na poniższym listingu przedstawiono minimalną implementację, która może realizować żądaną funkcjonalność: class ipv4 { std::array<unsigned char, 4> data; public: constexpr ipv4() : data{ {0} } {} constexpr ipv4(unsigned char const a, unsigned char const b, unsigned char const c, unsigned char const d): data{{a,b,c,d}} {} explicit constexpr ipv4(unsigned long a) : data{ { static_cast<unsigned char>((a >> 24) & 0xFF), static_cast<unsigned char>((a >> 16) & 0xFF), static_cast<unsigned char>((a >> 8) & 0xFF), static_cast<unsigned char>(a & 0xFF) } } {} ipv4(ipv4 const & other) noexcept : data(other.data) {} ipv4& operator=(ipv4 const & other) noexcept { data = other.data; return *this; } std::string to_string() const { std::stringstream sstr; sstr << *this; return sstr.str(); } constexpr unsigned long to_ulong() const noexcept { return (static_cast<unsigned long>(data[0]) << 24) | (static_cast<unsigned long>(data[1]) << 16) | (static_cast<unsigned long>(data[2]) << 8) | static_cast<unsigned long>(data[3]); } friend std::ostream& operator<<(std::ostream& os, const ipv4& a) 37 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów { os << static_cast<int>(a.data[0]) << '.' << static_cast<int>(a.data[1]) << '.' << static_cast<int>(a.data[2]) << '.' << static_cast<int>(a.data[3]); return os; } friend std::istream& operator>>(std::istream& is, ipv4& a) { char d1, d2, d3; int b1, b2, b3, b4; is >> b1 >> d1 >> b2 >> d2 >> b3 >> d3 >> b4; if (d1 == '.' && d2 == '.' && d3 == '.') a = ipv4(b1, b2, b3, b4); else is.setstate(std::ios_base::failbit); return is; } }; Klasa IPv4 może zostać użyta w następujący sposób: int main() { ipv4 address(168, 192, 0, 1); std::cout << address << std::endl; ipv4 ip; std::cout << ip << std::endl; std::cin >> ip; if(!std::cin.fail()) std::cout << ip << std::endl; } 16. Wyliczanie zakresu adresów IPv4 W celu wyliczenia adresów IPv4 z danego zakresu powinno być najpierw możliwe porównanie wartości IPv4. Dlatego musimy zdefiniować działanie przynajmniej operatora <, jednakże poniższy listing zawiera implementację wszystkich operatorów porównania: ==, !=, <, >, <= oraz >=. Ponadto w celu zwiększenia wartości IPv4 zaimplementowano operatory ++ przedrostkowe i przyrostkowe. Poniższy kod jest rozszerzeniem klasy IPv4 z poprzedniego zadania: ipv4& operator++() { *this = ipv4(1 + to_ulong()); return *this; } ipv4& operator++(int) { ipv4 result(*this); ++(*this); 38 ecb84badecb8c394873734f1e9bfb90f e Rozdział 2. • Funkcje języka return *this; } friend bool operator==(ipv4 const & a1, ipv4 const & a2) noexcept { return a1.data == a2.data; } friend bool operator!=(ipv4 const & a1, ipv4 const & a2) noexcept { return !(a1 == a2); } friend bool operator<(ipv4 const & a1, ipv4 const & a2) noexcept { return a1.to_ulong() < a2.to_ulong(); } friend bool operator>(ipv4 const & a1, ipv4 const & a2) noexcept { return a2 < a1; } friend bool operator<=(ipv4 const & a1, ipv4 const & a2) noexcept { return !(a1 > a2); } friend bool operator>=(ipv4 const & a1, ipv4 const & a2) noexcept { return !(a1 < a2); } Mając powyższe zmiany wprowadzone w klasie ipv4 pochodzącej z poprzedniego zadania, możemy napisać następujący program: int main() { setlocale(LC_ALL, "polish"); std::cout << "zakres wejściowy: "; ipv4 a1, a2; std::cin >> a1 >> a2; if (a2 > a1) { for (ipv4 a = a1; a <= a2; a++) { std::cout << a << std::endl; } } else { std::cerr << "niepoprawny zakres!" << std::endl; } } 39 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 17. Utworzenie dwuwymiarowej tablicy z podstawowymi operacjami Zanim zastanowimy się, jak zdefiniować taką strukturę, rozważmy kilka przypadków testowych. W poniższym fragmencie kodu zaprezentowano wszystkie wymagane funkcjonalności: int main() { // dostęp do elementu array2d<int, 2, 3> a {1, 2, 3, 4, 5, 6}; for (size_t i = 0; i < a.size(1); ++i) for (size_t j = 0; j < a.size(2); ++j) a(i, j) *= 2; // przetwarzanie elementów std::copy(std::begin(a), std::end(a), std::ostream_iterator<int>(std::cout, " ")); // wypełnianie elementów array2d<int, 2, 3> b; b.fill(1); // zamiana elementów a.swap(b); // przenoszenie elementów array2d<int, 2, 3> c(std::move(b)); } Zwróć uwagę, że w przypadku dostępu do elementu używamy operatora () (na przykład a(i, j)), a nie operatora [] (na przykład a[i][j]), ponieważ tylko ten pierwszy może przyjmować wiele argumentów (po jednym dla indeksu każdego wymiaru). Ten drugi może mieć tylko jeden argument, więc aby umożliwić używanie wyrażeń takich jak a[i][j], musimy zwrócić typ pośredni (taki, który w zasadzie reprezentuje wiersz), co z kolei powoduje przeciążenie operatora [] w celu zwrócenia pojedynczego elementu. Istnieją już standardowe kontenery przechowujące ciągi elementów o stałych lub zmiennych długościach. Klasa definiująca tablicę dwuwymiarową powinna być po prostu interfejsem do takiego kontenera. Podczas wyboru między typami danych std::array i std::vector powinniśmy wziąć pod uwagę dwa zagadnienia: Klasa array2d powinna zawierać semantykę zapewniającą przenoszenie obiektów. Powinna istnieć możliwość inicjalizowania obiektu tej klasy przy użyciu listy. Kontener std::array zapewnia przenoszenie tylko wtedy, gdy przechowywane w nim elementy można przenosić i przypisywać. Z drugiej strony nie można utworzyć obiektu przy użyciu inicjatora listy std::initializer_list, dlatego też bardziej realną opcją wyboru staje się std::vector. 40 ecb84badecb8c394873734f1e9bfb90f e Rozdział 2. • Funkcje języka Wewnętrznie interfejs kontenera może przechowywać swoje dane albo w wektorze wektorów (każdy wiersz jest wektorem vector<T> z elementami C, a tablica dwuwymiarowa ma R takich elementów przechowywanych w wektorze vector<vector<T>>), albo w pojedynczym wektorze R×C elementów typu T. W tym drugim przypadku element dostępny w wierszu i oraz kolumnie j można znaleźć przy użyciu indeksu i * C + j. Takie podejście pozwala na zużycie mniejszej ilości pamięci, umożliwia przechowywanie wszystkich danych w jednym ciągłym obszarze, a także jest łatwiejsze do wdrożenia. Z tych powodów jest to preferowane rozwiązanie. Możliwa implementacja klasy tablicy dwuwymiarowej z żądaną funkcjonalnością została zaprezentowana poniżej: template <class T, size_t R, class array2d { typedef T typedef value_type* typedef value_type const* size_t C> value_type; iterator; const_iterator; std::vector<T> arr; public: array2d() :arr(R*C) {} explicit array2d(std::initializer_list<T> l):arr(l) {} constexpr T* data() noexcept { return arr.data(); } constexpr T const * data() const noexcept { return arr.data(); } constexpr T& at(size_t const r, size_t const c) { return arr.at(r*C + c); } constexpr T const & at(size_t const r, size_t const c) const { return arr.at(r*C + c); } constexpr T& operator() (size_t const r, size_t const c) { return arr[r*C + c]; } constexpr T const & operator() (size_t const r, size_t const c) const { return arr[r*C + c]; } constexpr bool empty() const noexcept { return R == 0 || C == 0; } constexpr size_t size(int const rank) const { if (rank == 1) return R; 41 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów else if (rank == 2) return C; throw std::out_of_range("Pozycja poza zakresem!"); } void fill(T const & value) { std::fill(std::begin(arr), std::end(arr), value); } void swap(array2d & other) noexcept { arr.swap(other.arr); } const_iterator begin() const { return arr.data(); } const_iterator end() const { return arr.data() + arr.size(); } iterator begin() { return arr.data(); } iterator end() { return arr.data() + arr.size(); } }; template <class T, size_t R, size_t C> void print_array2d(array2d<T, R, C> const & arr) { for (int i = 0; i < R; ++i) { for (int j = 0; j < C; ++j) { std::cout << arr.at(i, j) << ' '; } std::cout << std::endl; } } 18. Funkcja wyznaczająca minimum dla dowolnej liczby argumentów Możliwe jest utworzenie szablonów funkcji, które mogą przyjmować zmienną liczbę argumentów przy użyciu szablonów funkcji wariadycznej. W tym celu musimy zaimplementować rekurencję w czasie kompilacji (rekurencja ta w rzeczywistości wywołuje tylko zestaw przeciążonych funkcji). W poniższym fragmencie kodu pokazano, w jaki sposób można zaimplementować żądaną funkcję: template <typename T> T minimum(T const a, T const b) { return a < b ? a : b; } template <typename T1, typename... T> T1 minimum(T1 a, T... args) { return minimum(a, minimum(args...)); 42 ecb84badecb8c394873734f1e9bfb90f e Rozdział 2. • Funkcje języka } int main() { auto x = minimum(5, 4, 2, 3); } Aby móc korzystać z funkcji porównania binarnego dostarczanej przez użytkownika, musimy utworzyć inny szablon funkcji. Funkcja porównania musi być pierwszym argumentem, ponieważ nie może podążać za pakietem parametrów funkcji. Z drugiej strony nie można utworzyć przeciążenia poprzedniej funkcji wyznaczającej minimum, lecz trzeba utworzyć funkcję o innej nazwie. Wynika to stąd, że kompilator nie będzie w stanie rozróżnić list parametrów szablonu <typename T1, typename... T> i <class Compare, typename T1, typename... T>. Zmiany są niewielkie i powinny być łatwe do prześledzenia w poniższym fragmencie kodu: template <class Compare, typename T> T minimumc(Compare comp, T const a, T const b) { return comp(a, b) ? a : b; } template <class Compare, typename T1, typename... T> T1 minimumc(Compare comp, T1 a, T... args) { return minimumc(comp, a, minimumc(comp, args...)); } int main() { auto y = minimumc(std::less<>(), 3, 2, 1, 0); } 19. Dodawanie zakresu wartości do kontenera Utworzenie funkcji z dowolną liczbą argumentów jest możliwe przy użyciu szablonów funkcji wariadycznej. Pierwszym jej parametrem powinien być kontener, po którym następuje zmienna liczba argumentów reprezentujących wartości, które zostaną umieszczone na końcu tego kontenera. Jednak napisanie takiego szablonu funkcji można znacznie uprościć, używając wyrażeń fold. Taką implementację pokazano poniżej: template<typename C, typename... Args> void push_back(C& c, Args&&... args) { (c.push_back(args), ...); } Oto przykłady użycia tego szablonu funkcji wykorzystującego różne typy kontenerów: int main() { std::vector<int> v; push_back(v, 1, 2, 3, 4); 43 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów std::copy(std::begin(v), std::end(v), std::ostream_iterator<int>(std::cout, " ")); std::list<int> l; push_back(l, 1, 2, 3, 4); std::copy(std::begin(l), std::end(l), std::ostream_iterator<int>(std::cout, " ")); } 20. Dowolny, wszystkie lub żaden argument w kontenerze Konieczność sprawdzania istnienia zmiennej liczby argumentów lub określania ich braku sugeruje, że powinniśmy utworzyć szablony funkcji wariadycznej. Jednak funkcje te wymagają funkcji pomocniczej, uniwersalnej, która sprawdza, czy element znajduje się w kontenerze, i zwraca wartość bool oznaczającą powodzenie lub niepowodzenie. Ponieważ wszystkie te funkcje (o przykładowych nazwach include_all, contains_any i contains_none) stosują operatory logiczne w wynikach zwracanych przez funkcję pomocniczą, to w celu uproszczenia kodu moglibyśmy użyć wyrażeń fold. Po rozwinięciu wyrażenia fold staje się aktywne wartościowanie leniwe, co oznacza, że wyznaczamy tylko te elementy, które prowadzą do ostatecznego wyniku. Jeśli więc szukamy wartości 1, 2 i 3 i brakuje nam 2, funkcja powróci po sprawdzeniu w kontenerze wartości 2 i nie będzie sprawdzać wartości 3: template<class C, class T> bool contains(C const & c, T const & value) { return std::end(c) != std::find(std::begin(c), std::end(c), value); } template<class C, class... T> bool contains_any(C const & c, T &&... value) { return (... || contains(c, value)); } template<class C, class... T> bool contains_all(C const & c, T &&... value) { return (... && contains(c, value)); } template<class C, class... T> bool contains_none(C const & c, T &&... value) { return !contains_any(c, std::forward<T>(value)...); } 44 ecb84badecb8c394873734f1e9bfb90f e Rozdział 2. • Funkcje języka 21. Klasa opakowująca dla uchwytu systemowego Uchwyty systemowe są metodą odwoływania się do zasobów systemowych. Ponieważ wszystkie systemy operacyjne były przynajmniej początkowo pisane w języku C, tworzenie i zwalnianie uchwytów odbywa się za pośrednictwem dedykowanych funkcji systemowych. Zwiększa to ryzyko wycieku zasobów z powodu błędnego zwalniania uchwytu, na przykład w przypadku pojawienia się wyjątku. W poniższym fragmencie kodu, specyficznym dla systemu Windows, można zobaczyć funkcję, w której plik jest otwierany, odczytywany i ostatecznie zamykany. Istnieje jednak kilka problemów: w jednym przypadku programista zapomniał zamknąć uchwyt przed opuszczeniem funkcji, w innym zaś funkcja rzucająca wyjątek jest wywoływana, zanim uchwyt zostanie właściwie zamknięty, bez przechwycenia tego wyjątku. Jednak ponieważ ta funkcja rzuca wyjątek, kod porządkujący nigdy nie zostanie wykonany: void bad_handle_example() { bool condition1 = false; bool condition2 = true; HANDLE handle = CreateFile(L"sample.txt", GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) return; if (condition1) { CloseHandle(handle); return; } std::vector<char> buffer(1024); unsigned long bytesRead = 0; ReadFile(handle, buffer.data(), buffer.size(), &bytesRead, nullptr); if (condition2) { // oj, zapomnieliśmy zamknąć uchwyt return; } // rzucenie wyjątku; kolejny wiersz nie zostanie wykonany function_that_throws(); CloseHandle(handle); } 45 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Klasa opakowująca w języku C++ może zapewnić prawidłowe usuwanie uchwytu, gdy obiekt opakowany wykracza poza zakres i jest niszczony (niezależnie od tego, czy dzieje się to za pośrednictwem normalnej ścieżki wykonywania, czy w wyniku wyjątku). Właściwa implementacja powinna uwzględniać różne typy uchwytów, z zakresem wartości oznaczającym uchwyt niepoprawny (na przykład 0, null lub –1). Przedstawiona poniżej implementacja zapewnia następujące funkcjonalności: jawne pozyskiwanie i automatyczne zwalnianie uchwytu po zniszczeniu obiektu; semantykę przenoszenia, aby umożliwiać przenoszenie własności uchwytu; operatory porównania, aby sprawdzać, czy dwa obiekty odnoszą się do tego samego uchwytu; dodatkowe operacje, takie jak zamiana i resetowanie. Implementacja przedstawiona w tej książce jest zmodyfikowaną wersją klasy obsługi uchwytów zdefiniowanej przez Kenny’ego Kerra, opublikowanej w artykule Windows with C++ - C++ and the Windows API (MSDN Magazine, lipiec 2011, https://msdn.microsoft.com/en-us/magazine/hh288076.aspx). Chociaż pokazane tutaj cechy uchwytów odnoszą się do systemu Windows, utworzenie kodu przeznaczonego dla innych platform powinno być dość proste. template <typename Traits> class unique_handle { using pointer = typename Traits::pointer; pointer m_value; public: unique_handle(unique_handle const &) = delete; unique_handle& operator=(unique_handle const &) = delete; explicit unique_handle(pointer value = Traits::invalid()) noexcept :m_value{ value } {} unique_handle(unique_handle && other) noexcept : m_value{ other.release() } {} unique_handle& operator=(unique_handle && other) noexcept { if (this != &other) reset(other.release()); return *this; } ~unique_handle() noexcept { Traits::close(m_value); } explicit operator bool() const noexcept { 46 ecb84badecb8c394873734f1e9bfb90f e Rozdział 2. • Funkcje języka return m_value != Traits::invalid(); } pointer get() const noexcept { return m_value; } pointer release() noexcept { auto value = m_value; m_value = Traits::invalid(); return value; } bool reset(pointer value = Traits::invalid()) noexcept { if (m_value != value) { Traits::close(m_value); m_value = value; } return static_cast<bool>(*this); } void swap(unique_handle<Traits> & other) noexcept { std::swap(m_value, other.m_value); } }; template <typename Traits> void swap(unique_handle<Traits> & left, unique_handle<Traits> & right) noexcept { left.swap(right); } template <typename Traits> bool operator==(unique_handle<Traits> const & left, unique_handle<Traits> const & right) noexcept { return left.get() == right.get(); } template <typename Traits> bool operator!=(unique_handle<Traits> const & left, unique_handle<Traits> const & right) noexcept { return left.get() != right.get(); } struct null_handle_traits { using pointer = HANDLE; static pointer invalid() noexcept { return nullptr; } static void close(pointer value) noexcept 47 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów { CloseHandle(value); } }; struct invalid_handle_traits { using pointer = HANDLE; static pointer invalid() noexcept { return INVALID_HANDLE_VALUE; } static void close(pointer value) noexcept { CloseHandle(value); } }; using null_handle = unique_handle<null_handle_traits>; using invalid_handle = unique_handle<invalid_handle_traits>; Mając już zdefiniowaną powyższą klasę, możemy uprościć poprzedni przykład i uniknąć wszystkich problemów związanych z uchwytami, które nie są właściwie zamknięte z powodu wystąpienia wyjątków, nie są prawidłowo obsługiwane, lub uniknąć problemów, które po prostu wynikają stąd, że programiści zapominają zwolnić zasoby, gdy nie są one już potrzebne. Ten kod jest zarówno prostszy, jak i bardziej niezawodny: void good_handle_example() { bool condition1 = false; bool condition2 = true; invalid_handle handle{ CreateFile(L"sample.txt", GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) }; if (!handle) return; if (condition1) return; std::vector<char> buffer(1024); unsigned long bytesRead = 0; ReadFile(handle.get(), buffer.data(), buffer.size(), &bytesRead, nullptr); if (condition2) return; function_that_throws(); } 48 ecb84badecb8c394873734f1e9bfb90f e Rozdział 2. • Funkcje języka 22. Wyświetlanie różnych skal temperatur Aby rozwiązać zadanie, musimy zapewnić implementację dla kilku typów, operatorów i funkcji: typu wyliczeniowego o nazwie scale dla obsługiwanych skal temperatur; szablonu klasy nazwanego quantity, reprezentującego wartość temperatury, którego parametrem jest skala; operatorów porównania ==, !=, <, >, <= oraz >= porównujących dwie wielkości w tym samym czasie; operatorów arytmetycznych + i - dodających i odejmujących wartości tego samego typu; dodatkowo moglibyśmy zaimplementować operatory członkowskie += i -=; szablonu funkcji do przeliczania temperatur z jednej skali na inną, nazwanego temperature_cast; ta funkcja nie wykonuje bezpośrednio konwersji, ale wykorzystuje do tego cechy typu; operatorów literalnych ""_deg, ""_f oraz ""_k do tworzenia zdefiniowanych przez użytkownika literałów temperatury. W celu zapewnienia zwięzłości poniższy listing zawiera tylko kod, który obsługuje temperatury Celsjusza i Fahrenheita. Następnym Twoim ćwiczeniem mogłoby być rozszerzenie tego kodu o obsługę skali Kelvina. Program dołączony do książki zawiera pełną implementację wszystkich trzech wymaganych skal. Funkcja are_equal() jest funkcją pomocniczą służącą do porównywania wartości zmiennoprzecinkowych: bool are_equal(double const d1, double const d2, double const epsilon = 0.001) { return std::fabs(d1 - d2) < epsilon; } Typ wyliczeniowy możliwych skal temperatur i klasa reprezentująca wartość temperatury zostały zdefiniowane w następujący sposób: namespace temperature { enum class scale { celsius, fahrenheit, kelvin }; template <scale S> class quantity { const double amount; public: constexpr explicit quantity(double const a) : amount(a) {} explicit operator double() const { return amount; } }; } 49 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Operatory porównania dla klasy quantity<S> zostały zaprezentowane poniżej: namespace temperature { template <scale S> inline bool operator==(quantity<S> const & lhs, quantity<S> const & rhs) { return are_equal(static_cast<double>(lhs), static_cast<double>(rhs)); } template <scale S> inline bool operator!=(quantity<S> const & lhs, quantity<S> const & rhs) { return !(lhs == rhs); } template <scale S> inline bool operator< (quantity<S> const & lhs, quantity<S> const & rhs) { return static_cast<double>(lhs) < static_cast<double>(rhs); } template <scale S> inline bool operator> (quantity<S> const & lhs, quantity<S> const & rhs) { return rhs < lhs; } template <scale S> inline bool operator<=(quantity<S> const & lhs, quantity<S> const & rhs) { return !(lhs > rhs); } template <scale S> inline bool operator>=(quantity<S> const & lhs, quantity<S> const & rhs) { return !(lhs < rhs); } template <scale S> constexpr quantity<S> operator+(quantity<S> const quantity<S> const { return quantity<S>(static_cast<double>(q1) + static_cast<double>(q2)); } template <scale S> constexpr quantity<S> operator-(quantity<S> const quantity<S> const { return quantity<S>(static_cast<double>(q1) static_cast<double>(q2)); } } 50 ecb84badecb8c394873734f1e9bfb90f &q1, &q2) &q1, &q2) e Rozdział 2. • Funkcje języka Aby wykonać konwersję wartości temperatury w różnych skalach, zdefiniujemy szablon funkcji o nazwie temperature_cast(), który wykorzystuje kilka cech typu w celu wykonania faktycznej operacji. Odpowiedni listing został zaprezentowany poniżej, chociaż nie pokazano na nim wszystkich cech typu; pozostałe można znaleźć w kodzie źródłowym towarzyszącym książce: namespace temperature { template <scale S, scale R> struct conversion_traits { static double convert(double const value) = delete; }; template <> struct conversion_traits<scale::celsius, scale::fahrenheit> { static double convert(double const value) { return (value * 9) / 5 + 32; } }; template <> struct conversion_traits<scale::fahrenheit, scale::celsius> { static double convert(double const value) { return (value - 32) * 5 / 9; } }; template <scale R, scale S> constexpr quantity<R> temperature_cast(quantity<S> const q) { return quantity<R>(conversion_traits<S, R>::convert( static_cast<double>(q))); } } Operatory literalne służące do tworzenia wartości temperatury zostały pokazane w poniższym fragmencie kodu. Operatory te są zdefiniowane w osobnej przestrzeni nazw o nazwie temperature_scale_literals, co jest dobrą praktyką w celu zminimalizowania ryzyka kolizji nazw z innymi operatorami literalnymi: namespace temperature { namespace temperature_scale_literals { constexpr quantity<scale::celsius> operator "" _deg( long double const amount) { return quantity<scale::celsius> {static_cast<double>(amount)}; } 51 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów constexpr quantity<scale::fahrenheit> operator "" _f( long double const amount) { return quantity<scale::fahrenheit> {static_cast<double>(amount)}; } } } W poniższym przykładzie pokazano, w jaki sposób można zdefiniować dwie wartości temperatury, jedną w stopniach Celsjusza, a drugą w stopniach Fahrenheita, a następnie wykonać konwersje między tymi dwiema wartościami: int main() { using namespace temperature; using namespace temperature_scale_literals; auto t1{ 36.5_deg }; auto t2{ 79.0_f }; auto tf = temperature_cast<scale::fahrenheit>(t1); auto tc = temperature_cast<scale::celsius>(tf); assert(t1 == tc); } 52 ecb84badecb8c394873734f1e9bfb90f e 3 Łańcuchy i wyrażenia regularne Zadania 23. Zamiana typu binarnego na łańcuch Napisz funkcję, która biorąc pod uwagę zestaw 8-bitowych liczb całkowitych (taki jak tablica lub wektor), zwraca ciąg znaków zawierający szesnastkową reprezentację danych wejściowych. Funkcja powinna być w stanie wyświetlać zarówno wielkie, jak i małe litery. Oto kilka przykładów danych wejściowych i wyjściowych: wejście: { 0xBA, 0xAD, 0xF0, 0x0D }, wyjście: "BAADF00D" lub "baadf00d" wejście: { 1,2,3,4,5,6 }, wyjście: "010203040506" 24. Zamiana typu łańcuchowego na binarny Napisz funkcję, która wykorzystując jako argument wejściowy łańcuch zawierający cyfry szesnastkowe, zwraca wektor 8-bitowych liczb całkowitych będących numeryczną reprezentacją zawartości ciągu. Oto przykłady: wejście: "BAADF00D" lub "baadf00d", wyjście: {0xBA, 0xAD, 0xF0, 0x0D} wejście "010203040506", wyjście: {1, 2, 3, 4, 5, 6} ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 25. Wielkie litery w tytule artykułu Utwórz funkcję przekształcającą tekst wejściowy na wersję, w której każde słowo zaczyna się od wielkiej litery, a pozostałe litery są małe. Na przykład tekst "nowoczesne wyzwania języka C++" powinien zostać przekształcony na "Nowoczesne Wyzwania Języka C++". 26. Łączenie łańcuchów oddzielanych separatorem Napisz funkcję, która za pomocą podanej listy łańcuchów i separatora tworzy nowy łańcuch, łącząc wszystkie dane wejściowe przy użyciu określonego separatora. Separator nie może się pojawić po ostatnim łańcuchu. Jeśli nie podano żadnego łańcucha wejściowego, funkcja musi zwrócić pusty ciąg znaków. Przykład: wejście { "to", "jest", "prosty", "przykład" } oraz separator ' ' (spacja); wyjście: "to jest prosty przykład". 27. Dzielenie łańcucha na tokeny z listą możliwych separatorów Napisz funkcję, która wykorzystując łańcuch wejściowy i listę możliwych znaków separatora, dzieli go na tokeny, a następnie zwraca je w strukturze std::vector. Przykład: wejście "to,jest.prosty przykład!!" oraz separatory ",.! "; wyjście: { "to", "jest", "prosty", "przykład" }. 28. Najdłuższy podciąg palindromiczny Napisz funkcję, która używając łańcucha wejściowego, znajduje i zwraca najdłuższą sekwencję znaków będącą palindromem. Jeśli istnieje wiele palindromów o tej samej długości, powinien zostać zwrócony pierwszy z nich. 29. Sprawdzanie tablic rejestracyjnych Biorąc pod uwagę tablice rejestracyjne w formacie LLL-LL DDD lub LLL-LL DDDD (gdzie L jest wielką literą od A do Z, a D cyfrą), utwórz: jedną funkcję potwierdzającą, że numer tablicy rejestracyjnej ma poprawny format; jedną funkcję, która wykorzystując podany tekst, wyodrębnia i zwraca wszystkie numery tablic rejestracyjnych w nim zawarte. 54 ecb84badecb8c394873734f1e9bfb90f e Rozdział 3. • Łańcuchy i wyrażenia regularne 30. Wyodrębnianie elementów adresu URL Napisz funkcję, która używając jako danej wejściowej łańcucha reprezentującego adres URL, analizuje go i wyodrębnia jego elementy (nazwę protokołu, domenę, port, ścieżkę, zapytanie i fragment). 31. Przekształcanie dat w łańcuchach Napisz funkcję, która biorąc pod uwagę tekst zawierający daty w formacie dd.mm.rrrr lub dd-mm-rrrr, przekształca go tak, by zawierał daty w formacie rrrr-mm-dd. 55 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Rozwiązania 23. Zamiana typu binarnego na łańcuch Aby napisać funkcję ogólnego przeznaczenia, która może obsługiwać różne rodzaje kontenerów, takie jak std::array, std::vector czy tablica języka C, powinniśmy utworzyć szablon funkcji. Wykorzystamy dwie funkcje przeciążone: jedną, której argumentami są kontener oraz wskaźnik wielkich liter, i drugą, która pobiera dwa iteratory (aby określić początkowy element zakresu i pierwszy po końcowym) oraz wskaźnik określający opcję wielkich liter. Zawartość zostanie zapisana w obiekcie std::ostringstream z odpowiednimi manipulatorami wejścia i wyjścia, takimi jak długość, znak wypełnienia oraz wskaźnik wielkich liter: template <typename Iter> std::string bytes_to_hexstr(Iter begin, Iter end, bool const uppercase = false) { std::ostringstream oss; if(uppercase) oss.setf(std::ios_base::uppercase); for (; begin != end; ++begin) oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(*begin); return oss.str(); } template <typename C> std::string bytes_to_hexstr(C const & c, bool const uppercase = false) { return bytes_to_hexstr(std::cbegin(c), std::cend(c), uppercase); } Powyższe funkcje mogą zostać użyte w następujący sposób: int main() { std::vector<unsigned char> v{ 0xBA, 0xAD, 0xF0, 0x0D }; std::array<unsigned char, 6> a{ {1,2,3,4,5,6} }; unsigned char buf[5] = {0x11, 0x22, 0x33, 0x44, 0x55}; assert(bytes_to_hexstr(v, true) == "BAADF00D"); assert(bytes_to_hexstr(a, true) == "010203040506"); assert(bytes_to_hexstr(buf, true) == "1122334455"); assert(bytes_to_hexstr(v) == "baadf00d"); assert(bytes_to_hexstr(a) == "010203040506"); assert(bytes_to_hexstr(buf) == "1122334455"); } 56 ecb84badecb8c394873734f1e9bfb90f e Rozdział 3. • Łańcuchy i wyrażenia regularne 24. Zamiana typu łańcuchowego na binarny Wymagana operacja jest przeciwieństwem tej, którą zastosowano w poprzednim zadaniu. Tym razem jednak moglibyśmy utworzyć funkcję, a nie jej szablon. Dane wejściowe to struktura std::string_view, która jest lekkim opakowaniem dla sekwencji znaków. Wyjście to wektor 8-bitowych liczb całkowitych bez znaku. Poniższa funkcja hexstr_to_bytes przekształca każde dwa znaki tekstu w wartość unsigned char (przykładowo "A0" staje się 0xA0), umieszczając je w wektorze std::vector, a następnie zwraca ten wektor: unsigned char hexchar_to_int(char const ch) { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; throw std::invalid_argument("Błędna wartość szesnastkowa"); } std::vector<unsigned char> hexstr_to_bytes(std::string_view str) { std::vector<unsigned char> result; for (size_t i = 0; i < str.size(); i += 2) { result.push_back( (hexchar_to_int(str[i]) << 4) | hexchar_to_int(str[i+1])); } return result; } W tej funkcji założono, że łańcuch wejściowy zawiera parzystą liczbę cyfr szesnastkowych. W przypadkach, w których łańcuch wejściowy zawiera nieparzystą liczbę cyfr szesnastkowych, ostatnia z nich jest odrzucana (na przykład łańcuch "BAD" zostaje przekształcony w {0xBA}). W kolejnym ćwiczeniu zmodyfikuj poprzednią funkcję w taki sposób, aby zamiast odrzucać ostatnią cyfrę nieparzystą, dodawać wiodące zero — przykładowo, łańcuch "BAD" powinien zostać zamieniony na {0x0B, 0xAD}. Ponadto dodatkowym ćwiczeniem może być utworzenie wersji funkcji, która deserializuje treść zawierającą cyfry szesnastkowe oddzielone separatorem takim jak spacja (na przykład "BA AD F0 0D"). W poniższym przykładzie kodu pokazano, w jaki sposób można użyć utworzonej funkcji: int main() { std::vector<unsigned char> expected{ 0xBA, 0xAD, 0xF0, 0x0D, 0x42 }; assert(hexstr_to_bytes("BAADF00D42") == expected); assert(hexstr_to_bytes("BaaDf00d42") == expected); } 57 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 25. Wielkie litery w tytule artykułu Szablon funkcji capitalize(), zaimplementowany w poniższy sposób, działa z łańcuchami dowolnego typu znaków. Nie modyfikuje łańcucha wejściowego, lecz tworzy nowy. Aby to zrealizować, używa klasy std::string. Przetwarza wszystkie znaki w łańcuchu wejściowym i ustawia flagę równą true wskazującą nowe słowo za każdym razem, gdy napotkana zostanie spacja lub znak interpunkcyjny. Wprowadzane znaki są przekształcane na wielkie litery, gdy reprezentują pierwszy znak w słowie. W przeciwnym razie są zamieniane na małe litery: template <class Elem> using tstring = std::basic_string<Elem, std::char_traits<Elem>, std::allocator<Elem>>; template <class Elem> using tstringstream = std::basic_stringstream< Elem, std::char_traits<Elem>, std::allocator<Elem>>; template <class Elem> tstring<Elem> capitalize(tstring<Elem> const & text) { tstringstream<Elem> result; bool newWord = true; for (auto const ch : text) { newWord = newWord || std::ispunct(ch) || std::isspace(ch); if (std::isalpha(ch)) { if (newWord) { result << static_cast<Elem>(std::toupper(ch)); newWord = false; } else result << static_cast<Elem>(std::tolower(ch)); } else result << ch; } return result.str(); } W poniższym programie możesz zobaczyć, w jaki sposób powyższa funkcja została użyta do pisania wielkimi literami: int main() { setlocale(LC_ALL, "polish"); using namespace std::string_literals; std::string text = "TO JEST prosty PrzykłaD, powinien dZIAŁAć!"; std::string expected = "To Jest Prosty Przykład, Powinien Działać!"; std::cout << text << std::endl; std::cout << expected << std::endl; 58 ecb84badecb8c394873734f1e9bfb90f e Rozdział 3. • Łańcuchy i wyrażenia regularne std::cout << capitalize(text) << std::endl; assert(expected == capitalize(text)); assert("Nowoczesne Wyzwania Języka C++"s == capitalize("nowoczesne wyzwania języka C++"s)); } 26. Łączenie łańcuchów oddzielanych separatorem W poniższym kodzie użyto dwóch funkcji przeciążonych join_strings(). Pierwsza używa kontenera łańcuchów i wskaźnika do ciągu znaków reprezentujących separatory, podczas gdy druga wykorzystuje dwa iteratory bezpośredniego dostępu (aby określić początkowy element zakresu i pierwszy po końcowym), a także separator. Obie funkcje zwracają nowy łańcuch utworzony przez połączenie wszystkich łańcuchów wejściowych, używając wyjściowego strumienia łańcuchowego oraz funkcji std::copy. Ta funkcja ogólnego przeznaczenia kopiuje wszystkie elementy w danym zakresie do zakresu wyjściowego reprezentowanego przez iterator wyjściowy. Używamy tutaj iteratora std::ostream_iterator, który za każdym razem, gdy ma przypisaną wartość, wykorzystuje operator << do przesłania jej do określonego strumienia wyjściowego: template <typename Iter> std::string join_strings(Iter begin, Iter end, char const * const separator) { std::ostringstream os; std::copy(begin, end-1, std::ostream_iterator<std::string>(os, separator)); os << *(end-1); return os.str(); } template <typename C> std::string join_strings(C const & c, char const * const separator) { if (c.size() == 0) return std::string{}; return join_strings(std::begin(c), std::end(c), separator); } int main() { setlocale(LC_ALL, "polish"); using namespace std::string_literals; std::vector<std::string> v1{ "to","jest","prosty","przykład" }; std::vector<std::string> v2{ "przykład" }; std::vector<std::string> v3{ }; 59 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów assert(join_strings(v1, " ") == "to jest prosty przykład"s); assert(join_strings(v2, " ") == "przykład"s); assert(join_strings(v3, " ") == ""s); } W kolejnym ćwiczeniu należy zmodyfikować funkcję przeciążoną, której argumentami są iteratory, aby działała z innymi ich typami, takimi jak iteratory dwukierunkowe, co umożliwi używanie w tej funkcji list i pozostałych rodzajów kontenerów. 27. Dzielenie łańcucha na tokeny z listą możliwych separatorów Oto dwie wersje funkcji dzielącej: Pierwsza z nich używa pojedynczego znaku jako ogranicznika. Wykorzystywany jest tutaj strumień łańcuchowy zainicjalizowany zawartością łańcucha wejściowego. W tym celu używana jest funkcja std::getline(), która służy do odczytywania fragmentów ciągów, aż napotkany zostanie następny separator lub znak końca wiersza. Druga funkcja używa listy możliwych znaków separatorów zawartych w typie std::string. Aby zlokalizować pierwszą pozycję dowolnego znaku separatora, zaczynając od podanego miejsca, wykorzystywana jest funkcja std::string::find_first_of(). Tę operację wykonuje się w pętli, dopóki cały łańcuch wejściowy nie zostanie przetworzony. Wyodrębnione podciągi są dodawane do wektora wynikowego: template <class Elem> using tstring = std::basic_string<Elem, std::char_traits<Elem>, std::allocator<Elem>>; template <class Elem> using tstringstream = std::basic_stringstream< Elem, std::char_traits<Elem>, std::allocator<Elem>>; template<typename Elem> inline std::vector<tstring<Elem>> split(tstring<Elem> text, Elem const delimiter) { auto sstr = tstringstream<Elem>{ text }; auto tokens = std::vector<tstring<Elem>>{}; auto token = tstring<Elem>{}; while (std::getline(sstr, token, delimiter)) { if (!token.empty()) tokens.push_back(token); } return tokens; } 60 ecb84badecb8c394873734f1e9bfb90f e Rozdział 3. • Łańcuchy i wyrażenia regularne template<typename Elem> inline std::vector<tstring<Elem>> split(tstring<Elem> text, tstring<Elem> const & delimiters) { auto tokens = std::vector<tstring<Elem>>{}; size_t pos, prev_pos = 0; while ((pos = text.find_first_of(delimiters, prev_pos)) != std::string::npos) { if (pos > prev_pos) tokens.push_back(text.substr(prev_pos, pos - prev_pos)); prev_pos = pos + 1; } if (prev_pos < text.length()) tokens.push_back(text.substr(prev_pos, std::string::npos)); return tokens; } W poniższym kodzie pokazano dwa przykłady dzielenia różnych łańcuchów przy użyciu jednego lub wielu znaków separatorów: int main() { setlocale(LC_ALL, "polish"); using namespace std::string_literals; std::vector<std::string> expected{"to", "jest", "prosty", "przykład"}; assert(expected == split("to jest prosty przykład"s, ' ')); assert(expected == split("to,jest prosty.przykład!!"s, ",.! "s)); } 28. Najdłuższy podciąg palindromiczny Najprostszym rozwiązaniem tego zadania jest zastosowanie metody „na siłę” w celu sprawdzenia, czy każdy podciąg jest palindromem. Oznacza to jednak, że musimy sprawdzić C(N, 2) podciągów (gdzie N jest liczbą znaków w łańcuchu), co spowoduje, że złożoność czasowa będzie równa O(N3). Złożoność można ograniczyć do wartości O(N2) poprzez przechowywanie rozwiązań składowych. Aby to zrealizować, potrzebujemy tabeli wartości logicznych o rozmiarze N×N, w której element [i, j] oznacza, czy podłańcuch od pozycji i do j jest palindromem. Zaczynamy od zainicjalizowania wszystkich elementów [i, i] (palindromy jednoznakowe) oraz [i, i + i] (każde kolejne dwa identyczne znaki, czyli palindromy dwuznakowe) wartościami true. Następnie przeglądamy podciągi o długościach powyżej dwóch znaków, przypisując elementowi [i, j] wartość true, jeśli element [i + i, j - 1] jest także równy true, a znaki w łańcuchu na pozycjach i oraz j są takie same. W trakcie wykonywania operacji zapamiętujemy pozycję początkową i długość najdłuższego podciągu palindromicznego, aby mieć do niego dostęp po zakończeniu przeliczania całej tablicy. 61 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Oto rozwiązanie dostępne w kodzie: std::string longest_palindrome(std::string_view str) { size_t const len = str.size(); size_t longestBegin = 0; size_t maxLen = 1; std::vector<bool> table(len * len, false); for (size_t i = 0; i < len; i++) table[i*len + i] = true; for (size_t i = 0; i < len - 1; i++) { if (str[i] == str[i + 1]) { table[i*len + i + 1] = true; if (maxLen < 2) { longestBegin = i; maxLen = 2; } } } for (size_t k = 3; k <= len; k++) { for (size_t i = 0; i < len - k + 1; i++) { size_t j = i + k - 1; if (str[i] == str[j] && table[(i + 1)*len + j - 1]) { table[i*len +j] = true; if (maxLen < k) { longestBegin = i; maxLen = k; } } } } return std::string(str.substr(longestBegin, maxLen)); } Oto przykłady użycia funkcji longest_palindrome(): int main() { setlocale(LC_ALL, "polish"); using namespace std::string_literals; assert(longest_palindrome("popotopie") == "potop"); assert(longest_palindrome("radar") == "radar"); assert(longest_palindrome("s") == "s"); } 62 ecb84badecb8c394873734f1e9bfb90f e Rozdział 3. • Łańcuchy i wyrażenia regularne 29. Sprawdzanie tablic rejestracyjnych Najprostszą metodą rozwiązania tego zadania jest użycie wyrażeń regularnych. Wyrażenie regularne, które spełnia opisany format, to "[A-Z]{3}-[A-Z]{2} \d{3,4}". Pierwsza funkcja musi tylko sprawdzić, czy łańcuch wejściowy zawiera jedynie tekst pasujący do tego wyrażenia regularnego. W tym celu możemy w następujący sposób użyć metody std::regex_match(): bool validate_license_plate_format(std::string_view str) { std::regex rx(R"([A-Z]{3}-[A-Z]{2} \d{3,4})"); return std::regex_match(str.data(), rx); } int main() { assert(validate_license_plate_format("ABC-DE 123")); assert(validate_license_plate_format("ABC-DE 1234")); assert(!validate_license_plate_format("ABC-DE 12345")); assert(!validate_license_plate_format("abc-de 1234")); } Druga funkcja jest nieco inna. Zamiast dopasowywać łańcuch wejściowy, musi zidentyfikować wszystkie wystąpienia wyrażenia regularnego w ciągu znaków. Wyrażenie regularne ulegnie zatem zmianie na "([A-Z]{3}-[A-Z]{2} \d{3,4})*". Aby znaleźć wszystkie pasujące podłańcuchy, musimy użyć iteratora std::sregex_iterator: std::vector<std::string> extract_license_plate_numbers( std::string const & str) { std::regex rx(R"(([A-Z]{3}-[A-Z]{2} \d{3,4})*)"); std::smatch match; std::vector<std::string> results; for(auto i = std::sregex_iterator(std::cbegin(str), std::cend(str), rx); i != std::sregex_iterator(); ++i) { if((*i)[1].matched) results.push_back(i->str()); } return results; } int main() { std::vector<std::string> expected { "AAA-AA 123", "ABC-DE 1234", "XYZ-WW 0001"}; std::string text("AAA-AA 123qwe-ty 1234 ABC-DE 123456..XYZ-WW 0001"); assert(expected == extract_license_plate_numbers(text)); } 63 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 30. Wyodrębnianie elementów adresu URL To zadanie również można rozwiązać za pomocą wyrażeń regularnych. Utworzenie wyrażenia regularnego, które może pasować do dowolnego adresu URL, jest jednak niełatwe. Istotą tego ćwiczenia jest wsparcie czytelnika w nabyciu umiejętności posługiwania się biblioteką regex, a nie znalezienie właściwego wyrażenia regularnego dla tego konkretnego celu. Wykorzystywane tu wyrażenie regularne służy dlatego też jedynie celom dydaktycznym. Wyrażenia regularne możesz sprawdzać za pomocą internetowych testerów i debuggerów, takich jak https://regex101.com/. Może to być przydatne podczas opracowywania wyrażeń regularnych i ich testowania z różnymi zestawami danych. Dla potrzeb tego zadania założymy, że w adresie URL protokół i domena są obowiązkowe, natomiast port, ścieżka, zapytanie i fragment są opcjonalne. Do zwrócenia wyników analizy adresu URL jest używana następująca struktura (alternatywnie mógłbyś zwrócić krotkę i użyć powiązania strukturalnego w celu połączenia zmiennych z różnymi jej elementami): struct uri_parts { std::string std::string std::optional<int> std::optional<std::string> std::optional<std::string> std::optional<std::string> }; protocol; domain; port; path; query; fragment; Funkcja, która może analizować adres URL i wyodrębniać oraz zwracać jego części, mogłaby mieć implementację taką, jak przedstawiono poniżej. Zwróć uwagę na to, że zwracanym typem jest std::optional<uri_parts>, ponieważ funkcji może się nie udać dopasować łańcuch wejściowy do wyrażenia regularnego — w tym przypadku zwracana wartość będzie równa std::nullopt: std::optional<uri_parts> parse_uri(std::string uri) { std::regex rx(R"(^(\w+):\/\/([\w.]+)(:(\d+))?([\w\/\.]+)?(\?([\w=&]*)(#?(\w+))?)?$)"); auto matches = std::smatch{}; if (std::regex_match(uri, matches, rx)) { if (matches[1].matched && matches[2].matched) { uri_parts parts; parts.protocol = matches[1].str(); parts.domain = matches[2].str(); if (matches[4].matched) parts.port = std::stoi(matches[4]); if (matches[5].matched) parts.path = matches[5]; 64 ecb84badecb8c394873734f1e9bfb90f e Rozdział 3. • Łańcuchy i wyrażenia regularne if (matches[7].matched) parts.query = matches[7]; if (matches[9].matched) parts.fragment = matches[9]; return parts; } } return {}; } W poniższym programie testujemy funkcję parse_uri() z dwoma adresami URL, które zawierają różne składowe: int main() { auto p1 = parse_uri("https://helion.pl"); assert(p1); assert(p1->protocol == "https"); assert(p1->domain == "helion.pl"); assert(!p1->port); assert(!p1->path); assert(!p1->query); assert(!p1->fragment); auto p2 = parse_uri("http://www.agh.edu.pl:443/?id=728#ui"); assert(p2); assert(p2->protocol == "http"); assert(p2->domain == "www.agh.edu.pl"); assert(p2->port == 443); assert(p2->path.value() == "/"); assert(p2->query.value() == "id=728"); assert(p2->fragment.value() == "ui"); } 31. Przekształcanie dat w łańcuchach Przekształcanie tekstu może zostać zrealizowane za pomocą wyrażeń regularnych i odpowiedniej funkcji std::regex_replace(). Wyrażenie regularne, które pasuje do dat z podanymi formatami, jest następujące: (\d{1,2})(\.|-|/)(\d{1,2})(\.|-|/)(\d{4}). To wyrażenie definiuje pięć grup dopasowania: pierwsza dotyczy dnia, druga separatora (. lub -), trzecia miesiąca, czwarta ponownie separatora (. lub -), a piąta roku. Ponieważ chcemy przekształcić daty z formatu dd.mm.rrrr lub dd-mm-rrrr na rrrr-mm-dd, wartość łańcucha wyrażenia regularnego dla funkcji std::regex_replace() powinna być równa "($5-$3-$1)": std::string transform_date(std::string_view text) { auto rx = std::regex{ R"((\d{1,2})(\.|-|/)(\d{1,2})(\.|-|/)(\d{4}))" }; return std::regex_replace(text.data(), rx, R"($5-$3-$1)"); } 65 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów int main() { setlocale(LC_ALL, "polish"); using namespace std::string_literals; assert(transform_date("dziś mamy 11.10.2018!"s) == "dziś mamy 2018-10-11!"s); } 66 ecb84badecb8c394873734f1e9bfb90f e 4 Strumienie i systemy plików Zadania 32. Trójkąt Pascala Napisz funkcję, która wyświetla w konsoli do 10 wierszy trójkąta Pascala. 33. Lista procesów w postaci tabeli Załóżmy, że masz listę wszystkich procesów działających w danej chwili w systemie. Informacje związane z każdym z nich zawierają jego nazwę, identyfikator, stan (wykonywany lub oczekujący), nazwę użytkownika (dla którego został uruchomiony dany proces), rozmiar pamięci w bajtach i platformę (która może być 32-bitowa lub 64-bitowa). Twoim zadaniem jest napisanie funkcji, która pobiera taką listę procesów, sortuje alfabetycznie, a następnie wyświetla w konsoli w formacie tabelarycznym. Wszystkie kolumny muszą być wyrównane do lewej, z wyjątkiem kolumny informującej o zajętości pamięci (musi być wyrównana do prawej). Wartość rozmiaru pamięci powinna być wyświetlana w kilobajtach. Poniżej znajduje się przykład działania takiej funkcji: chrome.exe chrome.exe cmd.exe explorer.exe skype.exe 1044 10100 512 7108 22456 Wykonywany Wykonywany Wykonywany Wykonywany Oczekujący marius.bancila marius.bancila SYSTEM marius.bancila marius.bancila ecb84badecb8c394873734f1e9bfb90f 25180 227756 48 29529 656 32 32 64 64 64 bity bity bity bity bity e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 34. Usuwanie pustych wierszy z pliku tekstowego Napisz program, który mając podaną ścieżkę do pliku tekstowego, modyfikuje go, usuwając wszystkie puste wiersze. Wiersze zawierające tylko białe znaki są uważane za puste. 35. Obliczanie rozmiaru katalogu Utwórz funkcję, która w sposób rekurencyjny oblicza wielkość katalogu. Wynik powinien być wyrażony w bajtach. Musi być także dostępna opcja określająca, czy należy uwzględniać dowiązania symboliczne. 36. Usuwanie plików starszych od określonej daty Napisz funkcję, która jako parametry wejściowe przyjmuje ścieżkę do katalogu oraz czas, a następnie usuwa rekurencyjnie wszystkie elementy (pliki lub podkatalogi) starsze od tego czasu. Czas może być reprezentowany przez dowolne wartości, takie jak dni, godziny, minuty, sekundy lub ich kombinacje, na przykład jedna godzina i dwadzieścia minut. Jeśli podany katalog jest starszy niż podany czas, powinien zostać całkowicie usunięty. 37. Wyszukiwanie w katalogu plików, które pasują do wyrażenia regularnego Utwórz funkcję, która biorąc pod uwagę ścieżkę do katalogu i wyrażenie regularne, zwraca listę wszystkich elementów o nazwach pasujących do tego wyrażenia. 38. Tymczasowe pliki logów Utwórz klasę logowania zapisującą informacje tekstowe w pliku tekstowym, który można usuwać. Plik tekstowy powinien mieć unikatową nazwę i musi się znajdować w katalogu tymczasowym. O ile nie określono inaczej, plik ten powinien zostać usunięty, gdy instancja klasy zostanie zniszczona. Powinno jednak być możliwe zachowanie pliku logów poprzez przeniesienie go do innej lokalizacji. 68 ecb84badecb8c394873734f1e9bfb90f e Rozdział 4. • Strumienie i systemy plików Rozwiązania 32. Trójkąt Pascala Trójkąt Pascala jest konstrukcją reprezentującą współczynniki dwumianu. Zaczyna się on od wiersza zawierającego pojedynczą wartość 1. Elementy każdego kolejnego wiersza są tworzone poprzez zsumowanie dwóch najbliższych liczb z lewej i prawej strony z wiersza nadrzędnego oraz traktowanie brakujących wpisów jako wartości 0. Oto przykład trójkąta z pięcioma wierszami: 1 1 1 1 1 1 2 3 4 1 3 6 1 4 1 Aby wyświetlić trójkąt, musimy wykonać następujące działania: Przesunąć pozycję wyjściową w prawo o odpowiednią liczbę spacji, tak aby najwyższy wiersz był wyświetlany na środku podstawy trójkąta. Obliczyć każdą wartość, sumując wartości z lewej i prawej strony z wierszy nadrzędnych. Prostsza metoda polega na tym, że dla wiersza i oraz kolumny j każda nowa wartość x jest równa poprzedniej wartości x pomnożonej przez (i - j) / (j + 1), przy czym x rozpoczyna się od 1. Oto możliwa implementacja funkcji wyświetlającej trójkąt Pascala: unsigned int number_of_digits(unsigned int const i) { return i > 0 ? (int)log10((double)i) + 1 : 1; } void print_pascal_triangle(int const n) { for (int i = 0; i < n; i++) { auto x = 1; std::cout << std::string((n - i - 1)*(n / 2), ' '); for (int j = 0; j <= i; j++) { auto y = x; x = x * (i - j) / (j + 1); auto maxlen = number_of_digits(x) - 1; std::cout << y << std::string(n - 1 - maxlen - n%2, ' '); } std::cout << std::endl; } } 69 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Poniższy program prosi użytkownika o wprowadzenie liczby poziomów i wyświetla trójkąt w konsoli: int main() { setlocale(LC_ALL, "polish"); int n = 0; std::cout << "Liczba poziomów (maksymalnie 10): "; std::cin >> n; if (n > 10) std::cout << "Wartość za duża" << std::endl; else if (n < 2) std::cout << "Wartość za mała" << std::endl; else print_pascal_triangle(n); } 33. Lista procesów w postaci tabeli Aby rozwiązać to zadanie, weźmiemy pod uwagę następującą klasę reprezentującą informacje o procesie: enum class procstatus {suspended, running}; enum class platforms {p32bit, p64bit}; struct procinfo { int id; std::string name; procstatus status; std::string account; size_t memory; platforms platform; }; Aby wyświetlić stan procesu i rodzaj platformy w postaci tekstowej, a nie liczbowej, potrzebujemy funkcji zamieniającej typ wyliczeniowy na std::string: std::string status_to_string(procstatus const status) { if (status == procstatus::suspended) return "oczekujący"; else return "wykonywany"; } std::string platform_to_string(platforms const platform) { if (platform == platforms::p32bit) return "32 bity"; else return "64 bity"; } 70 ecb84badecb8c394873734f1e9bfb90f e Rozdział 4. • Strumienie i systemy plików Procesy muszą zostać posortowane alfabetycznie według ich nazw, dlatego tę operację należy wykonać na samym początku. Podczas wyświetlania informacji powinniśmy używać manipulatorów wejścia-wyjścia: void print_processes(std::vector<procinfo> processes) { std::sort( std::begin(processes), std::end(processes), [](procinfo const & p1, procinfo const & p2) { return p1.name < p2.name; }); for (auto const { std::cout << << std::cout << << std::cout << << std::cout << << std::cout << << std::cout << std::cout << } & pi : processes) std::left << std::setw(25) << std::setfill(' ') pi.name; std::left << std::setw(8) << std::setfill(' ') pi.id; std::left << std::setw(12) << std::setfill(' ') status_to_string(pi.status); std::left << std::setw(15) << std::setfill(' ') pi.account; std::right << std::setw(10) << std::setfill(' ') (int)(pi.memory/1024); std::left << ' ' << platform_to_string(pi.platform); std::endl; } W poniższym programie zdefiniowano listę procesów (przy użyciu interfejsów API specyficznych dla danego systemu operacyjnego można też pobrać listę rzeczywiście uruchomionych procesów) i wyświetlono ją w konsoli w żądanym formacie: int main() { setlocale(LC_ALL, "polish"); using namespace std::string_literals; std::vector<procinfo> processes { {512, "cmd.exe"s, procstatus::running, "SYSTEM"s, 148293, platforms::p64bit }, {1044, "chrome.exe"s, procstatus::running, "marius.bancila"s, 25180454, platforms::p32bit}, {7108, "explorer.exe"s, procstatus::running, "marius.bancila"s, 2952943, platforms::p64bit }, {10100, "chrome.exe"s, procstatus::running, "marius.bancila"s, 227756123, platforms::p32bit}, {22456, "skype.exe"s, procstatus::suspended, "marius.bancila"s, 16870123, platforms::p64bit }, }; print_processes(processes); } 71 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 34. Usuwanie pustych wierszy z pliku tekstowego Możliwym sposobem rozwiązania tego zadania jest wykonanie następujących działań: 1. Utworzenie pliku tymczasowego służącego do przechowywania tekstu, który chcesz zachować z pliku pierwotnego. 2. Czytanie wierszy z pliku wejściowego, a następnie kopiowanie do pliku tymczasowego tych, które nie są puste. 3. Usunięcie pliku oryginalnego po zakończeniu przetwarzania. 4. Przeniesienie pliku tymczasowego w miejsce pliku oryginalnego. Alternatywą jest przeniesienie pliku tymczasowego i nadpisanie pierwotnego. W poniższej implementacji zastosowano wymienione wcześniej działania. Plik tymczasowy jest tworzony w katalogu tymczasowym zwróconym przez funkcję filesystem::temp_directory_path(): namespace fs = std::experimental::filesystem; void remove_empty_lines(fs::path filepath) { std::ifstream filein(filepath.native(), std::ios::in); if (!filein.is_open()) throw std::runtime_error("nie można otworzyć pliku wejściowego"); auto temppath = fs::temp_directory_path() / "temp.txt"; std::ofstream fileout(temppath.native(), std::ios::out | std::ios::trunc); if (!fileout.is_open()) throw std::runtime_error("nie można utworzyć pliku tymczasowego"); std::string line; while (std::getline(filein, line)) { if (line.length() > 0 && line.find_first_not_of(' ') != line.npos) { fileout << line << '\n'; } } filein.close(); fileout.close(); fs::remove(filepath); fs::rename(temppath, filepath); } 72 ecb84badecb8c394873734f1e9bfb90f e Rozdział 4. • Strumienie i systemy plików 35. Obliczanie rozmiaru katalogu Aby obliczyć rozmiar katalogu, musimy przeanalizować wszystkie pliki i zsumować ich wielkości. Iterator filesystem::recursive_directory_iterator pochodzi z biblioteki filesystem. Pozwala on na rekurencyjne przetwarzanie wszystkich elementów w katalogu. Posiada różne konstruktory — niektóre z nich używają argumentu typu filesystem::directory_options, który wskazuje, czy dowiązania symboliczne powinny być brane pod uwagę. Do sumowania wielkości plików może posłużyć algorytm ogólnego zastosowania std::accumulate(). Ponieważ całkowity rozmiar katalogu może przekraczać 2 GB, dla wartości sumarycznej nie powinieneś używać typu int ani long, lecz unsigned long long. Poniżej przedstawiono funkcję z przykładową implementacją naszego zadania: namespace fs = std::experimental::filesystem; std::uintmax_t get_directory_size(fs::path const & dir, bool const follow_symlinks = false) { auto iterator = fs::recursive_directory_iterator( dir, follow_symlinks ? fs::directory_options::follow_directory_symlink : fs::directory_options::none); return std::accumulate( fs::begin(iterator), fs::end(iterator), 0ull, [](std::uintmax_t const total, fs::directory_entry const & entry) { return total + (fs::is_regular_file(entry) ? fs::file_size(entry.path()) : 0); }); } int main() { setlocale(LC_ALL, "polish"); std::string path; std::cout << "Ścieżka: "; std::cin >> path; std::cout << "Rozmiar: " << get_directory_size(path) << std::endl; } 36. Usuwanie plików starszych od określonej daty W celu wykonywania operacji dotyczących systemu plików powinieneś używać biblioteki filesystem. Aby realizować zagadnienia związane czasem i okresem trwania, należy używać biblioteki chrono. Funkcja implementująca wymagania tego zadania musi wykonać następujące czynności: 73 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 1. Sprawdzić, czy element wskazany przez ścieżkę docelową istnieje i jest starszy od podanego czasu; jeśli tak, należy go usunąć. 2. Jeśli nie jest starszy, lecz jest katalogiem, musi przetworzyć wszystkie jego elementy i rekurencyjnie wywołać funkcję: namespace fs = std::experimental::filesystem; namespace ch = std::chrono; template <typename Duration> bool is_older_than(fs::path const & path, Duration const duration) { auto ftimeduration = fs::last_write_time(path).time_since_epoch(); auto nowduration = (ch::system_clock::now() - duration) .time_since_epoch(); return ch::duration_cast<Duration>(nowduration - ftimeduration) .count() > 0; } template <typename Duration> void remove_files_older_than(fs::path const & path, Duration const duration) { try { if (fs::exists(path)) { if (is_older_than(path, duration)) { fs::remove(path); } else if(fs::is_directory(path)) { for (auto const & entry : fs::directory_iterator(path)) { remove_files_older_than(entry.path(), duration); } } } } catch (std::exception const & ex) { std::cerr << ex.what() << std::endl; } } Alternatywą dla użycia iteratora directory_iterator i rekurencyjnego wywoływania funkcji remove_files_older_than() mogłoby być wykorzystanie iteratora recursive_directory_iterator i po prostu usuwanie elementu starszego niż podany czas. Jednak w tym podejściu pojawiłoby się niezdefiniowane zachowanie, ponieważ w przypadku, gdyby plik lub folder został usunięty lub dodany do drzewa katalogów po utworzeniu rekurencyjnego iteratora, nie zostałoby określone, czy zmiana będzie obserwowana w tym iteratorze. Dlatego też należy unikać tej metody. 74 ecb84badecb8c394873734f1e9bfb90f e Rozdział 4. • Strumienie i systemy plików Szablon funkcji is_older_than() określa czas, który minął od początku epoki zegara systemowego do bieżącej chwili oraz ostatniej operacji zapisu pliku, a następnie sprawdza, czy różnica tych dwóch wartości jest większa niż podany czas trwania. Funkcja remove_files_older_than() może zostać użyta w następujący sposób: int main() { using namespace std::chrono_literals; #ifdef _WIN32 auto path = R"(..\Test\)"; #else auto path = R"(../Test/)"; #endif remove_files_older_than(path, 1h + 20min); } 37. Wyszukiwanie w katalogu plików, które pasują do wyrażenia regularnego Implementacja wymaganej funkcjonalności powinna być prosta: przetwórz rekurencyjnie wszystkie elementy w określonym katalogu, zachowując te, które są zwykłymi plikami i których nazwa pasuje do wyrażenia regularnego. Innymi słowy, należy wykonać następujące działania: Użyć iteratora filesystem::recursive_directory_iterator w celu przetworzenia elementów katalogu. Zastosować bibliotekę regex i funkcję regex_match(), aby sprawdzić, czy nazwa pliku odpowiada wyrażeniu regularnemu. Użyć funkcji copy_if() oraz back_inserter, by umieścić na końcu wektora te elementy katalogu, które spełniają podane kryteria. Utworzona funkcja mogłaby wyglądać jak poniżej: namespace fs = std::experimental::filesystem; std::vector<fs::directory_entry> find_files( fs::path const & path, std::string_view regex) { std::vector<fs::directory_entry> result; std::regex rx(regex.data()); std::copy_if( fs::recursive_directory_iterator(path), fs::recursive_directory_iterator(), std::back_inserter(result), [&rx](fs::directory_entry const & entry) { return fs::is_regular_file(entry.path()) && 75 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów std::regex_match(entry.path().filename().string(), rx); }); return result; } Mając dostępny powyższy kod, możemy napisać taki oto program: int main() { auto dir = fs::temp_directory_path(); auto pattern = R"(wct[0-9a-zA-Z]{3}\.tmp)"; auto result = find_files(dir, pattern); for (auto const & entry : result) { std::cout << entry.path().string() << std::endl; } } 38. Tymczasowe pliki logów Klasa logowania, którą należy utworzyć jako rozwiązanie tego zadania, powinna się charakteryzować następującymi cechami: posiadać konstruktor, który tworzy plik tekstowy w katalogu tymczasowym i otwiera go w trybie do zapisu; podczas destrukcji zamykać i usuwać plik tekstowy (jeśli wciąż istnieje); posiadać metodę, która zamyka plik i przenosi go do innej lokalizacji; przeciążać operator << w celu wprowadzania wiadomości tekstowej do pliku wyjściowego. Aby tworzyć unikatowe nazwy plików, możesz wykorzystać identyfikatory UUID (zwane również identyfikatorami GUID). Mimo że standard C++ nie obsługuje żadnej funkcjonalności związanej z tym zagadnieniem, istnieją biblioteki innych firm, takie jak boost::uuid, CrossGuid lub stduuid, która jest biblioteką utworzoną przeze mnie i wykorzystaną do rozwiązania tego zadania. Możesz ją pobrać ze strony https://github.com/mariusbancila/stduuid: namespace fs = std::experimental::filesystem; class logger { fs::path logpath; std::ofstream logfile; public: logger() { auto name = uuids::to_string(uuids::uuid_random_generator{}()); logpath = fs::temp_directory_path() / (name + ".tmp"); logfile.open(logpath.c_str(), std::ios::out|std::ios::trunc); } 76 ecb84badecb8c394873734f1e9bfb90f e Rozdział 4. • Strumienie i systemy plików ~logger() noexcept { try { if(logfile.is_open()) logfile.close(); if (!logpath.empty()) fs::remove(logpath); } catch (...) {} } void persist(fs::path const & path) { logfile.close(); fs::rename(logpath, path); logpath.clear(); } logger& operator<<(std::string_view message) { logfile << message.data() << '\n'; return *this; } }; Oto przykład użycia powyższej klasy: int main() { setlocale(LC_ALL, "polish"); logger log; try { log << "to jest jakiś wiersz tekstu," << "a to kolejny wiersz tekstu"; throw std::runtime_error("błąd"); } catch (...) { log.persist(R"(lastlog.txt)"); } } 77 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 78 ecb84badecb8c394873734f1e9bfb90f e 5 Data i czas Zadania 39. Pomiar czasu wykonania funkcji Napisz procedurę, która może mierzyć czas wykonania funkcji (posiadającej dowolną liczbę argumentów) i zwracać wartość w określonej jednostce (na przykład w sekundach, milisekundach czy mikrosekundach). 40. Liczba dni zawartych między dwiema datami Utwórz funkcję, która przyjmuje jako argumenty wejściowe dwie daty i zwraca liczbę dni zawartych między nimi. Funkcja powinna działać poprawnie bez względu na kolejność dat będących danymi wejściowymi. 41. Dzień tygodnia Napisz funkcję, która dla danej daty określa jej dzień tygodnia. Ta funkcja powinna zwracać wartość zawartą między 1 (dla poniedziałku) a 7 (dla niedzieli). 42. Numer dnia i tygodnia w roku Napisz funkcję, która dla podanej daty zwraca odpowiadający jej numer dnia w roku (od 1 do 365 lub 366 dla lat przestępnych), a także inną funkcję, która dla tego samego argumentu zwraca odpowiadający mu numer tygodnia kalendarzowego w roku. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 43. Czasy spotkań dla wielu stref czasowych Napisz funkcję, która biorąc pod uwagę listę uczestników spotkania i ich strefy czasowe, wyświetla lokalny czas spotkania dla każdego z nich. 44. Kalendarz miesięczny Utwórz funkcję, która dla podanego roku i miesiąca wyświetla w konsoli kalendarz miesięczny. Oczekiwany format wyjściowy jest taki (przykład dotyczy grudnia 2018 roku): Pon. Wt. Śr. Czw. Pt. 3 10 17 24 31 4 11 18 25 5 12 19 26 6 13 20 27 7 14 21 28 Sob. 1 8 15 22 29 80 ecb84badecb8c394873734f1e9bfb90f Niedz. 2 9 16 23 30 e Rozdział 5. • Data i czas Rozwiązania 39. Pomiar czasu wykonania funkcji Aby zmierzyć czas wykonania funkcji, należy pobrać bieżący czas przed jej wykonaniem, uruchomić ją, a następnie ponownie pobrać bieżący czas i określić, ile czasu upłynęło między tymi dwoma momentami. Dla wygody można nasze rozwiązanie umieścić w szablonie funkcji wariadycznej, który przyjmuje jako argumenty wejściowe funkcję do wykonania i jej parametry oraz charakteryzuje się następującymi cechami: Używa domyślnie klasy std::high_resolution_clock do określenia bieżącego czasu. Stosuje metodę std::invoke() w celu wywołania mierzonej funkcji z podanymi argumentami. Zwraca czas trwania, a nie liczbę tyknięć zegara. Ważne jest, by funkcja umożliwiała dokładny pomiar czasu i wykorzystywała różne rozdzielczości, takie jak sekundy czy milisekundy, co nie jest możliwe w przypadku użycia liczby tyknięć zegara: template <typename Time = std::chrono::microseconds, typename Clock = std::chrono::high_resolution_clock> struct perf_timer { template <typename F, typename... Args> static Time duration(F&& f, Args... args) { auto start = Clock::now(); std::invoke(std::forward<F>(f), std::forward<Args>(args)...); auto end = Clock::now(); return std::chrono::duration_cast<Time>(end - start); } }; Powyższy szablon funkcji może zostać użyty w następujący sposób: void f() { // symulacja działania std::this_thread::sleep_for(2s); } void g(int const a, int const b) { // symulacja działania std::this_thread::sleep_for(1s); } int main() { 81 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów auto t1 = perf_timer<std::chrono::microseconds>::duration(f); auto t2 = perf_timer<std::chrono::milliseconds>::duration(g, 1, 2); } auto total = std::chrono::duration<double, std::nano>(t1 + t2).count(); 40. Liczba dni zawartych między dwiema datami W wersji C++ 17 standardowa biblioteka chrono nie obsługuje zagadnień związanych z datami, tygodniami, kalendarzami, strefami czasowymi i podobnymi przydatnymi funkcjonalnościami. Zostanie to jednak zmienione w wersji C++ 20, ponieważ na spotkaniu w Jacksonville w marcu 2018 roku strefy czasowe i obsługa kalendarza zostały dodane do standardu. Nowe dodatki są oparte na bibliotece date o otwartych źródłach, zbudowanej przy użyciu biblioteki chrono i zaprojektowanej przez Howarda Hinnanta. Jest ona dostępna w portalu GitHub pod adresem https://github.com/HowardHinnant/date. Użyjemy tej biblioteki do rozwiązania kilku zadań z tego rozdziału. Chociaż w bieżącej implementacji przestrzenią nazw jest date, w wersji C++ 20 będzie nią std::chrono. Wystarczy jednak w przyszłości po prostu zastąpić starą przestrzeń nazw, bez konieczności wprowadzania jakichkolwiek dalszych zmian w kodzie. Aby rozwiązać obecne zadanie, możesz użyć klasy date::sys_days, dostępnej w nagłówku date.h. Reprezentuje ona liczbę dni, które upłynęły od początku epoki std::system_clock. Mamy tu dostępny punkt czasowy (time_point) obsługujący odstępy czasowe o rozmiarze dnia, domyślnie przekształcany na std::system_clock::time_point. Zasadniczo powinieneś utworzyć dwa obiekty typu date::sys_days i odjąć je od siebie. Wynikiem będzie liczba dni zawartych między dwiema datami. Poniżej znajduje się prosta implementacja przykładowej funkcji: inline int number_of_days( int const y1, unsigned int const m1, unsigned int const d1, int const y2, unsigned int const m2, unsigned int const d2) { using namespace date; return (sys_days{ year{ y1 } / month{ m1 } / day{ d1 } } sys_days{ year{ y2 } / month{ m2 } / day{ d2 } }).count(); } inline int number_of_days(date::sys_days const & first, date::sys_days const & last) { return (last - first).count(); } Oto kilka przykładów, w jaki sposób można wykorzystać te przeciążone funkcje: int main() { auto diff1 = number_of_days(2016, 9, 23, 2017, 5, 15); } using namespace date::literals; auto diff2 = number_of_days(2016_y/sep/23, 15_d/may/2017); 82 ecb84badecb8c394873734f1e9bfb90f e Rozdział 5. • Data i czas 41. Dzień tygodnia Rozwiązanie tego zadania będzie ponownie stosunkowo proste, jeśli skorzystasz z biblioteki date. Jednak tym razem musisz użyć następujących typów: date::year_month_day — struktura reprezentująca datę z dostępnymi polami przeznaczonymi dla roku, miesiąca (wartość od 1 do 12) i dnia (wartość od 1 do 31); date::iso_week::year_weeknum_weekday — struktura pochodząca z nagłówka iso_week.h i zawierającą pola pozwalające na przechowywanie roku, liczby tygodni w roku, a także liczby dni w tygodniu (wartość od 1 do 7). Ta klasa jest niejawnie przekształcana w obie strony na date::sys_days, co powoduje, że jest ona następnie jawnie zamieniana na dowolny inny system kalendarzowy (na przykład date::year_month_day), który jest niejawnie przekształcany w dwie strony na date:sys_days. Jeśli weźmie się powyższe pod uwagę, rozwiązanie zadania nastąpi przez utworzenie obiektu year_month_day w celu reprezentowania żądanej daty, a następnie pochodzącego z niego obiektu year_weeknum_weekday, co skutkuje pobraniem dnia tygodnia za pomocą metody weekday(): unsigned int week_day(int const y, unsigned int const m, unsigned int const d) { using namespace date; if(m < 1 || m > 12 || d < 1 || d > 31) return 0; auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }}; auto const tiso = iso_week::year_weeknum_weekday{ dt }; return (unsigned int)tiso.weekday(); } int main() { auto wday = week_day(2018, 5, 9); } 42. Numer dnia i tygodnia w roku Rozwiązanie tego dwuczęściowego zadania powinno wynikać wprost z dwóch poprzednich: Aby wyznaczyć dzień roku, odejmij od siebie dwa obiekty date::sys_days — jeden reprezentujący dany dzień, a drugi odpowiadający dacie 0 stycznia tego samego roku. Możesz ewentualnie zacząć od 1 stycznia i dodać wartość 1 do wyniku. Aby określić numer tygodnia w danym roku, utwórz — podobnie jak w poprzednim zadaniu — obiekt year_weeknum_weekday, a następnie pobierz wartość weeknum(): int day_of_year(int const y, unsigned int const m, unsigned int const d) { 83 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów using namespace date; if(m < 1 || m > 12 || d < 1 || d > 31) return 0; return (sys_days{ year{ y } / month{ m } / day{ d } } sys_days{ year{ y } / jan / 0 }).count(); } unsigned int calendar_week(int const y, unsigned int const m, unsigned int const d) { using namespace date; if(m < 1 || m > 12 || d < 1 || d > 31) return 0; auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }}; auto const tiso = iso_week::year_weeknum_weekday{ dt }; return (unsigned int)tiso.weeknum(); } Powyższe funkcje mogą zostać użyte w następujący sposób: int main() { setlocale(LC_ALL, "polish"); int y = 0; unsigned int std::cout << std::cout << std::cout << m = 0, d = 0; "Rok:"; std::cin >> y; "Miesiąc:"; std::cin >> m; "Dzień:"; std::cin >> d; std::cout << "Tydzień kalendarzowy:" << calendar_week(y, m, d) << std::endl; std::cout << "Dzień w roku:" << day_of_year(y, m, d) << std::endl; } 43. Czasy spotkań dla wielu stref czasowych Aby obsługiwać strefy czasowe, musisz użyć nagłówka tz.h dostępnego w bibliotece date. Jednak wymaga to wcześniejszego pobrania i rozpakowania bazy danych IANA Time Zone Database. Oto procedura przygotowania bazy danych stref czasowych dla biblioteki date: Pobierz bazę danych ze strony https://www.iana.org/time-zones. W czasie tłumaczenia tej książki najnowszą wersją bazy był plik o nazwie tzdata2018e.tar.gz. Rozpakuj plik w dowolnym miejscu systemu plików w komputerze, w podkatalogu o nazwie tzdata. Załóżmy, że katalogiem nadrzędnym jest c:\work\challenges\libs\date (w komputerze z systemem Windows). Wówczas w tym katalogu powinien się pojawić podfolder o nazwie tzdata. 84 ecb84badecb8c394873734f1e9bfb90f e Rozdział 5. • Data i czas W przypadku środowiska Windows musisz pobrać plik o nazwie windowsZones.xml zawierający odwzorowania stref czasowych systemu Windows na strefy czasowe IANA. Jest on dostępny pod adresem https://unicode.org/repos/cldr/trunk/ common/supplemental/windowsZones.xml. Plik musi zostać umieszczony w tym samym, wcześniej utworzonym katalogu podrzędnym tzdata. W ustawieniach projektu zdefiniuj makro preprocesora o nazwie INSTALL, które będzie wskazywać katalog nadrzędny dla podkatalogu tzdata. W naszym przykładzie powinieneś zdefiniować wartość INSTALL=c:\\work\\challenges\\libs\\date (zwróć uwagę na to, że konieczne będzie użycie podwójnego ukośnika odwrotnego, ponieważ makro służy do tworzenia ścieżki do pliku za pomocą stringifikacji i konkatenacji — jeśli tego nie wykonasz, ścieżka będzie niepoprawna). Aby rozwiązać to zadanie, weźmiemy pod uwagę strukturę user zawierającą minimalną ilość informacji — nazwę użytkownika i strefę czasową. Sama strefa czasowa jest tworzona przy użyciu funkcji date::locate_zone(): struct user { std::string Name; date::time_zone const * Zone; explicit user(std::string_view name, std::string_view zone) : Name{name.data()}, Zone(date::locate_zone(zone.data())) {} }; Funkcja wyświetlająca listę użytkowników i ich lokalne czasy rozpoczęcia spotkania powinna przekształcić podany czas ze strefy odniesienia na czas w strefie własnej. Aby to zrealizować, możemy użyć konstruktora konwertującego z klasy date::zoned_time: template <class Duration, class TimeZonePtr> void print_meeting_times( date::zoned_time<Duration, TimeZonePtr> const & time, std::vector<user> const & users) { std::cout << std::left << std::setw(15) << std::setfill(' ') << "Czas lokalny: " << time << std::endl; for (auto const & user : users) { std::cout << std::left << std::setw(15) << std::setfill(' ') << user.Name << date::zoned_time<Duration, TimeZonePtr>(user.Zone, time) << std::endl; } } 85 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Utworzona funkcja może zostać użyta w poniższym kodzie, w którym dany czas (odpowiadający określonej godzinie i minucie) jest reprezentowany w bieżącej strefie czasowej: int main() { setlocale(LC_ALL, "polish"); std::vector<user> users{ user{ "Vit", "Europe/Moscow" }, user{ "Krzysztof", "Europe/Warsaw" }, user{ "Jane", "America/New_York" } }; unsigned int h, m; std::cout << "Godzina:"; std::cin >> h; std::cout << "Minuty:"; std::cin >> m; date::year_month_day today = date::floor<date::days>(ch::system_clock::now()); auto localtime = date::zoned_time<std::chrono::minutes>( date::current_zone(), static_cast<date::local_days>(today) + ch::hours{ h } +ch::minutes{ m }); print_meeting_times(localtime, users); } 44. Kalendarz miesięczny Rozwiązanie tego problemu jest częściowo oparte na wynikach poprzednich zadań. Aby odpowiednio wyświetlić dni miesiąca, powinieneś: wiedzieć, jaki dzień tygodnia jest pierwszym dniem miesiąca — można to ustalić za pomocą funkcji week_day() utworzonej dla poprzedniego zadania; znać liczbę dni w danym miesiącu — taką informację można uzyskać za pomocą struktury date::year_month_day_last, odczytując wynik metody day(). Po ustaleniu powyższych informacji należy wykonać następujące działania: w przypadku pierwszego tygodnia wyświetlić spacje dla dni poprzedzających pierwszy dzień miesiąca; wyświetlić numery dni od pierwszego do ostatniego dnia miesiąca, zachowując odpowiednie formatowanie; przechodzić do nowego wiersza po każdych siedmiu dniach (licząc od pierwszego dnia pierwszego tygodnia, nawet jeśli należałby on do poprzedniego miesiąca). 86 ecb84badecb8c394873734f1e9bfb90f e Rozdział 5. • Data i czas Oto implementacja założeń zadania: unsigned int week_day(int const y, unsigned int const m, unsigned int const d) { using namespace date; if(m < 1 || m > 12 || d < 1 || d > 31) return 0; auto const dt = date::year_month_day{ year{ y }, month{ m }, day{ d } }; auto const tiso = iso_week::year_weeknum_weekday{ dt }; return (unsigned int)tiso.weekday(); } void print_month_calendar(int const y, unsigned int m) { using namespace date; std::cout << "Pon. Wt. Śr. Czw. Pt. Sob. Niedz." << std::endl; auto first_day_weekday = week_day(y, m, 1); auto last_day = (unsigned int)year_month_day_last( year{ y }, month_day_last{ month{ m } }).day(); unsigned int index = 1; for (unsigned int day = 1; day < first_day_weekday; ++day, ++index) { std::cout << " "; } std::string str_day; for (unsigned int day = 1; day <= last_day; ++day) { str_day = (day < 10 ? " " : "") + std::to_string(day); std::cout << std::left << std::setfill(' ') << std::setw(6) << str_day << ' '; if (index++ % 7 == 0) std::cout << std::endl; } std::cout << std::endl; } int main() { setlocale(LC_ALL, "polish"); print_month_calendar(2018, 12); } 87 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 88 ecb84badecb8c394873734f1e9bfb90f e 6 Algorytmy i struktury danych Zadania 45. Kolejka priorytetowa Utwórz strukturę danych, która reprezentuje kolejkę priorytetową o stałej złożoności obliczeniowej w przypadku wyszukiwania największego elementu oraz logarytmiczną złożoność obliczeniową podczas dodawania i usuwania elementów. Nowe elementy są wstawiane na koniec kolejki, a usuwane z jej początku. Domyślnie kolejka powinna używać operatora < do porównywania elementów, jednakże powinna również istnieć możliwość dostarczenia przez użytkownika obiektu funkcji porównania, który zwróci wartość true, jeśli pierwszy argument będzie mniejszy od drugiego. Implementacja musi zapewniać wykonywanie co najmniej następujących operacji: push() — dodawanie nowego elementu, pop() — usuwanie elementu z początku kolejki, top() — dostęp do elementu na początku kolejki, size() — zwracanie liczby elementów w kolejce, empty() — sprawdzanie, czy kolejka jest pusta. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 46. Bufor cykliczny Utwórz strukturę danych, która reprezentuje bufor cykliczny o stałym rozmiarze. W tego typu buforze istniejące elementy zostaną nadpisane, gdy nastąpi jego przepełnienie. Klasa, którą musisz utworzyć, powinna charakteryzować się następującymi cechami: nie używać domyślnego konstruktora; obsługiwać tworzenie obiektów o określonym rozmiarze; pozwalać na sprawdzanie rozmiaru bufora oraz jego stanu (metody empty(), full(), size(), capacity()); pozwalać na dodawanie nowego elementu — taka operacja może w określonej sytuacji zastępować najstarszy element w buforze; usuwać najstarszy element z bufora; wspierać przetwarzanie iteracyjne elementów bufora. 47. Podwójne buforowanie Utwórz klasę implementującą bufor, który może być zapisywany i odczytywany w tym samym czasie bez kolizji tych dwóch operacji. Powinna zostać zapewniona możliwość odczytywania poprzednich danych w trakcie wykonywania operacji zapisu. Nowo zapisane dane muszą być dostępne do odczytu po zakończeniu operacji zapisu. 48. Najczęściej występujący element w zbiorze danych Napisz funkcję, która zwraca najczęściej pojawiający się element w zbiorze danych oraz liczbę jego wystąpień. Jeśli więcej niż jeden element pojawia się taką samą maksymalną liczbę razy, funkcja powinna zwrócić je wszystkie. Na przykład dla zestawu danych {1,1,3,5,8,13,3,5,8,8,5} powinny zostać zwrócone dwie pary {5, 3} oraz {8, 3}. 49. Histogram tekstu Napisz program, który oblicza i wyświetla histogram zawierający częstość występowania każdej z liter alfabetu w podanym tekście. Częstość występowania to wartość procentowa wyznaczana jako liczba wystąpień danej litery w porównaniu z całkowitą liczbą liter w tekście. Program powinien brać pod uwagę tylko same litery i ignorować cyfry oraz inne możliwe znaki. Częstość musi zostać określona na podstawie liczby liter, a nie rozmiaru tekstu. 90 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych 50. Filtrowanie listy numerów telefonów Utwórz funkcję, która używając jako argumentu wejściowego listy numerów telefonów, zwraca z niej tylko te numery, które pochodzą z określonego kraju. Każdy kraj ma numer kierunkowy, na przykład 48 oznacza Polskę. Numery telefonów mogą się rozpoczynać od numeru kierunkowego kraju, od znaku +, po którym następuje numer kierunkowy lub mogą w ogóle go nie mieć. Takie, które należą do tej ostatniej kategorii, powinny zostać zignorowane. 51. Przekształcanie listy numerów telefonów Napisz funkcję, która mając listę numerów telefonów, przekształca je w taki sposób, by wszystkie rozpoczynały się od określonego numeru kierunkowego poprzedzonego znakiem +. Wszelkie znaki białe występujące w pierwotnym numerze telefonu powinny zostać usunięte. Poniżej przedstawiono przykładową listę wejściową i wyjściową: 07555 123456 07555123456 +48 7555 123456 48 7555 123456 7555 123456 => => => => => +487555123456 +487555123456 +487555123456 +487555123456 +487555123456 52. Generowanie wszystkich permutacji ciągu znaków Utwórz funkcję, która wyświetla w konsoli wszystkie możliwe permutacje danego łańcucha. Powinieneś udostępnić dwie wersje tej funkcji — z wykorzystaniem rekurencji i bez niej. 53. Średnia ocena filmów Napisz program, który oblicza i wyświetla średnią ocenę listy filmów. Każdy film może być oceniany w zakresie od 1 do 10 (przy czym 1 oznacza ocenę najniższą, a 10 — najwyższą). Przed obliczeniem średniej ocen powinieneś usunąć 5% najwyższych i najniższych ocen. Wynik musi być wyświetlany z dokładnością do jednego miejsca po przecinku. 54. Algorytm tworzenia par Utwórz funkcję ogólnego przeznaczenia, która mając podany zestaw danych, zwraca nowy zestaw, zawierający pary kolejnych elementów pochodzących ze zbioru wejściowego. Jeśli zestaw danych wejściowych ma nieparzystą liczbę elementów, ostatni z nich musi zostać zignorowany. Na przykład jeśli zbiór wejściowy jest równy {1, 1, 3, 5, 8, 13, 21}, w wyniku powinniśmy uzyskać {{1, 1}, {3, 5}, {8, 13}}. 91 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 55. Algorytm scalania Napisz funkcję, która mając podane dwa zestawy danych, zwraca nowy zbiór zawierający pary elementów pochodzące z tych dwóch zestawów. Jeśli oba zestawy mają różne rozmiary, wynik musi zawierać tyle elementów, ile zawiera najmniejszy z nich. Na przykład jeżeli zestawy danych wejściowych są następujące: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} i {1, 1, 3, 5, 8, 13, 21}, wówczas wynik powinien być równy {{1,1}, {2,1}, {3,3}, {4,5}, {5,8}, {6,13}, {7,21}}. 56. Algorytm wyboru Utwórz funkcję, która biorąc pod uwagę zestaw danych oraz funkcję rzutowania, przekształca każdą wartość na nową oraz zwraca zbiór z wybranymi danymi. Na przykład jeśli zdefiniowałeś typ danych o nazwie book, który zawiera pola id, title i author, a także posiadasz zbiór wartości o typie book, funkcja powinna mieć możliwość wyboru tylko tytułu książki (zwracać title). Oto przykład użycia tej funkcji: struct book { int id; std::string title; std::string author; }; std::vector<book> books{ {101, "Język C++", "Bjarne Stroustrup"}, {203, "Skuteczny nowoczesny C++", "Scott Meyers"}, {404, "Nowoczesne wyzwania języka C++", "Marius Bancila"}}; auto titles = select(books, [](book const & b) {return b.title; }); 57. Algorytm sortowania Utwórz funkcję, która wykorzystując parę iteratorów dostępu swobodnego do zdefiniowania początku i końca zbioru danych, sortuje jego elementy za pomocą algorytmu quicksort. Powinny istnieć dwa przeciążenia funkcji sortowania: pierwsze, które używa operatora < do porównywania elementów zbioru, a następnie umieszcza je w porządku rosnącym, i drugie, które wykorzystuje funkcję porównania binarnego zdefiniowaną przez użytkownika do porównywania elementów. 58. Najkrótsza ścieżka między węzłami Napisz program, który mając podaną sieć węzłów oraz odległości między nimi, oblicza i wyświetla najkrótsze odległości od określonego węzła do wszystkich pozostałych, a także ścieżkę między początkowym i końcowym węzłem. Jako dane wejściowe rozważ następujący graf nieskierowany: 92 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Wynik programu dla powyższego grafu powinien być następujący: A A A A A A -> -> -> -> -> -> A B C D E F : : : : : : 0 7 9 20 20 11 A A A A A A -> -> -> -> -> B C C -> D C -> F -> E C -> F 59. Program Weasel Napisz program, który implementuje symulację komputerową Weasel autorstwa Richarda Dawkinsa, opisaną jego słowami w następujący sposób („Ślepy zegarmistrz”, rozdział 3.): Ponownie wykorzystujemy naszą komputerową małpę, ale z zasadniczą różnicą w jej programie. Jeszcze raz zaczyna ona od wybrania losowej sekwencji 28 liter, tak jak poprzednio […] wielokrotnie ją duplikuje, lecz z pewną możliwością wystąpienia przypadkowego błędu („mutacji”) w trakcie kopiowania. Komputer analizuje zmutowane nonsensowne frazy — „potomstwo” frazy pierwotnej, a następnie wybiera tę z nich, która, choć nieznacznie, najbardziej przypomina frazę docelową: METHINKS IT IS LIKE A WEASEL. 60. Gra w życie Utwórz program, który implementuje automat komórkowy Gra w życie zaproponowany przez Johna Hortona Conwaya. Wszechświat tej gry składa się z siatki kwadratowych komórek, które mogą mieć jeden z dwóch stanów: martwy lub żywy. Każda komórka współdziała ze swoimi najbliższymi sąsiadami. Podczas każdej iteracji brane są pod uwagę następujące zasady: 93 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Każda żywa komórka z mniej niż dwoma żywymi sąsiadami umiera, jakby z powodu wyludnienia. Każda żywa komórka z dwoma lub trzema żywymi sąsiadami przeżywa dla następnego pokolenia. Każda żywa komórka z więcej niż trzema żywymi sąsiadami umiera, jakby z powodu przeludnienia. Każda martwa komórka z dokładnie trzema żywymi sąsiadami staje się żywą komórką, jak gdyby przez rozmnażanie. Stan gry w każdej iteracji powinien być wyświetlany w konsoli. Dla wygody powinieneś wybrać rozsądny rozmiar siatki, na przykład 20 wierszy na 50 kolumn. 94 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Rozwiązania 45. Kolejka priorytetowa Kolejka priorytetowa jest abstrakcyjnym typem danych, którego elementy mają przypisany priorytet. Zamiast działać jako kontener FIFO, kolejka priorytetowa udostępnia elementy w kolejności ich priorytetów. Ta struktura danych jest używana w takich algorytmach, jak najkrótsza ścieżka Dijkstry, algorytm Prima, sortowanie sterty, algorytm wyszukiwania A*, kody Huffmana stosowane do kompresji danych, i w innych. Bardzo prostym podejściem do implementacji kolejki priorytetowej byłoby użycie typu std::vector jako podstawowego kontenera elementów i dbanie o to, by zawsze były one posortowane. Oznacza to, że elementy o wartościach maksymalnych i minimalnych powinny się zawsze znajdować na dwóch końcach kolejki. Jednak taka metoda nie zapewnia najlepszej wydajności podczas wykonywania operacji. Najbardziej odpowiednią strukturą danych, którą można wykorzystać do zaimplementowania kolejki priorytetowej, jest sterta. Jest to struktura danych oparta na drzewach, która charakteryzuje się następującą właściwością: jeśli P jest węzłem nadrzędnym dla C, to klucz (wartość) P jest w przypadku sterty typu max większy lub równy kluczowi C, a w przypadku sterty typu min — mniejszy lub równy kluczowi C. Standardowa biblioteka udostępnia kilka operacji pozwalających na obsługę sterty: std::make_heap(): tworzy stertę typu max dla podanego zbioru danych, używając w celu porządkowania elementów operatora < lub funkcji porównującej podanej przez użytkownika; std::push_heap(): umieszcza nowy element na końcu sterty typu max; std::pop_heap(): usuwa pierwszy element sterty (zamieniając wartości dla pierwszej i ostatniej pozycji oraz czyniąc podzbiór [pierwszy, ostatni-1) stertą typu max). Implementacja kolejki priorytetowej, która wykorzystuje typ std::vector do przechowywania danych i standardowe funkcje obsługi sterty, może mieć taką postać: template <class T, class Compare = std::less<typename std::vector<T>::value_type>> class priority_queue { typedef typename std::vector<T>::value_type value_type; typedef typename std::vector<T>::size_type size_type; typedef typename std::vector<T>::reference reference; typedef typename std::vector<T>::const_reference const_reference; public: bool empty() const noexcept { return data.empty(); } size_type size() const noexcept { return data.size(); } 95 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów void push(value_type const & value) { data.push_back(value); std::push_heap(std::begin(data), std::end(data), comparer); } void pop() { std::pop_heap(std::begin(data), std::end(data), comparer); data.pop_back(); } const_reference top() const { return data.front(); } void swap(priority_queue& other) noexcept { swap(data, other.data); swap(comparer, other.comparer); } private: std::vector<T> data; Compare comparer; }; template<class T, class Compare> void swap(priority_queue<T, Compare>& lhs, priority_queue<T, Compare>& rhs) noexcept(noexcept(lhs.swap(rhs))) { lhs.swap(rhs); } Powyższa klasa może zostać użyta w następujący sposób: int main() { priority_queue<int> q; for (int i : {1, 5, 3, 1, 13, 21, 8}) { q.push(i); } assert(!q.empty()); assert(q.size() == 7); while (!q.empty()) { std::cout << q.top() << ' '; q.pop(); } } 96 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych 46. Bufor cykliczny Bufor cykliczny to kontener o stałym rozmiarze, który zachowuje się w taki sposób, jakby jego dwa końce zostały połączone w celu utworzenia wirtualnego systemu z pamięcią cykliczną. Jego główną zaletą jest to, że nie potrzebujesz dużej ilości pamięci do przechowywania danych, ponieważ starsze wpisy są nadpisywane nowszymi. Bufory cykliczne są używane w buforowaniu wejścia i wyjścia, logowaniu ograniczonym (gdy chcemy zachowywać tylko ostatnie komunikaty), buforowaniu podczas przetwarzania asynchronicznego i innych przypadkach. Możemy rozróżnić dwie sytuacje: 1. Liczba elementów dodanych do bufora nie osiągnęła jego pojemności (stałego rozmiaru zdefiniowanego przez użytkownika). W tym przypadku bufor zachowuje się jak zwykły kontener, taki jak wektor. 2. Liczba elementów dodanych do bufora osiągnęła i przekroczyła jego pojemność. W takim przypadku pamięć bufora jest ponownie wykorzystywana, a starsze elementy są nadpisywane. Moglibyśmy reprezentować taką strukturę za pomocą: standardowego kontenera z wstępnie przydzieloną liczbą elementów, wskaźnika (head) pokazującego pozycję ostatnio wstawionego elementu, licznika rozmiaru (size) określającego liczbę elementów w kontenerze, która nie może przekroczyć jego pojemności (ponieważ wówczas elementy będą nadpisywane). Istnieją dwie podstawowe operacje wykonywane na buforze cyklicznym: Dodawanie nowego elementu do bufora. Element wstawiamy zawsze w pozycji znajdującej się za wskaźnikiem (lub inaczej mówiąc, indeksem). Jest to metoda push() pokazana poniżej. Usuwanie istniejącego elementu z bufora. Zawsze usuwamy element najstarszy. Ten element znajduje się w pozycji head - size (operacja musi uwzględniać cykliczną naturę indeksu). Jest to metoda pop() zaprezentowana poniżej. Oto implementacja powyżej zdefiniowanej struktury danych: template <class T> class circular_buffer { typedef circular_buffer_iterator<T> const_iterator; circular_buffer() = delete; public: explicit circular_buffer(size_t const size) :data_(size) {} bool clear() noexcept { head_ = -1; size_ = 0; } bool empty() const noexcept { return size_ == 0; } bool full() const noexcept { return size_ == data_.size(); } 97 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów size_t capacity() const noexcept { return data_.size(); } size_t size() const noexcept { return size_; } void push(T const item) { head_ = next_pos(); data_[head_] = item; if (size_ < data_.size()) size_++; } T pop() { if (empty()) throw std::runtime_error("bufor pusty"); auto pos = first_pos(); size_--; return data_[pos]; } const_iterator begin() const { return const_iterator(*this, first_pos(), empty()); } const_iterator end() const { return const_iterator(*this, next_pos(), true); } private: std::vector<T> data_; size_t head_ = -1; size_t size_ = 0; size_t next_pos() const noexcept { return size_ == 0 ? 0 : (head_ + 1) % data_.size(); } size_t first_pos() const noexcept { return size_ == 0 ? 0 : (head_ + data_.size() - size_ + 1) % data_.size(); } friend class circular_buffer_iterator<T>; }; Ze względu na cykliczną naturę indeksów odwzorowywanych na ciągły układ pamięci typ iteratora dla tej klasy nie może być typem wskaźnikowym. Iteratory muszą mieć możliwość wskazywania elementów poprzez zastosowanie operacji modulo na indeksie. Oto możliwa implementacja takiego iteratora: template <class T> class circular_buffer_iterator { typedef circular_buffer_iterator typedef T typedef T& typedef T const& typedef T* self_type; value_type; reference; const_reference; pointer; 98 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych typedef std::random_access_iterator_tag iterator_category; typedef ptrdiff_t difference_type; public: circular_buffer_iterator(circular_buffer<T> const & buf, size_t const pos, bool const last) : buffer_(buf), index_(pos), last_(last) {} self_type & operator++ () { if (last_) throw std::out_of_range("Iterator nie może zostać zwiększony poza koniec zakresu danych."); index_ = (index_ + 1) % buffer_.data_.size(); last_ = index_ == buffer_.next_pos(); return *this; } self_type operator++ (int) { self_type tmp = *this; ++*this; return tmp; } bool operator== (self_type const & other) const { assert(compatible(other)); return index_ == other.index_ && last_ == other.last_; } bool operator!= (self_type const & other) const { return !(*this == other); } const_reference operator* () const { return buffer_.data_[index_]; } const_reference operator-> () const { return buffer_.data_[index_]; } private: bool compatible(self_type const & other) const { return &buffer_ == &other.buffer_; } circular_buffer<T> const & buffer_; size_t index_; bool last_; }; 99 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Po zaimplementowaniu wszystkich wymagań możemy utworzyć przykładowy kod zaprezentowany poniżej. Pamiętaj, że pierwszy zbiór podany w komentarzach reprezentuje rzeczywistą zawartość wektora wewnętrznego, a drugi jest zawartością logiczną dostępną dla iteratora: int main() { circular_buffer<int> cbuf(5); // {0, 0, 0, 0, 0} -> {} cbuf.push(1); cbuf.push(2); cbuf.push(3); // {1, 0, 0, 0, 0} -> {1} // {1, 2, 0, 0, 0} -> {1, 2} // {1, 2, 3, 0, 0} -> {1, 2, 3} auto item = cbuf.pop(); cbuf.push(4); cbuf.push(5); cbuf.push(6); cbuf.push(7); cbuf.push(8); // {1, 2, 3, 0, 0} -> {2, 3} // {1, 2, 3, 4, 0} -> {2, 3, 4} // {1, 2, 3, 4, 5} -> {2, 3, 4, 5} // {6, 2, 3, 4, 5} -> {2, 3, 4, 5, 6} // {6, 7, 3, 4, 5} -> {3, 4, 5, 6, 7} // {6, 7, 8, 4, 5} -> {4, 5, 6, 7, 8} item = cbuf.pop(); item = cbuf.pop(); item = cbuf.pop(); // {6, 7, 8, 4, 5} -> {5, 6, 7, 8} // {6, 7, 8, 4, 5} -> {6, 7, 8} // {6, 7, 8, 4, 5} -> {7, 8} item = cbuf.pop(); item = cbuf.pop(); // {6, 7, 8, 4, 5} -> {8} // {6, 7, 8, 4, 5} -> {} cbuf.push(9); // {6, 7, 8, 9, 5} -> {9} } 47. Podwójne buforowanie Zadanie przedstawione tutaj jest typową sytuacją związaną z podwójnym buforowaniem. Takie buforowanie jest najczęstszym przypadkiem wielokrotnego buforowania, czyli metodą, która umożliwia wątkowi czytającemu zobaczenie pełnej wersji danych, a nie wersji częściowo już zaktualizowanej przez program piszący. Jest to powszechnie stosowana technika — szczególnie w grafice komputerowej — w celu uniknięcia migotania ekranu. Aby zaimplementować wymaganą funkcjonalność, klasa bufora, którą powinniśmy utworzyć, musi mieć dwa bufory wewnętrzne: jeden zawierający dane tymczasowe, a drugi zawierający dane gotowe (lub zatwierdzone). Po zakończeniu operacji zapisu zawartość bufora tymczasowego jest zapisywana w buforze podstawowym. W przypadku buforów wewnętrznych poniższa implementacja wykorzystuje strukturę std::vector. Zamiast po ukończeniu operacji zapisu kopiować dane z jednego bufora do drugiego, po prostu wymieniamy wzajemnie ich zawartości, co jest znacznie szybszą techniką. Do gotowych danych można uzyskać dostęp za pomocą funkcji read(), która przesyła zawartość bufora odczytu na określone wyjście lub pozwala na bezpośredni dostęp do elementu (przeciążony operator []). Dostęp do bufora odczytu jest synchronizowany za pomocą mechanizmu wykluczania std::mutex, aby umożliwić bezpieczne odczytanie przy użyciu jednego wątku, podczas gdy drugi w tym czasie zapisuje w buforze: 100 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych template <typename T> class double_buffer { typedef T value_type; typedef T& reference; typedef T const & const_reference; typedef T* pointer; public: explicit double_buffer(size_t const size) : rdbuf(size), wrbuf(size) {} size_t size() const noexcept { return rdbuf.size(); } void write(T const * const ptr, { std::unique_lock<std::mutex> auto length = std::min(size, std::copy(ptr, ptr + length, wrbuf.swap(rdbuf); } size_t const size) lock(mt); wrbuf.size()); std::begin(wrbuf)); template <class Output> void read(Output it) const { std::unique_lock<std::mutex> lock(mt); std::copy(std::cbegin(rdbuf), std::cend(rdbuf), it); } pointer data() const { std::unique_lock<std::mutex> lock(mt); return rdbuf.data(); } reference operator[](size_t const pos) { std::unique_lock<std::mutex> lock(mt); return rdbuf[pos]; } const_reference operator[](size_t const pos) const { std::unique_lock<std::mutex> lock(mt); return rdbuf[pos]; } void swap(double_buffer other) { std::swap(rdbuf, other.rdbuf); std::swap(wrbuf, other.wrbuf); } private: std::vector<T> rdbuf; std::vector<T> wrbuf; mutable std::mutex mt; }; 101 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów W poniższym kodzie zaprezentowano użycie klasy podwójnego buforowania w celu zapisywania i odczytywania danych przez dwa różne wątki: template <typename T> void print_buffer(double_buffer<T> const & buf) { buf.read(std::ostream_iterator<T>(std::cout, " ")); std::cout << std::endl; } int main() { double_buffer<int> buf(10); std::thread t([&buf]() { for (int i = 1; i < 1000; i += 10) { int data[] = { i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6,i + 7,i + 8,i + 9 }; buf.write(data, 10); using namespace std::chrono_literals; std::this_thread::sleep_for(100ms); } }); auto start = std::chrono::system_clock::now(); do { print_buffer(buf); using namespace std::chrono_literals; std::this_thread::sleep_for(150ms); } while (std::chrono::duration_cast<std::chrono::seconds>( std::chrono::system_clock::now() - start).count() < 12); t.join(); } 48. Najczęściej występujący element w zbiorze danych Aby ustalić i zwrócić najczęściej występujący element w zbiorze danych, wykonaj takie czynności: Policz wystąpienia każdego elementu i umieść wyniki w strukturze std::map. Klucz jest elementem, a wartość liczbą wystąpień. Ustal element mapy o maksymalnej wartości za pomocą metody std::max_element(). Wynikiem jest element mapy, czyli para zawierająca klucz i liczbę jego wystąpień. Skopiuj te elementy mapy, których wartość (liczba wystąpień) jest równa wartości maksymalnego elementu, i zwróć uzyskany zbiór jako wynik końcowy. 102 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Implementacja założeń opisanych powyżej została zaprezentowana na następującym listingu: template <typename T> std::vector<std::pair<T, size_t>> find_most_frequent( std::vector<T> const & range) { std::map<T, size_t> counts; for (auto const & e : range) counts[e]++; auto maxelem = std::max_element( std::cbegin(counts), std::cend(counts), [](auto const & e1, auto const & e2) { return e1.second < e2.second; }); std::vector<std::pair<T, size_t>> result; std::copy_if( std::begin(counts), std::end(counts), std::back_inserter(result), [maxelem](auto const & kvp) { return kvp.second == maxelem->second; }); } return result; Funkcja find_most_frequent() może zostać użyta w poniższy sposób: int main() { auto range = std::vector<int>{1,1,3,5,8,13,3,5,8,8,5}; auto result = find_most_frequent(range); } for (auto const & e : result) { std::cout << e.first << " : " << e.second << std::endl; } 49. Histogram tekstu Histogram jest reprezentacją rozkładu danych liczbowych. Dobrze znanymi przykładami są histogramy kolorów i obrazów używane w fotografii oraz przetwarzaniu grafiki. Histogram tekstowy opisany w tej książce jest reprezentacją częstości występowania liter w danym tekście. Niniejsze zadanie jest częściowo podobne do poprzedniego, z tą różnicą, że elementy zbioru danych są obecnie znakami i należy określić ich częstość występowania. Aby rozwiązać to zadanie, powinieneś wykonać następujące działania: Obliczyć wystąpienia każdej z liter i zapisać je przy użyciu mapy. Klucz jest literą, a wartość liczbą wystąpień. Podczas obliczeń ignorować wszystkie znaki, które nie są literami. Wielkie i małe litery muszą być traktowane identycznie, ponieważ reprezentują ten sam znak. 103 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Użyć metody std::accumulate(), aby obliczyć całkowitą liczbę wystąpień wszystkich liter w podanym tekście. Zastosować algorytm std::for_each() lub pętlę opartą na zakresie danych, aby przetworzyć wszystkie elementy mapy i przekształcić liczbę wystąpień na częstość. Poniżej zaprezentowano możliwą implementację rozwiązania zadania: std::map<char, double> analyze_text(std::string_view text) { std::map<char, double> frequencies; for (char ch = 'a'; ch <= 'z'; ch++) frequencies[ch] = 0; for (auto ch : text) { if (isalpha(ch)) frequencies[tolower(ch)]++; } auto total = std::accumulate( std::cbegin(frequencies), std::cend(frequencies), 0ull, [](auto sum, auto const & kvp) { return sum + static_cast<unsigned long long>(kvp.second); }); std::for_each( std::begin(frequencies), std::end(frequencies), [total](auto & kvp) { kvp.second = (100.0 * kvp.second) / total; }); return frequencies; } Przy użyciu następującego programu wyświetlamy w konsoli częstość występowania liter w tekście: int main() { auto result = analyze_text(R"(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.)"); for (auto const { std::cout << << << << } & kvp : result) kvp.first << " : " std::fixed std::setw(5) << std::setfill(' ') std::setprecision(2) << kvp.second << std::endl; } 104 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych 50. Filtrowanie listy numerów telefonów Rozwiązanie tego zadania jest stosunkowo proste: musisz przetworzyć wszystkie numery telefonów i skopiować do osobnego kontenera (na przykład std::vector) numery telefonów rozpoczynające się od kodu kraju. Jeśli podanym kodem kraju jest na przykład 48, musisz sprawdzić zarówno 48, jak i +48. Takie filtrowanie zbioru wejściowego jest możliwe przy użyciu funkcji std::copy_if (). Oto przykładowe rozwiązanie zadania: bool starts_with(std::string_view str, std::string_view prefix) { return str.find(prefix) == 0; } template <typename InputIt> std::vector<std::string> filter_numbers(InputIt begin, InputIt end, std::string const & countryCode) { std::vector<std::string> result; std::copy_if( begin, end, std::back_inserter(result), [countryCode](auto const & number) { return starts_with(number, countryCode) || starts_with(number, "+" + countryCode); }); return result; } std::vector<std::string> filter_numbers( std::vector<std::string> const & numbers, std::string const & countryCode) { return filter_numbers(std::cbegin(numbers), std::cend(numbers), countryCode); } Powyższa funkcja może zostać użyta w następujący sposób: int main() { std::vector<std::string> numbers{ "+40744909080", "48 7520 112233", "+48 7555 123456", "40 7200 123456", "7555 123456" }; auto result = filter_numbers(numbers, "48"); } for (auto const & number : result) { std::cout << number << std::endl; } 105 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 51. Przekształcanie listy numerów telefonów To zadanie jest nieco podobne w pewnych aspektach do poprzedniego. Zamiast jednak wybierać numery telefonów zaczynające się od określonego kodu kraju, należy przekształcić każdy numer w taki sposób, by wszystkie zaczynały się od kodu kraju poprzedzonego znakiem +. Istnieje kilka zagadnień, które należy wziąć pod uwagę: Numer telefonu rozpoczyna się od cyfry 0. Oznacza to liczbę bez kodu kraju. Aby zmodyfikować numer w celu uwzględnienia kodu kraju, musisz zastąpić 0 rzeczywistym kodem kraju poprzedzonym znakiem +. Numer telefonu rozpoczyna się od kodu kraju. W tym przypadku po prostu należy umieścić znak + na samym początku. Numer telefonu rozpoczyna się od znaku +, po którym następuje kod kraju. Wówczas liczba ma już oczekiwany format. Żaden z powyższych przypadków nie ma zastosowania, dlatego wynik uzyskuje się przez połączenie znaku +, kodu kraju oraz numeru telefonu. Dla uproszczenia zignorujemy sytuację, w której numer telefonu jest poprzedzony kodem innego kraju. Kolejnym ćwiczeniem mogłoby być takie zmodyfikowanie obecnej implementacji, by program mógł obsługiwać numery telefonów zawierające prefiks innego kraju. Takie numery powinny zostać usunięte z listy. Dla każdego z omawianych przypadków numer telefonu może zawierać spacje. Zgodnie z podanymi wymaganiami muszą one zostać usunięte. W tym celu używane będą funkcje std::remove_if() i isspace(). Poniżej przedstawiono implementację omawianego rozwiązania: bool starts_with(std::string_view str, std::string_view prefix) { return str.find(prefix) == 0; } void normalize_phone_numbers(std::vector<std::string>& numbers, std::string const & countryCode) { std::transform( std::cbegin(numbers), std::cend(numbers), std::begin(numbers), [countryCode](std::string const & number) { std::string result; if (number.size() > 0) { if (number[0] == '0') result = "+" + countryCode + number.substr(1); else if (starts_with(number, countryCode)) result = "+" + number; 106 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych else if (starts_with(number, "+" + countryCode)) result = number; else result = "+" + countryCode + number; } result.erase( std::remove_if(std::begin(result), std::end(result), [](const char ch) {return isspace(ch); }), std::end(result)); return result; }); } Poniższy program normalizuje zgodnie z wymaganiami podaną listę numerów telefonów i wyświetla ją w konsoli: int main() { std::vector<std::string> numbers{ "07555 123456", "07555123456", "+48 7555 123456", "48 7555 123456", "7555 123456" }; normalize_phone_numbers(numbers, "48"); for (auto const & number : numbers) { std::cout << number << std::endl; } } 52. Generowanie wszystkich permutacji ciągu znaków To zadanie możesz rozwiązać, korzystając z niektórych algorytmów ogólnego zastosowania zawartych w bibliotece standardowej. Najprostsza z dwóch wymaganych wersji jest nierekurencyjna, przynajmniej wówczas, gdy używasz algorytmu std::next_permutation(). Funkcja przekształca dane wejściowe (które należy posortować) na kolejną permutację należącą do zbioru wszystkich możliwych permutacji, uporządkowaną leksykograficznie za pomocą operatora < lub określonego obiektu funkcji porównania. Jeśli taka permutacja istnieje, funkcja zwraca wartość true; w przeciwnym razie przekształca zbiór danych w pierwszą permutację i zwraca wartość false. Wynika stąd, że nierekurencyjna implementacja oparta na algorytmie std::next_permutation() będzie następująca: void print_permutations(std::string str) { std::sort(std::begin(str), std::end(str)); 107 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów do { std::cout << str << std::endl; } while (std::next_permutation(std::begin(str), std::end(str))); } Alternatywna wersja rekurencyjna jest nieco bardziej złożona. Jednym ze sposobów implementacji jest użycie łańcucha wejściowego i wyjściowego. Na początku łańcuch wejściowy jest ciągiem znaków, dla którego chcemy generować permutacje, a łańcuch wyjściowy jest pusty. Pobieramy po jednym znaku z łańcucha wejściowego i umieszczamy go w łańcuchu wyjściowym. Kiedy łańcuch wejściowy stanie się pusty, łańcuch wyjściowy będzie reprezentować następną permutację. Odpowiedni algorytm rekurencyjny jest następujący: Jeśli łańcuch wejściowy jest pusty, wyświetl ciąg wyjściowy i wróć. W przeciwnym razie przetwórz iteracyjnie wszystkie znaki w łańcuchu wejściowym i dla każdego znalezionego elementu: wywołaj metodę rekurencyjnie, usuwając pierwszy znak z łańcucha wejściowego i dołączając go na koniec łańcucha wyjściowego; wykonaj rotację łańcucha wejściowego w taki sposób, aby pierwszy znak stał się ostatnim, drugi stał się pierwszym itd. Powyższy algorytm został wizualnie przedstawiony na następującym rysunku: W celu rotacji łańcucha wejściowego możemy użyć standardowej funkcji bibliotecznej std::rotate(), która wykonuje rotację w lewo dla określonego zakresu elementów. Implementacja opisanego algorytmu rekurencyjnego ma taką postać: void next_permutation(std::string str, std::string perm) { if (str.empty()) std::cout << perm << std::endl; else { for (size_t i = 0; i < str.size(); ++i) { next_permutation(str.substr(1), perm + str[0]); 108 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych std::rotate(std::begin(str), std::begin(str) + 1, std::end(str)); } } } void print_permutations_recursive(std::string str) { next_permutation(str, ""); } W ten sposób można użyć obu wersji funkcji: int main() { setlocale(LC_ALL, "polish"); std::cout << "wersja nierekurencyjna" << std::endl; print_permutations("lato"); std::cout << "wersja rekurencyjna" << std::endl; print_permutations_recursive("lato"); } 53. Średnia ocena filmów Zadanie wymaga wyznaczenia oceny filmu za pomocą średniej ucinanej. Jest to miara statystyczna tendencji centralnej, dla której średnia jest obliczana po odrzuceniu części rozkładu prawdopodobieństwa lub najbardziej ekstremalnych próbek. Zazwyczaj odbywa się to poprzez usunięcie równej liczby punktów na obu krańcach. Aby rozwiązać to zadanie, musisz usunąć 5% zarówno najwyższych, jak i najniższych ocen użytkowników. Funkcja, która oblicza średnią ucinaną dla podanego zakresu, powinna wykonać następujące działania: posortować zakres danych tak, aby elementy zostały uporządkowane (rosnąco lub malejąco); usunąć wymagany procent elementów na obu krańcach; obliczyć sumę wszystkich pozostałych elementów; obliczyć średnią, dzieląc sumę przez pozostałą liczbę elementów. Powyżej opisany algorytm jest implementowany przez przedstawioną tutaj funkcję truncated_mean() : double truncated_mean(std::vector<int> values, double const percentage) { std::sort(std::begin(values), std::end(values)); auto remove_count = static_cast<size_t>( values.size() * percentage + 0.5); values.erase(std::begin(values), std::begin(values) + remove_count); 109 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów values.erase(std::end(values) - remove_count, std::end(values)); auto total = std::accumulate( std::cbegin(values), std::cend(values), 0ull, [](auto const sum, auto const e) { return sum + e; }); return static_cast<double>(total) / values.size(); } Program, który wykorzystuje tę funkcję do obliczania i wyświetlania średniej oceny filmów, może mieć taką postać: struct movie { int id; std::string title; std::vector<int> ratings; }; void print_movie_ratings(std::vector<movie> const & movies) { for (auto const & m : movies) { std::cout << m.title << " : " << std::fixed << std::setprecision(1) << truncated_mean(m.ratings, 0.05) << std::endl; } } int main() { std::vector<movie> movies { { 101, "Matrix",{ 10, 9, 10, 9, 9, 8, 7, 10, 5, 9, 9, 8 } }, { 102, "Gladiator",{ 10, 5, 7, 8, 9, 8, 9, 10, 10, 5, 9, 8, 10 } }, { 103, "Interstellar",{ 10, 10, 10, 9, 3, 8, 8, 9, 6, 4, 7, 10 } } }; print_movie_ratings(movies); } 54. Algorytm tworzenia par Funkcja tworzenia par zaproponowana dla tego zadania musi łączyć sąsiednie elementy zakresu wejściowego i tworzyć zestawy std::pair, które będą dodawane do zakresu wyjściowego. Na poniżej przedstawionym listingu zostaną zaprezentowane dwie implementacje: Ogólny szablon funkcji, który wykorzystuje iteratory jako argumenty: iterator początkowy i końcowy definiują zakres wejściowy, a iterator wyjściowy określa położenie w zakresie wyjściowym, do którego powinny zostać wstawione wyniki. 110 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Przeciążenie funkcji, które wykorzystuje typ std::vector<T> jako argument wejściowy i zwraca std::vector<std::pair<T, T>> jako wynik. To przeciążenie wywołuje po prostu pierwszą funkcję: template <typename Input, typename Output> void pairwise(Input begin, Input end, Output result) { auto it = begin; while (it != end) { auto v1 = *it++; if (it == end) break; auto v2 = *it++; result++ = std::make_pair(v1, v2); } } template <typename T> std::vector<std::pair<T, T>> pairwise(std::vector<T> const & range) { std::vector<std::pair<T, T>> result; pairwise(std::begin(range), std::end(range), std::back_inserter(result)); return result; } Poniższy program łączy w pary elementy wektora liczb całkowitych i wyświetla je w konsoli: int main() { std::vector<int> v{ 1, 1, 3, 5, 8, 13, 21 }; auto result = pairwise(v); for (auto const & p : result) { std::cout << '{' << p.first << ',' << p.second << '}' << std::endl; } } 55. Algorytm scalania To zadanie jest dość podobne do poprzedniego, chociaż obecnie mamy do czynienia z dwoma zakresami wejściowymi zamiast tylko z jednym. Rezultatem jest kolejny raz zbiór struktur typu std::pair. Dwa zakresy wejściowe mogą jednak zawierać elementy różnych typów. Pokazana poniżej implementacja zawiera ponownie dwa przeciążenia: Funkcję ogólnego zastosowania, która wykorzystuje iteratory jako argumenty. Iterator początkowy i końcowy definiują zakresy wejściowe, a iterator wyjściowy określa położenie w zakresie wyjściowym, do którego powinny zostać wstawione wyniki. Przeciążenie funkcji, które wykorzystuje dwa argumenty typu std::vector<T> (jeden, który przechowuje elementy typu T, oraz drugi, służący do przechowywania elementów typu U) i zwraca std::vector<std::pair<T, U>>. To przeciążenie wywołuje po prostu pierwszą funkcję: 111 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów template <typename Input1, typename Input2, typename Output> void zip(Input1 begin1, Input1 end1, Input2 begin2, Input1 end2, Output result) { auto it1 = begin1; auto it2 = begin2; while (it1 != end1 && it2 != end2) { result++ = std::make_pair(*it1++, *it2++); } } template <typename T, typename U> std::vector<std::pair<T, U>> zip( std::vector<T> const & range1, std::vector<U> const & range2) { std::vector<std::pair<T, U>> result; zip(std::begin(range1), std::end(range1), std::begin(range2), std::end(range2), std::back_inserter(result)); return result; } Program pokazany na poniższym listingu wykonuje scalanie dwóch wektorów liczb całkowitych oraz wyświetla wynik w konsoli: int main() { std::vector<int> v1{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; std::vector<int> v2{ 1, 1, 3, 5, 8, 13, 21 }; auto result = zip(v1, v2); for (auto const & p : result) { std::cout << '{' << p.first << ',' << p.second << '}' << std::endl; } } 56. Algorytm wyboru Funkcja select(), którą musisz zaimplementować, używa jako argumentów wejściowych struktury std::vector<T> oraz funkcji typu F, a zwraca std::vector<R>, gdzie R jest wynikiem zastosowania funkcji F dla typu T. Moglibyśmy użyć funkcji std::result_of(), aby w czasie kompilacji wywnioskować o typie zwracanym przez wywoływane wyrażenie. Wewnętrznie funkcja select() powinna użyć algorytmu std::transform() w celu przetworzenia elementów wektora wejściowego, zastosowania funkcji F dla każdego z nich oraz wstawienia wyniku do wektora wyjściowego. 112 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Na poniższym listingu przedstawiono implementację tej funkcji: template < typename T, typename A, typename F, typename R = typename std::decay<typename std::result_of< typename std::decay<F>::type&( typename std::vector<T, A>::const_reference)>::type>::type> std::vector<R> select(std::vector<T, A> const & c, F&& f) { std::vector<R> v; std::transform(std::cbegin(c), std::cend(c), std::back_inserter(v), std::forward<F>(f)); return v; } Funkcja ta może zostać użyta w następujący sposób: int main() { setlocale(LC_ALL, "polish"); std::vector<book> books{ {101, "Język C++", "Bjarne Stroustrup"}, {203, "Skuteczny nowoczesny C++", "Scott Meyers"}, {404, "Nowoczesne wyzwania języka C++", "Marius Bancila"} }; auto titles = select(books, [](book const & b) {return b.title; }); for (auto const & title : titles) { std::cout << title << std::endl; } } 57. Algorytm sortowania Sortowanie szybkie (ang. quicksort) to algorytm sortowania porównawczego dla elementów tablicy, dla której zdefiniowano porządek globalny. Jeśli zostanie dobrze zaimplementowane, będzie znacznie szybsze niż sortowanie przez scalanie (ang. merge sort) lub sortowanie stogowe (ang. heap sort). Chociaż w najgorszych przypadkach algorytm wykonuje O(n2) porównań (gdy zbiór danych jest już posortowany), jego średnia złożoność obliczeniowa wynosi tylko O(n · log(n)). Quicksort jest algorytmem typu „dziel i zwyciężaj”. Dzieli (partycjonuje) on duży zakres danych na mniejsze zakresy i sortuje je rekurencyjnie. Istnieje kilka schematów dzielenia. W wykorzystanej tu implementacji używamy oryginału opracowanego przez Tony’ego Hoare’a. Algorytm schematu został przedstawiony za pomocą poniższego pseudokodu: algorithm quicksort(A, lo, hi) is if lo < hi then p := partition(A, lo, hi) quicksort(A, lo, p) 113 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów quicksort(A, p + 1, hi) algorithm partition(A, lo, hi) is pivot := A[lo] i := lo - 1 j := hi + 1 loop forever do i := i + 1 while A[i] < pivot do j := j - 1 while A[j] > pivot if i >= j then return j swap A[i] with A[j] W implementacji algorytmu ogólnego zastosowania należy używać iteratorów, a nie tablic i indeksów. Nasza implementacja wymaga zastosowania iteratorów o dowolnym dostępie (pozwalają na odniesienie się do dowolnego elementu w stałym czasie): template <class RandomIt> RandomIt partition(RandomIt first, RandomIt last) { auto pivot = *first; auto i = first + 1; auto j = last - 1; while (i <= j) { while (i <= j && *i <= pivot) i++; while (i <= j && *j > pivot) j--; if (i < j) std::iter_swap(i, j); } std::iter_swap(i - 1, first); return i - 1; } template <class RandomIt> void quicksort(RandomIt first, RandomIt last) { if (first < last) { auto p = partition(first, last); quicksort(first, p); quicksort(p + 1, last); } } 114 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Przedstawiona poniżej funkcja quicksort() może służyć do sortowania różnego rodzaju kontenerów: int main() { std::vector<int> v{ 1,5,3,8,6,2,9,7,4 }; quicksort(std::begin(v), std::end(v)); std::array<int, 9> a{ 1,2,3,4,5,6,7,8,9 }; quicksort(std::begin(a), std::end(a)); int a[]{ 9,8,7,6,5,4,3,2,1 }; quicksort(std::begin(a), std::end(a)); } Zgodnie z wymaganiami algorytm sortowania powinien umożliwiać określanie funkcji porównującej zdefiniowanej przez użytkownika. W tym przypadku jedyna zmiana pojawi się w funkcji partycjonowania, w której zamiast użycia operatorów < i > w celu porównania bieżącego elementu z elementem rozdzielającym używamy zdefiniowanej przez użytkownika funkcji porównującej: template <class RandomIt, class Compare> RandomIt partitionc(RandomIt first, RandomIt last, Compare comp) { auto pivot = *first; auto i = first + 1; auto j = last - 1; while (i <= j) { while (i <= j && comp(*i, pivot)) i++; while (i <= j && !comp(*j, pivot)) j--; if (i < j) std::iter_swap(i, j); } std::iter_swap(i - 1, first); return i - 1; } template <class RandomIt, class Compare> void quicksort(RandomIt first, RandomIt last, Compare comp) { if (first < last) { auto p = partitionc(first, last, comp); quicksort(first, p, comp); quicksort(p + 1, last, comp); } } 115 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Mając tak przeciążoną funkcję, możemy posortować zbiór danych w porządku malejącym, jak pokazano w następującym przykładzie: int main() { std::vector<int> v{ 1,5,3,8,6,2,9,7,4 }; quicksort(std::begin(v), std::end(v), std::greater<>()); } Możliwe jest również zaimplementowanie iteracyjnej wersji algorytmu quicksort. Wydajność wersji iteracyjnej jest w większości sytuacji taka sama jak wersji rekurencyjnej (O(n · log(n))), lecz maleje do O(n2) w najgorszym przypadku, gdy zbiór jest już posortowany. Przekształcenie algorytmu z wersji rekurencyjnej na iteracyjną jest stosunkowo proste — odbywa się to za pomocą stosu emulującego wywołania rekurencyjne i przechowującego granice partycji. Poniżej przedstawiono iteracyjną wersję algorytmu, która używa operatora < do porównywania elementów: template <class RandomIt> void quicksorti(RandomIt first, RandomIt last) { std::stack<std::pair<RandomIt, RandomIt>> st; st.push(std::make_pair(first, last)); while (!st.empty()) { auto iters = st.top(); st.pop(); if (iters.second - iters.first < 2) continue; auto p = partition(iters.first, iters.second); st.push(std::make_pair(iters.first, p)); st.push(std::make_pair(p+1, iters.second)); } } Wersja iteracyjna może być używana podobnie jak jej rekurencyjny odpowiednik: int main() { std::vector<int> v{ 1,5,3,8,6,2,9,7,4 }; quicksorti(std::begin(v), std::end(v)); } 58. Najkrótsza ścieżka między węzłami Aby rozwiązać to zadanie, należy użyć algorytmu Dijkstry w celu znalezienia najkrótszej ścieżki w grafie. Chociaż pierwotny algorytm wyszukuje najkrótszą ścieżkę między dwoma danymi węzłami, wymaganie związane z niniejszym zadaniem polega na znalezieniu najkrótszej ścieżki w grafie między jednym określonym węzłem a wszystkimi pozostałymi, co sprawia, że mamy do czynienia z inną wersją algorytmu. 116 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Skuteczną metodą implementacji rozwiązania jest użycie kolejki priorytetowej. Odpowiedni pseudokod dla algorytmu (patrz https://pl.wikipedia.org/wiki/Algorytm_Dijkstry) ma taką postać: function Dijkstra(Graph, source): // inicjalizacja dist[source] 0 create vertex set Q for each vertex v in Graph: if v source // nieznana odległość między węzłem źródłowym a węzłem v dist[v] INFINITY prev[v] UNDEFINED // poprzednik v Q.add_with_priority(v, dist[v]) while Q is not empty: u Q.extract_min() for each neighbor v of u: alt dist[u] + length(u, if alt < dist[v] dist[v] alt prev[v] u Q.decrease_priority(v, // główna pętla // usuń i zwróć najlepszy węzeł // tylko węzeł v, który wciąż jest w kolejce Q v) alt) return dist[], prev[] Poniżej przedstawiona struktura danych pozwala na utworzenie reprezentacji grafu. Można jej używać dla grafów skierowanych i nieskierowanych. Klasa umożliwia dodawanie nowych wierzchołków i krawędzi, a także pozwala na zwracanie listy węzłów i sąsiadów określonego wierzchołka (czyli zarówno węzłów, jak i odległości do nich): template <typename Vertex = int, typename Weight = double> class graph { public: typedef Vertex vertex_type; typedef Weight weight_type; typedef std::pair<Vertex, Weight> neighbor_type; typedef std::vector<neighbor_type> neighbor_list_type; public: void add_edge(Vertex const source, Vertex const target, Weight const weight, bool const bidirectional = true) { adjacency_list[source].push_back(std::make_pair(target, weight)); adjacency_list[target].push_back(std::make_pair(source, weight)); } size_t vertex_count() const { return adjacency_list.size(); } std::vector<Vertex> verteces() const { std::vector<Vertex> keys; for (auto const & kvp : adjacency_list) keys.push_back(kvp.first); return keys; } neighbor_list_type const & neighbors(Vertex const & v) const { 117 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów auto pos = adjacency_list.find(v); if (pos == adjacency_list.end()) throw std::runtime_error("vertex not found"); return pos->second; } constexpr static Weight Infinity = std::numeric_limits<Weight>::infinity(); private: std::map<vertex_type, neighbor_list_type> adjacency_list; }; Implementacja algorytmu wyszukiwania najkrótszej ścieżki, który został przedstawiony we wcześniejszym pseudokodzie, może wyglądać tak. Zamiast kolejki priorytetowej zostaje użyta struktura std::set (czyli zrównoważone binarne drzewo wyszukiwań). Kontener std::set ma taką samą złożoność obliczeniową (o wartości O(log(n))) w przypadku dodawania i usuwania górnego elementu jak sterta binarna (używana w kolejce priorytetowej). Z drugiej strony std::set pozwala także na wyszukiwanie i usuwanie dowolnego innego elementu przy złożoności obliczeniowej również równej O(log(n)), co jest przydatne podczas implementacji procedury obniżania wartości klucza w czasie logarytmicznym poprzez usunięcie i ponowne wstawienie elementu: template <typename Vertex, typename Weight> void shortest_path( graph<Vertex, Weight> const & g, Vertex const source, std::map<Vertex, Weight>& min_distance, std::map<Vertex, Vertex>& previous) { auto const n = g.vertex_count(); auto const verteces = g.verteces(); min_distance.clear(); for (auto const & v : verteces) min_distance[v] = graph<Vertex, Weight>::Infinity; min_distance[source] = 0; previous.clear(); std::set<std::pair<Weight, Vertex> > vertex_queue; vertex_queue.insert(std::make_pair(min_distance[source], source)); while (!vertex_queue.empty()) { auto dist = vertex_queue.begin()->first; auto u = vertex_queue.begin()->second; vertex_queue.erase(std::begin(vertex_queue)); auto const & neighbors = g.neighbors(u); for (auto const & neighbor : neighbors) { auto v = neighbor.first; auto w = neighbor.second; 118 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych auto dist_via_u = dist + w; if (dist_via_u < min_distance[v]) { vertex_queue.erase(std::make_pair(min_distance[v], v)); min_distance[v] = dist_via_u; previous[v] = u; vertex_queue.insert(std::make_pair(min_distance[v], v)); } } } } Następujące funkcje pomocnicze pozwalają na wyświetlanie wyników w określonym formacie: template <typename Vertex> void build_path( std::map<Vertex, Vertex> const & prev, Vertex const v, std::vector<Vertex> & result) { result.push_back(v); auto pos = prev.find(v); if (pos == std::end(prev)) return; build_path(prev, pos->second, result); } template <typename Vertex> std::vector<Vertex> build_path(std::map<Vertex, Vertex> const & prev, Vertex const v) { std::vector<Vertex> result; build_path(prev, v, result); std::reverse(std::begin(result), std::end(result)); return result; } template <typename Vertex> void print_path(std::vector<Vertex> const & path) { for (size_t i = 0; i < path.size(); ++i) { std::cout << path[i]; if (i < path.size() - 1) std::cout << " -> "; } } Przykładowe rozwiązanie zadania zaprezentowano w poniższym programie: int main() { graph<char, double> g; g.add_edge('A', 'B', 7); g.add_edge('A', 'C', 9); 119 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów g.add_edge('A', g.add_edge('B', g.add_edge('B', g.add_edge('C', g.add_edge('C', g.add_edge('D', g.add_edge('E', 'F', 'C', 'D', 'D', 'F', 'E', 'F', 14); 10); 15); 11); 2); 6); 9); char source = 'A'; std::map<char, double> min_distance; std::map<char, char> previous; shortest_path(g, source, min_distance, previous); for (auto const & kvp : min_distance) { std::cout << source << " -> " << kvp.first << " : " << kvp.second << '\t'; print_path(build_path(previous, kvp.first)); std::cout << std::endl; } } 59. Program Weasel Program Weasel jest eksperymentem myślowym zaproponowanym przez Richarda Dawkinsa, mającym na celu zademonstrowanie, w jaki sposób nagromadzone drobne ulepszenia (mutacje, które przynoszą korzyść jednostce w taki sposób, że zostają następnie wybrane przez dobór naturalny) dają szybko znaczące wyniki, co jest przeciwieństwem błędnego rozumowania prezentowanego w głównym nurcie, że ewolucja działa wyłącznie skokowo. Opisany w Wikipedii (https://en.wikipedia.org/wiki/Weasel_program) algorytm symulacji o nazwie Weasel (łasica) jest następujący: 1. Zacznij od łańcucha zawierającego ciąg 28 losowych znaków. 2. Wykonaj 100 kopii tego łańcucha z 5-procentową szansą na zastąpienie każdego ze znaków inną losową wartością. 3. Porównaj wszystkie nowo utworzone łańcuchy z docelowym METHINKS IT IS LIKE A WEASEL oraz przypisz im odpowiednią wartość wynikową (równą liczbie liter w ciągu, które są poprawne i znajdują się na właściwej pozycji). 4. Jeśli którykolwiek z nowych łańcuchów uzyskał wynik idealny (28), zatrzymaj działanie programu. 5. W przeciwnym razie użyj łańcucha o najwyższym wyniku i przejdź do kroku 2. Oto możliwa implementacja powyższego algorytmu: funkcja make_random() tworzy losową sekwencję początkową o tej samej długości co łańcuch docelowy; funkcja fitness() oblicza wartość dla każdego zmutowanego łańcucha (czyli wyznacza poziom zgodności z łańcuchem docelowym); funkcja mutate() tworzy nowy łańcuch na podstawie rodzica z określoną szansą na mutację każdego znaku: 120 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych class weasel { std::string target; std::uniform_int_distribution<> chardist; std::uniform_real_distribution<> ratedist; std::mt19937 mt; std::string const allowed_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "; public: weasel(std::string_view t) : target(t), chardist(0, 26), ratedist(0, 100) { std::random_device rd; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); mt.seed(seq); } void run(int const copies) { auto parent = make_random(); int step = 1; std::cout << std::left << std::setw(5) << std::setfill(' ') << step << parent << std::endl; do { std::vector<std::string> children; std::generate_n(std::back_inserter(children), copies, [parent, this]() {return mutate(parent, 5); }); parent = *std::max_element( std::begin(children), std::end(children), [this](std::string_view c1, std::string_view c2) { return fitness(c1) < fitness(c2); }); std::cout << std::setw(5) << std::setfill(' ') << step << parent << std::endl; step++; } while (parent != target); } private: weasel() = delete; double fitness(std::string_view candidate) const { int score = 0; for (size_t i = 0; i < candidate.size(); ++i) { if (candidate[i] == target[i]) score++; } return score; 121 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów } std::string mutate(std::string_view parent, double const rate) { std::stringstream sstr; for (auto const c : parent) { auto nc = ratedist(mt) > rate ? c : allowed_chars[chardist(mt)]; sstr << nc; } return sstr.str(); } std::string make_random() { std::stringstream sstr; for (size_t i = 0; i < target.size(); ++i) { sstr << allowed_chars[chardist(mt)]; } return sstr.str(); } }; Klasa ta może zostać użyta w poniższy sposób: int main() { weasel w("METHINKS IT IS LIKE A WEASEL"); w.run(100); } 60. Gra w życie W zaprezentowanej poniżej klasie universe zaimplementowano grę będącą tematem tego zadania. Zawiera ona kilka interesujących funkcji: Funkcja initialize() generuje początkowy układ gry. Mimo że kod dołączony do książki zawiera więcej opcji, tutaj użyto tylko dwóch z nich: random, która generuje układ losowy, oraz ten_cell_row, która reprezentuje wiersz 10 komórek w środku siatki. Funkcja reset() przypisuje wszystkim komórkom stan dead. Funkcja count_neighbors() zwraca liczbę żywych sąsiadów. Wykorzystuje ona pomocniczy szablon funkcji wariadycznej count_alive(). Chociaż rozwiązanie można zaimplementować za pomocą wyrażeń fold, nie są one jeszcze obsługiwane w środowisku Visual C++ i dlatego zdecydowałem się ich tutaj nie używać. Funkcja next_generation() tworzy nowy stan gry na podstawie reguł przejścia. Funkcja display() wyświetla stan gry w konsoli. Wykorzystano tutaj wywołanie systemowe w celu czyszczenia okna konsoli, chociaż można też użyć innych środków, takich jak określone funkcje interfejsu API systemu operacyjnego. 122 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych Funkcja run() inicjuje układ początkowy, a następnie generuje nowe stany w podanym przez użytkownika przedziale: dla określonej liczby iteracji lub w nieskończoność (jeśli liczba iteracji została ustawiona na wartość 0). class universe { private: universe() = delete; public: enum class seed { random, ten_cell_row }; public: universe(size_t const width, size_t const height): rows(height), columns(width),grid(width * height), dist(0, 4) { std::random_device rd; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); mt.seed(seq); } void run(seed const s, int const generations, std::chrono::milliseconds const ms = std::chrono::milliseconds(100)) { reset(); initialize(s); display(); int i = 0; do { next_generation(); display(); } using namespace std::chrono_literals; std::this_thread::sleep_for(ms); } while (i++ < generations || generations == 0); private: void next_generation() { std::vector<unsigned char> newgrid(grid.size()); for (size_t r = 0; r < rows; ++r) { for (size_t c = 0; c < columns; ++c) { auto count = count_neighbors(r, c); 123 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów if (cell(c, r) == alive) { newgrid[r * columns + c] = (count == 2 || count == 3) ? alive : dead; } else { newgrid[r * columns + c] = (count == 3) ? alive : dead; } } } grid.swap(newgrid); } void reset_display() { #ifdef WIN32 system("cls"); #endif } void display() { reset_display(); for (size_t r = 0; r < rows; ++r) { for (size_t c = 0; c < columns; ++c) { std::cout << (cell(c, r) ? '*' : ' '); } std::cout << std::endl; } } void initialize(seed const s) { if (s == seed::ten_cell_row) { for (size_t c = columns / 2 - 5; c < columns / 2 + 5; c++) cell(c, rows / 2) = alive; } else { for (size_t r = 0; r < rows; ++r) { for (size_t c = 0; c < columns; ++c) { cell(c, r) = dist(mt) == 0 ? alive : dead; } } } } 124 ecb84badecb8c394873734f1e9bfb90f e Rozdział 6. • Algorytmy i struktury danych void reset() { for (size_t r = 0; r < rows; ++r) { for (size_t c = 0; c < columns; ++c) { cell(c, r) = dead; } } } int count_alive() { return 0; } template<typename T1, typename... T> auto count_alive(T1 s, T... ts) { return s + count_alive(ts...); } int count_neighbors(size_t const row, size_t const col) { if (row == 0 && col == 0) return count_alive(cell(1, 0), cell(1,1), cell(0, 1)); if (row == 0 && col == columns - 1) return count_alive(cell(columns - 2, 0), cell(columns - 2, cell(columns - 1, 1)); if (row == rows - 1 && col == 0) return count_alive(cell(0, rows - 2), cell(1, rows - 2), cell(1, rows - 1)); if (row == rows - 1 && col == columns - 1) return count_alive(cell(columns - 1, rows - 2), cell(columns - 2, rows - 2), cell(columns - 2, rows - 1)); if (row == 0 && col > 0 && col < columns - 1) return count_alive(cell(col - 1, 0), cell(col - 1, 1), cell(col, 1), cell(col + 1, 1), cell(col + 1, 0)); if (row == rows - 1 && col > 0 && col < columns - 1) return count_alive(cell(col - 1, row), cell(col - 1, row cell(col, row - 1), cell(col + 1, row cell(col + 1, row)); if (col == 0 && row > 0 && row < rows - 1) return count_alive(cell(0, row - 1), cell(1, row - 1), cell(1, row), cell(1, row + 1), cell(0, row + 1)); if (col == columns - 1 && row > 0 && row < rows - 1) return count_alive(cell(col, row - 1), cell(col - 1, row cell(col - 1, row), cell(col - 1, row + cell(col, row + 1)); return count_alive(cell(col cell(col cell(col cell(col + + - 1, 1, 1, 1, row row row row + + 1), 1), 1), 1), cell(col, row cell(col + 1, cell(col, row cell(col - 1, 1), 1), 1), 1), 1), - 1), row), + 1), row)); } unsigned char& cell(size_t const col, size_t const row) 125 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów { return grid[row * columns + col]; } private: size_t rows; size_t columns; std::vector<unsigned char> grid; const unsigned char alive = 1; const unsigned char dead = 0; std::uniform_int_distribution<> dist; std::mt19937 mt; }; W taki sposób można uruchomić grę trwającą 100 iteracji, która rozpoczyna się od stanu losowego: int main() { using namespace std::chrono_literals; universe u(50, 20); u.run(universe::seed::random, 100, 100ms); } Oto przykładowy wynik działania programu (zrzut ekranu przedstawia pojedynczą iterację w świecie Gry w życie): 126 ecb84badecb8c394873734f1e9bfb90f e 7 Współbieżność Zadania 61. Algorytm przekształcania współbieżnego Utwórz algorytm ogólnego zastosowania, który wykorzystuje podaną funkcję jednoargumentową do równoległego przekształcania elementów zbioru. Operacja jednoargumentowa użyta do przekształcenia zbioru nie może unieważniać iteratorów zakresu ani modyfikować elementów tego zbioru. Poziom współbieżności, czyli liczba wątków wykonawczych i sposób jej realizacji, jest szczegółem implementacyjnym. 62. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu wątków Zaimplementuj algorytmy współbieżne ogólnego zastosowania, które znajdują minimalną i maksymalną wartość w danym zbiorze. Współbieżność powinna zostać zrealizowana za pomocą wątków. Liczba równoległych wątków jest szczegółem implementacyjnym. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 63. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu funkcji asynchronicznych Zaimplementuj algorytmy współbieżne ogólnego zastosowania, które dla danego zbioru znajdują wartość minimalną i maksymalną. Współbieżność powinna zostać zrealizowana za pomocą funkcji asynchronicznych. Liczba równoległych funkcji jest szczegółem implementacyjnym. 64. Algorytm sortowania współbieżnego Utwórz współbieżną wersję algorytmu sortowania zdefiniowanego dla zadania 57., „Algorytm sortowania”, znajdującego się w rozdziale 6. „Algorytmy i struktury danych”. Funkcja powinna wykorzystywać parę iteratorów dostępu swobodnego w celu zdefiniowania zakresu danych, a następnie sortować elementy zbioru za pomocą algorytmu quicksort. Do porównywania danych należy użyć operatorów porównania. Poziom współbieżności i sposób jej realizacji jest szczegółem implementacyjnym. 65. Wyświetlanie komunikatów w konsoli w sposób bezpieczny dla wątków Utwórz klasę, która umożliwia komponentom działającym w różnych wątkach bezpieczne wyświetlanie komunikatów w konsoli, w taki sposób synchronizując dostęp do standardowego strumienia wyjściowego, by zagwarantować integralność wyświetlanych danych. Utworzony moduł logowania powinien mieć metodę o nazwie log() zawierającą argument tekstowy reprezentujący komunikat, który ma zostać wyświetlony w konsoli. 66. System obsługi klienta Napisz program symulujący sposób obsługi klientów w biurze. Biuro ma trzy stanowiska, przy których w tym samym czasie mogą być obsługiwani różni klienci. Klienci mogą wejść do biura w dowolnym momencie. Pobierają oni bilet z numerem serwisowym z automatu biletowego i czekają, aż ich numer będzie mógł zostać obsłużony przy jednym ze stanowisk. Klienci są obsługiwani w kolejności, w jakiej weszli do biura, a dokładniej, w kolejności zdefiniowanej na ich bilecie. Za każdym razem, gdy punkt obsługi kończy obsługę danego klienta, zaczyna być obsługiwany kolejny klient. Symulacja powinna się zakończyć, gdy określona liczba klientów otrzyma bilety i zostanie obsłużona. 128 ecb84badecb8c394873734f1e9bfb90f e Rozdział 7. • Współbieżność Rozwiązania 61. Algorytm przekształcania współbieżnego Algorytm ogólnego zastosowania std::transform() wykorzystuje daną funkcję dla określonego zestawu danych i zapisuje wynik w innym (lub tym samym) zbiorze. Wymaganiem w naszym zadaniu jest zaimplementowanie współbieżnej wersji takiego algorytmu. W przypadku funkcji ogólnego zastosowania użyjemy iteratorów, które będą traktowane jako argumenty w celu zdefiniowania pierwszego i następnego po ostatnim elementu zakresu danych. Ponieważ funkcja jednoargumentowa będzie stosowana tak samo do wszystkich elementów zbioru, działanie operacji można w prosty sposób uczynić współbieżnym. Do tego celu użyjemy wątków. Ponieważ nie określono, ile wątków powinno działać w tym samym czasie, możemy zastosować funkcję std::thread::hardware_concurrency(). Zwraca ona wskazówkę dotyczącą liczby współbieżnych wątków obsługiwanych w danej implementacji. Współbieżna wersja algorytmu działa lepiej niż implementacja sekwencyjna tylko wówczas, gdy rozmiar zakresu danych przekracza określony próg, który może się różnić w zależności od opcji kompilacji, platformy lub sprzętu. W poniższej implementacji próg ten został ustawiony na wartość 10 000 elementów. W kolejnym ćwiczeniu możesz eksperymentować z różnymi progami i wielkościami zakresu danych, aby sprawdzić, jak zmienia się czas wykonania. Przedstawiona poniżej funkcja ptransform() implementuje algorytm przekształcania współbieżnego zgodnie z wymaganiami zadania. Wywołuje ona po prostu funkcję std::transform(), jeśli rozmiar zakresu nie przekracza zdefiniowanego progu. W przeciwnym razie zakres danych dzielony jest na kilka równych części, po jednej dla każdego z wątków, które następnie wywołują std::transform(). W takim przypadku funkcja blokuje wywołujący wątek aż do momentu zakończenia wszystkich wątków roboczych: template <typename RandomAccessIterator, typename F> void ptransform(RandomAccessIterator begin, RandomAccessIterator end, F&& f) { auto size = std::distance(begin, end); if (size <= 10000) { std::transform(begin, end, begin, std::forward<F>(f)); } else { std::vector<std::thread> threads; int thread_count = std::thread::hardware_concurrency(); auto first = begin; auto last = first; size /= thread_count; for (int i = 0; i < thread_count; ++i) { 129 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów first = last; if (i == thread_count - 1) last = end; else std::advance(last, size); threads.emplace_back([first, last, &f]() { std::transform(first, last, first, std::forward<F>(f)); }); } for (auto & t : threads) t.join(); } } Przedstawiona poniżej funkcja palter() jest funkcją pomocniczą, która wywołuje ptransform() dla kontenera std::vector, a następnie zwraca inny obiekt std::vector zawierający wynik: template <typename T, typename F> std::vector<T> palter(std::vector<T> data, F&& f) { ptransform(std::begin(data), std::end(data), std::forward<F>(f)); return data; } Funkcja może zostać użyta w następujący sposób (pełny przykład można znaleźć w kodzie źródłowym dołączonym do tej książki): int main() { std::vector<int> data(1000000); // inicjalizacja danych auto result = palter(data, [](int const e) {return e * e; }); } W języku C++ 17 szereg standardowych algorytmów ogólnego zastosowania (w tym std::transform()) posiada odpowiednie przeciążenia implementujące wersje współbieżne, które mogą być uruchamiane zgodnie z określoną polityką wykonywania. 62. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu wątków To zadanie i jego rozwiązanie są podobne pod wieloma względami do poprzedniego. Jedyną różnicą jest to, że funkcja współbieżnie wykonywana w każdym z wątków musi zwracać wartość reprezentującą minimalny lub maksymalny element w zbiorze. 130 ecb84badecb8c394873734f1e9bfb90f e Rozdział 7. • Współbieżność Przedstawiony poniżej szablon funkcji pprocess() jest funkcją wyższego poziomu, która implementuje wymaganą funkcjonalność w sposób ogólny: Jej argumenty to iteratory wskazujące na element pierwszy oraz następny po ostatnim, a także obiekt funkcji o nazwie f, który przetwarza zbiór danych. Jeśli rozmiar zakresu danych jest mniejszy niż określony próg (zdefiniowany tutaj jako 10 000 elementów), wykonywany jest po prostu obiekt funkcji f będący argumentem. W przeciwnym razie zbiór wejściowy jest dzielony na kilka podzakresów o tej samej wielkości, po jednym dla każdego ze współbieżnych wątków, które mogą zostać wykonane. Każdy wątek uruchamia funkcję f dla wybranego podzakresu. Wyniki współbieżnego wykonywania funkcji f są gromadzone w kontenerze std::vector, a po zakończeniu działania wszystkich wątków funkcja f zostaje ponownie użyta do ustalenia wyniku sumarycznego z wyników częściowych: template <typename Iterator, typename F> auto pprocess(Iterator begin, Iterator end, F&& f) { auto size = std::distance(begin, end); if (size <= 10000) { return std::forward<F>(f)(begin, end); } else { int thread_count = std::thread::hardware_concurrency(); std::vector<std::thread> threads; std::vector<typename std:: iterator_traits<Iterator>::value_type> mins(thread_count); auto first = begin; auto last = first; size /= thread_count; for (int i = 0; i < thread_count; ++i) { first = last; if (i == thread_count - 1) last = end; else std::advance(last, size); threads.emplace_back([first, last, &f, &r=mins[i]]() { r = std::forward<F>(f)(first, last); }); } for (auto & t : threads) t.join(); return std::forward<F>(f)(std::begin(mins), std::end(mins)); } } 131 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów W celu implementacji wymaganych algorytmów ogólnego przeznaczenia służących do wyszukiwania elementów minimalnych i maksymalnych są dostępne dwie funkcje o odpowiednich nazwach pmin() i pmax(). Wywołują one z kolei funkcję pprocess(), przekazując jako trzeci argument wyrażenie lambda, które używa standardowego algorytmu std::min_element() lub std::max_element(): template <typename Iterator> auto pmin(Iterator begin, Iterator end) { return pprocess(begin, end, [](auto b, auto e){return *std::min_element(b, e);}); } template <typename Iterator> auto pmax(Iterator begin, Iterator end) { return pprocess(begin, end, [](auto b, auto e){return *std::max_element(b, e);}); } Powyższe funkcje mogą zostać użyte w następujący sposób: int main() { std::vector<int> data(count); // inicjalizacja danych auto rmin = pmin(std::begin(data), std::end(data)); auto rmax = pmin(std::begin(data), std::end(data)); } Kolejnym ćwiczeniem może być zaimplementowanie algorytmu ogólnego przeznaczenia, który oblicza sumę wszystkich elementów w zbiorze, wykorzystując w tym celu współbieżność z użyciem wątków. 63. Algorytmy wyszukiwania współbieżnego minimalnych i maksymalnych elementów w zbiorze przy użyciu funkcji asynchronicznych Jedyna różnica między obecnym a poprzednim zadaniem polega na tym, w jaki sposób osiąga się współbieżność. Wcześniejsze zadanie wymagało użycia wątków. W tym przypadku należy użyć funkcji asynchronicznych. Zadanie może być wykonywane asynchronicznie z zastosowaniem funkcji std::async(). Tworzy ona obietnicę (ang. promise), która jest asynchronicznym dostawcą wyniku funkcji wykonywanej asynchronicznie. Obietnica zawiera zarówno stan współdzielony (który może przechowywać albo wartość zwracaną przez funkcję, albo wyjątek pojawiający się na skutek jej wykonania), jak i powiązany z nią obiekt przyszłości (ang. future), który zapewnia dostęp do tego stanu z innego wątku. Para obietnica-przyszłość definiuje kanał, który umożliwia przekazywanie wartości między wątkami. Funkcja std::async() zwraca przyszłość związaną z obietnicą, którą tworzy. 132 ecb84badecb8c394873734f1e9bfb90f e Rozdział 7. • Współbieżność W poniższej implementacji funkcji pprocess() użycie wątków w sposób wynikający z poprzedniego zadania zostało zastąpione przez wywołania std::async(). Zauważ, że w celu zagwarantowania wykonania asynchronicznego bez wartościowania leniwego musisz podać wartość std::launch::async jako pierwszy parametr funkcji std::async(). Ilość zmian w porównaniu z poprzednią implementacją jest bardzo mała, więc zrozumienie obecnego kodu na podstawie wcześniejszego algorytmu nie powinno sprawiać problemów: template <typename Iterator, typename F> auto pprocess(Iterator begin, Iterator end, F&& f) { auto size = std::distance(begin, end); if (size <= 10000) { return std::forward<F>(f)(begin, end); } else { int task_count = std::thread::hardware_concurrency(); std::vector<std::future< typename std::iterator_traits<Iterator>::value_type>> tasks; auto first = begin; auto last = first; size /= task_count; for (int i = 0; i < task_count; ++i) { first = last; if (i == task_count - 1) last = end; else std::advance(last, size); tasks.emplace_back(std::async( std::launch::async, [first, last, &f]() { return std::forward<F>(f)(first, last); })); } std::vector<typename std::iterator_traits<Iterator>::value_type> mins; for (auto & t : tasks) mins.push_back(t.get()); return std::forward<F>(f)(std::begin(mins), std::end(mins)); } } template <typename Iterator> auto pmin(Iterator begin, Iterator end) { return pprocess(begin, end, [](auto b, auto e){return *std::min_element(b, e);}); } 133 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów template <typename Iterator> auto pmax(Iterator begin, Iterator end) { return pprocess(begin, end, [](auto b, auto e){return *std::max_element(b, e);}); } Użycie omawianej funkcji pokazano w poniższym kodzie: int main() { std::vector<int> data(count); // inicjalizacja danych auto rmin = pmin(std::begin(data), std::end(data)); auto rmax = pmax(std::begin(data), std::end(data)); } Następnym ćwiczeniem może być ponowne zaimplementowanie algorytmu ogólnego przeznaczenia, który oblicza sumę wszystkich elementów w zbiorze, wykorzystując w tym celu współbieżność z użyciem funkcji asynchronicznych. 64. Algorytm sortowania współbieżnego Wcześniej zajmowaliśmy się sekwencyjną implementacją algorytmu quicksort. Jest on algorytmem typu „dziel i zwyciężaj”, którego działanie polega na podzieleniu zakresu danych przeznaczonego do posortowania na dwie części, z których jedna zawiera tylko wartości mniejsze niż wybrany element (zwany rozdzielającym), a druga składa się wyłącznie z wartości większych od niego. Następnie ten sam algorytm stosuje się rekurencyjnie dla dwóch partycji aż do momentu, gdy będą się one składać tylko z jednego elementu lub nie będą miały żadnego. Ze względu na charakter algorytmu sortowanie typu quicksort może zostać łatwo zrównoleglone, aby rekurencyjnie stosować je jednocześnie na dwóch partycjach. Aby uzyskać współbieżność, wykorzystano funkcje asynchroniczne. Takie rozwiązanie jest jednak skuteczne tylko w przypadku większych zakresów danych. Istnieje próg, dla którego narzut związany z kontekstem przełączającym dla wykonania równoległego jest zbyt duży, a czas takiego wykonania staje się większy od czasu wykonania sekwencyjnego. W poniższej implementacji próg ten został ustawiony na wartość 100 000 elementów. Kolejnym ćwiczeniem mogłyby być eksperymenty związane z ustawianiem różnych wartości tego progu i porównywaniem działania wersji współbieżnej z sekwencyjną: template <class RandomIt> RandomIt partition(RandomIt first, RandomIt last) { auto pivot = *first; auto i = first + 1; auto j = last - 1; while (i <= j) { 134 ecb84badecb8c394873734f1e9bfb90f e Rozdział 7. • Współbieżność while (i <= j && *i <= pivot) i++; while (i <= j && *j > pivot) j--; if (i < j) std::iter_swap(i, j); } std::iter_swap(i - 1, first); return i - 1; } template <class RandomIt> void pquicksort(RandomIt first, RandomIt last) { if (first < last) { auto p = partition(first, last); if(last - first <= 100000) { pquicksort(first, p); pquicksort(p + 1, last); } else { auto f1 = std::async(std::launch::async, [first, p](){ pquicksort(first, p);}); auto f2 = std::async(std::launch::async, [last, p]() { pquicksort(p+1, last);}); f1.wait(); f2.wait(); } } } Na poniższym przykładzie pokazano, w jaki sposób można sortować za pomocą funkcji pquicksort() duży wektor losowych liczb całkowitych (o wartościach od 1 do 1000) : int main() { std::random_device rd; std::mt19937 mt; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); mt.seed(seq); std::uniform_int_distribution<> ud(1, 1000); const size_t count = 1000000; std::vector<int> data(count); std::generate_n(std::begin(data), count, [&mt, &ud]() {return ud(mt); }); pquicksort(std::begin(data), std::end(data)); } 135 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 65. Wyświetlanie komunikatów w konsoli w sposób bezpieczny dla wątków Definicja języka C++ nie zawiera koncepcji konsoli, dlatego do wykonywania operacji wejścia oraz wyjścia na nośnikach sekwencyjnych, takich jak pliki, używane są strumienie, jednakże obiekty globalne std::cout i std::wcout sterują przesyłaniem danych do bufora strumieniowego powiązanego z wyjściowym strumieniem stdout języka C. Te globalne obiekty strumieniowe nie mogą być bezpiecznie dostępne z różnych wątków. W razie potrzeby dostęp do nich musisz zsynchronizować. Jest to wymaganie dotyczące komponentu, który będziesz musiał utworzyć w celu rozwiązania obecnego zadania. Przedstawiona poniżej klasa logger używa mechanizmu std::mutex w celu synchronizowania dostępu do obiektu std::cout w metodzie log(). Klasa została zaimplementowana jako singleton bezpieczny dla wątków. Statyczna metoda instance() zwraca odwołanie do lokalnego obiektu statycznego (który charakteryzuje się określonym czasem przechowywania). W języku C++ 11 inicjalizacja statycznego obiektu odbywa się tylko raz, nawet jeśli kilka wątków próbuje go zainicjalizować w tym samym czasie. W takiej sytuacji wątki współbieżne są blokowane aż do momentu zakończenia inicjalizacji wykonywanej w pierwszym wątku wywołującym. Dlatego też nie ma potrzeby stosowania dodatkowych mechanizmów synchronizacji zdefiniowanych przez użytkownika: class logger { protected: logger() {} public: static logger& instance() { static logger lg; return lg; } logger(logger const &) = delete; logger& operator=(logger const &) = delete; void log(std::string_view message) { std::lock_guard<std::mutex> lock(mt); std::cout << "LOG: " << message << std::endl; } private: std::mutex mt; }; Powyższa klasa logger może zostać użyta do wyświetlania w konsoli informacji pochodzących z wielu wątków: 136 ecb84badecb8c394873734f1e9bfb90f e Rozdział 7. • Współbieżność int main() { setlocale(LC_ALL, "polish"); std::vector<std::thread> modules; for(int id = 1; id <= 5; ++id) { modules.emplace_back([id](){ std::random_device rd; std::mt19937 mt(rd()); std::uniform_int_distribution<> ud(100, 1000); logger::instance().log("moduł " + std::to_string(id) + " uruchomiony"); std::this_thread::sleep_for(std::chrono::milliseconds(ud(mt))); logger::instance().log("moduł " + std::to_string(id) + " zakończony"); }); } for(auto & m : modules) m.join(); } 66. System obsługi klienta Aby zaimplementować symulację biura obsługi klienta zgodnie z wcześniej podanymi wymaganiami, możemy skorzystać z kilku klas pomocniczych. Klasa ticketing_machine modeluje bardzo prostą maszynę, która generuje przyrostowe numery biletów, zaczynając od wartości początkowej określonej przez użytkownika. Klasa customer reprezentuje klienta, który wchodzi do biura i pobiera bilet z automatu biletowego. W tej klasie został przeciążony operator <, aby przechowywać klientów w kolejce priorytetowej, z której będą pobierani w kolejności wynikającej z ich numerów biletów. Ponadto klasa logger, utworzona w poprzednim zadaniu, została użyta do wyświetlania komunikatów w konsoli: class ticketing_machine { public: ticketing_machine(int const start) : last_ticket(start),first_ticket(start) {} int next() { return last_ticket++; } int last() const { return last_ticket - 1; } void reset() { last_ticket = first_ticket; } private: int first_ticket; int last_ticket; }; class customer { public: 137 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów customer(int const no) : number(no) {} int ticket_number() const noexcept { return number; } private: int number; friend bool operator<(customer const & l, customer const & r); }; bool operator<(customer const & l, customer const & r) { return l.number > r.number; } Każde stanowisko w biurze jest modelowane przy użyciu innego wątku. Klienci wchodzący do biura i umieszczani w kolejce po otrzymaniu biletu są modelowani przy użyciu osobnego wątku. W poniższej symulacji nowy klient wchodzi do biura co 200 – 500 milisekund, otrzymuje bilet i zostaje dodany do kolejki priorytetowej. Wykonywanie wątku biura kończy się, gdy 25 klientów wejdzie do niego i zostanie umieszczonych w kolejce. Klasa std::condition_variable służy do komunikacji między wątkami w celu przesyłania powiadomień, że nowy klient został umieszczony w kolejce lub że istniejący klient został z niej usunięty (co dzieje się wówczas, gdy klient podchodzi do dostępnego stanowiska). Wątki reprezentujące stanowiska działają do momentu zresetowania wskaźnika informującego, że biuro jest dostępne. Nie nastąpi to jednak przed obsłużeniem wszystkich klientów znajdujących się w kolejce. W naszej symulacji każdy klient przebywa przy stanowisku od 2000 do 3000 milisekund: int main() { setlocale(LC_ALL, "polish"); std::priority_queue<customer> customers; bool store_open = true; std::mutex mt; std::condition_variable cv; std::vector<std::thread> desks; for (int i = 1; i <= 3; ++i) { desks.emplace_back([i, &store_open, &mt, &cv, &customers]() { std::random_device rd; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); std::mt19937 eng(seq); std::uniform_int_distribution<> ud(2000, 3000); logger::instance().log("stanowisko " + std::to_string(i) + " dostępne"); while (store_open || !customers.empty()) { std::unique_lock<std::mutex> locker(mt); cv.wait_for(locker, std::chrono::seconds(1), [&customers]() {return !customers.empty(); }); 138 ecb84badecb8c394873734f1e9bfb90f e Rozdział 7. • Współbieżność if (!customers.empty()) { auto const c = customers.top(); customers.pop(); logger::instance().log("[-] stanowisko " + std::to_string(i) + " obsługuje klienta " + std::to_string(c.ticket_number())); logger::instance().log("[=] rozmiar kolejki: " + std::to_string(customers.size())); locker.unlock(); cv.notify_one(); std::this_thread::sleep_for(std::chrono::milliseconds(ud(eng))); } } } logger::instance().log("[ ] stanowisko " + std::to_string(i) + " zakończyło obsługę klienta " + std::to_string(c.ticket_number())); logger::instance().log("stanowisko " + std::to_string(i) + " niedostępne"); }); std::thread store([&store_open, &customers, &mt, &cv]() { ticketing_machine tm(100); std::random_device rd; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); std::mt19937 eng(seq); std::uniform_int_distribution<> ud(200, 500); for (int i = 1; i <= 25; ++i) { customer c(tm.next()); customers.push(c); logger::instance().log("[+] nowy klient z biletem " + std::to_string(c.ticket_number())); logger::instance().log("[=] rozmiar kolejki: " + std::to_string(customers.size())); cv.notify_one(); } std::this_thread::sleep_for(std::chrono::milliseconds(ud(eng))); store_open = false; }); store.join(); } for (auto & desk : desks) desk.join(); 139 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Oto fragment wyników wyświetlanych w konsoli podczas wykonywania programu: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: … LOG: LOG: LOG: LOG: LOG: LOG: LOG: … LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: LOG: stanowisko 1 dostępne stanowisko 2 dostępne stanowisko 3 dostępne [+] nowy klient z biletem 100 [-] stanowisko 1 obsługuje klienta 100 [=] rozmiar kolejki: 0 [=] rozmiar kolejki: 0 [+] nowy klient z biletem 101 [=] rozmiar kolejki: 1 [-] stanowisko 2 obsługuje klienta 101 [=] rozmiar kolejki: 0 [+] nowy klient z biletem 102 [=] rozmiar kolejki: 1 [-] stanowisko 3 obsługuje klienta 102 [=] rozmiar kolejki: 0 [+] nowy klient z biletem 103 [=] rozmiar kolejki: 1 [+] [=] [+] [=] [ ] [-] [=] nowy klient z biletem 112 rozmiar kolejki: 7 nowy klient z biletem 113 rozmiar kolejki: 8 stanowisko 1 zakończyło obsługę klienta 103 stanowisko 1 obsługuje klienta 106 rozmiar kolejki: 7 [-] stanowisko 2 obsługuje klienta 120 [=] rozmiar kolejki: 4 [ ] stanowisko 3 zakończyło obsługę klienta [-] stanowisko 3 obsługuje klienta 121 [=] rozmiar kolejki: 3 [ ] stanowisko 1 zakończyło obsługę klienta [-] stanowisko 1 obsługuje klienta 122 [=] rozmiar kolejki: 2 [ ] stanowisko 2 zakończyło obsługę klienta [-] stanowisko 2 obsługuje klienta 123 [=] rozmiar kolejki: 1 [ ] stanowisko 3 zakończyło obsługę klienta [-] stanowisko 3 obsługuje klienta 124 [=] rozmiar kolejki: 0 [ ] stanowisko 1 zakończyło obsługę klienta stanowisko 1 niedostępne [ ] stanowisko 2 zakończyło obsługę klienta stanowisko 2 niedostępne [ ] stanowisko 3 zakończyło obsługę klienta stanowisko 3 niedostępne 118 119 120 121 122 123 124 W kolejnym ćwiczeniu możesz spróbować zmienić odstępy czasowe, w których klienci wchodzą do biura, a także liczbę klientów, którzy mogą otrzymać bilety przed jego zamknięciem, jak również ilość czasu potrzebnego do ich obsłużenia oraz liczbę stanowisk otwartych w biurze. 140 ecb84badecb8c394873734f1e9bfb90f e 8 Wzorce projektowe Zadania 67. Sprawdzanie poprawności haseł Napisz program sprawdzający siłę hasła na podstawie wstępnie zdefiniowanych reguł, które można następnie wybierać w różnych kombinacjach. Każde hasło musi spełniać co najmniej wymóg minimalnej długości. Ponadto można wymusić inne reguły, takie jak obecność przynajmniej jednego symbolu, cyfry, wielkiej i małej litery. 68. Generowanie losowych haseł Stwórz program, który może generować losowe hasła zgodnie z pewnymi predefiniowanymi regułami. Każde hasło musi mieć konfigurowalną minimalną długość. Ponadto powinno być możliwe uwzględnienie takich reguł jak obecność co najmniej jednej cyfry, symbolu, małej lub wielkiej litery. Te dodatkowe zasady muszą być konfigurowalne i możliwe do użycia razem z innymi. 69. Generowanie numerów ubezpieczenia socjalnego Napisz program, który może generować numery ubezpieczenia socjalnego dla dwóch krajów: Northerii i Southerii. Kraje te używają odmiennych, lecz jednocześnie podobnych do siebie formatów liczbowych: W Northerii numery mają format SYYYYMMDDNNNNNC, gdzie S jest cyfrą reprezentującą płeć, równą 9 w przypadku kobiet i 7 w przypadku mężczyzn, YYYYMMDD oznacza datę urodzenia, NNNNN jest pięciocyfrową liczbą losową unikatową dla danego dnia (co oznacza, że ta sama liczba może pojawić się ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów dwa razy dla dwóch różnych dat, ale nie w przypadku tej samej daty urodzenia), natomiast C jest cyfrą wybraną w taki sposób, że suma kontrolna obliczona na podstawie później przedstawionego algorytmu jest wielokrotnością liczby 11. W Southerii numery mają format SYYYYMMDDNNNNC, gdzie S jest cyfrą reprezentującą płeć, równą 1 dla kobiet i 2 dla mężczyzn, YYYYMMDD to data urodzenia, NNNN to czterocyfrowy numer losowy unikatowy dla danego dnia, a C jest cyfrą wybraną tak, że suma kontrolna obliczona w sposób opisany poniżej jest wielokrotnością liczby 10. Suma kontrolna w obu przypadkach jest sumą wszystkich cyfr, z których każda zostaje pomnożona przez jej wagę (czyli pozycję, licząc od najbardziej do najmniej znaczącej cyfry). Na przykład suma kontrolna numeru southerańskiego 12017120134895 jest obliczana w następujący sposób: crc = 14*1 + 13*2 + 12*0 + 11*1 + 10*7 + 9*1 + 8*2 + 7*0 + 6*1 + 5*3 + 4*4 + 3*8 + 2*9 + 1*5 = 230 = 23 * 10 70. System zatwierdzania Napisz program przeznaczony dla działu zakupów w firmie, który umożliwia pracownikom zatwierdzanie nowych zakupów (lub wydatków). W zależności od posiadanego stanowiska każdy pracownik może zatwierdzać wydatki tylko do wcześniej określonego limitu. Na przykład zwykli pracownicy mogą zatwierdzać wydatki do 1000 jednostek walutowych, menedżerowie zespołu — do 10 000, a kierownik działu — do 100 000. Każdy większy wydatek musi zostać jawnie zatwierdzony przez prezesa firmy. 71. Obserwowany kontener typu wektorowego Utwórz szablon klasy, która zachowuje się jak wektor, lecz może powiadamiać zarejestrowane strony o wewnętrznych zmianach stanu. W klasie muszą istnieć co najmniej poniższe funkcjonalności: różne konstruktory do tworzenia nowych instancji klasy, operator = w celu przypisania wartości do kontenera, metoda push_back() służąca do umieszczania nowego elementu na końcu kontenera, metoda pop_back() do usuwania ostatniego elementu z kontenera, metoda clear() do usuwania wszystkich elementów z kontenera, metoda size() zwracająca liczbę elementów w kontenerze, metoda empty() informująca, czy kontener jest pusty lub czy zawiera jakieś elementy. Operator =, a także metody push_back(), pop_back() i clear() muszą powiadamiać inne funkcje o zmianach stanu. Powiadomienie powinno zawierać typ zmiany, a także (w przypadku gdy występuje) indeks elementu, który został zmieniony (na przykład został dodany lub usunięty). 142 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe 72. Obliczanie ceny zamówienia z rabatami Sklep detaliczny posiada bogaty asortyment towarów i oferuje różne rodzaje rabatów dla wybranych klientów, artykułów lub zamówień. Dostępne mogą być następujące rodzaje rabatów: Rabat stały, na przykład 5%, niezależnie od zakupionego artykułu lub jego ilości. Rabat hurtowy, na przykład 10%, dla każdego artykułu przy zakupie większej jego ilości. Rabat od ceny za zamówienie określonego artykułu, czyli jest to sytuacja, gdy klient kupuje pewną jego ilość, a całkowity koszt przekracza określoną kwotę. Na przykład można ustalić 15% rabatu na artykuł, gdy całkowity koszt przekroczy 100 zł. Jeśli artykuł kosztuje 5 zł, a klient kupi 30 sztuk, całkowity koszt wyniesie 150 zł, w związku z czym 15% rabatu będzie dotyczyć zamówienia tego artykułu. Rabat od ceny za całe zamówienie (niezależnie od tego, jakie artykuły i w jakiej ilości zostały zamówione). Napisz program, który potrafi obliczyć ostateczną cenę dla konkretnego zamówienia. Powinno być możliwe wyznaczanie końcowej ceny na różne sposoby; na przykład wszystkie zniżki mogą być łączone lub jeśli z drugiej strony jakiś artykuł jest już sprzedawany ze zniżką, rabat wyznaczany dla danego klienta lub całkowitej wielkości zamówienia może nie być brany pod uwagę. 143 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Rozwiązania 67. Sprawdzanie poprawności haseł Opisany tutaj problem jest typowym przypadkiem dla wzorca projektowego Dekorator. Pozwala on na zwiększanie funkcjonalności danego obiektu bez wpływania na inne obiekty tego samego typu. Osiąga się to poprzez opakowywanie obiektu innym obiektem. Można stosować wiele „opakowań”, dodając za każdym razem nową funkcjonalność. W naszym przypadku powinna ona polegać na sprawdzaniu, czy dane hasło spełnia określone wymagania. Na poniższym diagramie klas przedstawiono wzorzec sprawdzania poprawności haseł: 144 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe Implementacja powyższego wzorca jest taka: class password_validator { public: virtual bool validate(std::string_view password) = 0; virtual ~password_validator() {} }; class length_validator final : public password_validator { public: length_validator(unsigned int min_length): length(min_length) {} virtual bool validate(std::string_view password) override { return password.length() >= length; } private: unsigned int length; }; class password_validator_decorator : public password_validator { public: explicit password_validator_decorator( std::unique_ptr<password_validator> validator): inner(std::move(validator)) { } virtual bool validate(std::string_view password) override { return inner->validate(password); } private: std::unique_ptr<password_validator> inner; }; class digit_password_validator final : public password_validator_decorator { public: explicit digit_password_validator( std::unique_ptr<password_validator> validator): password_validator_decorator(std::move(validator)) { } virtual bool validate(std::string_view password) override { if(!password_validator_decorator::validate(password)) 145 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów return false; return password.find_first_of("0123456789") != std::string::npos; } }; class case_password_validator final : public password_validator_decorator { public: explicit case_password_validator( std::unique_ptr<password_validator> validator): password_validator_decorator(std::move(validator)) { } virtual bool validate(std::string_view password) override { if(!password_validator_decorator::validate(password)) return false; bool haslower = false; bool hasupper = false; for(size_t i = 0; i < password.length() && !(hasupper && haslower); ++i) { if(islower(password[i])) haslower = true; else if(isupper(password[i])) hasupper = true; } return haslower && hasupper; } }; class symbol_password_validator final : public password_validator_decorator { public: explicit symbol_password_validator( std::unique_ptr<password_validator> validator): password_validator_decorator(std::move(validator)) { } virtual bool validate(std::string_view password) override { if(!password_validator_decorator::validate(password)) return false; return password.find_first_of("!@#$%^&*(){}[]?<>") != std::string::npos; } }; 146 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe Klasa password_validator jest klasą podstawową i zawiera jedną metodę wirtualną zwaną validate() z argumentem tekstowym reprezentującym hasło. Klasa length_validator dziedziczy po tej klasie i implementuje obowiązkowy wymóg hasła o minimalnej długości. Klasa password_validator_decorator również dziedziczy po password_validator i zawiera prywatne pole inner typu password_validator. Jej metoda validate() po prostu wywołuje inner->validate(). Pozostałe klasy, czyli digit_password_validator, symbol_password_validator oraz case_password_validator, pochodzą od niej i implementują pozostałe indywidualne wymagania dotyczące siły hasła. Poniższe przykłady ilustrują, w jaki sposób można łączyć wymienione klasy w celu tworzenia określonych walidatorów haseł: int main() { auto validator1 = std::make_unique<digit_password_validator>( std::make_unique<length_validator>(8)); assert(validator1->validate("abc123!@#")); assert(!validator1->validate("abcde!@#")); auto validator2 = std::make_unique<symbol_password_validator>( std::make_unique<case_password_validator>( std::make_unique<digit_password_validator>( std::make_unique<length_validator>(8)))); assert(validator2->validate("Abc123!@#")); assert(!validator2->validate("Abc123567")); } 68. Generowanie losowych haseł To zadanie można rozwiązać za pomocą wzorca Kompozyt lub jego zmodyfikowanej wersji. Ten wzorzec projektowy łączy obiekty w hierarchiczną strukturę drzewa i umożliwia traktowanie grup (lub drzew) obiektów w taki sam sposób jak poszczególnych elementów tego samego typu. Na diagramie klas na następnej stronie pokazano hierarchię klas, które mogą zostać użyte do generowania haseł: Klasa password_generator jest klasą podstawową zawierającą kilka metod wirtualnych: generate() zwraca nowy losowy ciąg znaków, length() zwraca długość generowanych łańcuchów, allowed_chars() zwraca ciąg znaków zawierający wszystkie znaki używane do generowania haseł, natomiast add() dodaje nowy komponent potomny do generatora złożonego. Klasa basic_password_generator dziedziczy po klasie bazowej i definiuje generator o minimalnej długości. Klasy digit_generator, symbol_generator, upper_letter_generator i lower_letter_generator dziedziczą po basic_password_generator i przesłaniają metodę allowed_chars() w celu zdefiniowania podzbiorów znaków używanych do generowania losowych tekstów. 147 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Klasa composite_password_generator również dziedziczy po password_generator i zawiera kolekcję obiektów password_generator , których używa do tworzenia losowego tekstu. Odbywa się to za pomocą przesłoniętej metody generate(), która łączy wszystkie ciągi generowane przez składniki podrzędne, a następnie rozmieszcza je losowo w celu utworzenia ostatecznego łańcucha reprezentującego hasło: class password_generator { public: virtual std::string generate() = 0; virtual std::string allowed_chars() const = 0; virtual size_t length() const = 0; 148 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe virtual void add(std::unique_ptr<password_generator> generator) = 0; virtual ~password_generator(){} }; class basic_password_generator : public password_generator { size_t len; public: explicit basic_password_generator(size_t const len) noexcept : len(len) {} virtual std::string generate() override { throw std::runtime_error("nie zaimplementowano"); } virtual void add(std::unique_ptr<password_generator>) override { throw std::runtime_error("nie zaimplementowano"); } virtual size_t length() const override final {return len;} }; class digit_generator : public basic_password_generator { public: explicit digit_generator(size_t const len) noexcept : basic_password_generator(len) {} virtual std::string allowed_chars() const override {return "0123456789";} }; class symbol_generator : public basic_password_generator { public: explicit symbol_generator(size_t const len) noexcept : basic_password_generator(len) {} virtual std::string allowed_chars() const override {return "!@#$%^&*(){}[]?<>";} }; class upper_letter_generator : public basic_password_generator { public: explicit upper_letter_generator(size_t const len) noexcept : basic_password_generator(len) {} virtual std::string allowed_chars() const override {return "ABCDEFGHIJKLMNOPQRSTUVWXYZ";} }; class lower_letter_generator : public basic_password_generator { 149 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów public: explicit lower_letter_generator(size_t const len) noexcept : basic_password_generator(len) {} virtual std::string allowed_chars() const override {return "abcdefghijklmnopqrstuvwxyz";} }; class composite_password_generator : public password_generator { virtual std::string allowed_chars() const override { throw std::runtime_error("nie zaimplementowano"); }; virtual size_t length() const override { throw std::runtime_error("nie zaimplementowano"); }; public: composite_password_generator() { auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); eng.seed(seq); } virtual std::string generate() override { std::string password; for(auto & generator : generators) { std::string chars = generator->allowed_chars(); std::uniform_int_distribution<> ud( 0, static_cast<int>(chars.length() - 1)); for(size_t i = 0; i < generator->length(); ++i) password += chars[ud(eng)]; } std::shuffle(std::begin(password), std::end(password), eng); return password; } virtual void add(std::unique_ptr<password_generator> generator) override { generators.push_back(std::move(generator)); } private: std::random_device rd; std::mt19937 eng; std::vector<std::unique_ptr<password_generator>> generators; }; 150 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe Powyższy kod może służyć do generowania haseł w następujący sposób: int main() { composite_password_generator generator; generator.add(std::make_unique<symbol_generator>(2)); generator.add(std::make_unique<digit_generator>(2)); generator.add(std::make_unique<upper_letter_generator>(2)); generator.add(std::make_unique<lower_letter_generator>(4)); auto password = generator.generate(); } Mógłbyś użyć walidatora haseł, który utworzyliśmy w poprzednim zadaniu, aby upewnić się, że wygenerowane w ten sposób hasła rzeczywiście spełniają wymagania. 69. Generowanie numerów ubezpieczenia socjalnego Formaty dla obu krajów są bardzo podobne, chociaż różnią się kilkoma szczegółami: wartością cyfry określającej płeć, długością części losowej, a zatem długością całego numeru, wartością wielokrotności dla sumy kontrolnej. To zadanie można rozwiązać za pomocą wzorca projektowego Metoda Szablonowa, który tworzy szkielet algorytmu i pozwala na nowo zdefiniować poszczególne kroki w klasach podrzędnych: Klasa social_number_generator jest klasą bazową, która zawiera publiczną metodę o nazwie generate() generującą nowy numer ubezpieczenia socjalnego dla określonej płci i daty urodzenia. Ta metoda wywołuje wewnętrznie kilka chronionych metod wirtualnych: sex_digit(), next_random() i modulo_value(). Te wirtualne metody są następnie przesłaniane w dwóch klasach pochodnych — northeria_social_number_generator i southeria_social_number_generator. Ponadto klasa fabryki przechowuje instancje generatorów numerów ubezpieczenia i udostępnia je wywołującym klientom: 151 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów enum class sex_type {female, male}; class social_number_generator { protected: virtual int sex_digit(sex_type const sex) const noexcept = 0; virtual int next_random(unsigned const year, unsigned const month, unsigned const day) = 0; virtual int modulo_value() const noexcept = 0; social_number_generator(int const min, int const max):ud(min, max) { std::random_device rd; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); eng.seed(seq); } public: std::string generate( sex_type const sex, unsigned const year, unsigned const month, unsigned const day) { std::stringstream snumber; snumber << sex_digit(sex); snumber << year << month << day; snumber << next_random(year, month, day); auto number = snumber.str(); auto index = number.length(); auto sum = std::accumulate(std::begin(number), std::end(number), 0, [&index](int const s, char const c) { return s + index-- * (c-'0');}); auto rest = sum % modulo_value(); snumber << modulo_value() - rest; return snumber.str(); } virtual ~social_number_generator() {} protected: std::map<unsigned, int> cache; std::mt19937 eng; std::uniform_int_distribution<> ud; }; 152 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe class southeria_social_number_generator final : public social_number_generator { public: southeria_social_number_generator(): social_number_generator(1000, 9999) { } protected: virtual int sex_digit(sex_type const sex) const noexcept override { if(sex == sex_type::female) return 1; else return 2; } virtual int next_random(unsigned const year, unsigned const month, unsigned const day) override { auto key = year * 10000 + month * 100 + day; while(true) { auto number = ud(eng); auto pos = cache.find(number); if(pos == std::end(cache)) { cache[key] = number; return number; } } } virtual int modulo_value() const noexcept override { return 11; } }; class northeria_social_number_generator final : public social_number_generator { public: northeria_social_number_generator(): social_number_generator(10000, 99999) { } protected: virtual int sex_digit(sex_type const sex) const noexcept override { if(sex == sex_type::female) return 9; else return 7; } virtual int next_random(unsigned const year, unsigned const month, unsigned const day) override 153 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów { auto key = year * 10000 + month * 100 + day; while(true) { auto number = ud(eng); auto pos = cache.find(number); if(pos == std::end(cache)) { cache[key] = number; return number; } } } virtual int modulo_value() const noexcept override { return 11; } }; class social_number_generator_factory { public: social_number_generator_factory() { generators["northeria"] = std::make_unique<northeria_social_number_generator>(); generators["southeria"] = std::make_unique<southeria_social_number_generator>(); } social_number_generator* get_generator(std::string_view country) const { auto it = generators.find(country.data()); if(it != std::end(generators)) return it->second.get(); throw std::runtime_error("błędna nazwa państwa"); } private: std::map<std::string, std::unique_ptr<social_number_generator>> generators; }; Za pomocą powyższego kodu możemy w następujący sposób generować numery ubezpieczenia socjalnego: int main() { setlocale(LC_ALL, "polish"); social_number_generator_factory factory; 154 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe auto sn1 = factory.get_generator("northeria")->generate(sex_type::female, 2017, 12, 25); auto sn2 = factory.get_generator("northeria")->generate(sex_type::female, 2017, 12, 25); auto sn3 = factory.get_generator("northeria")->generate(sex_type::male, 2017, 12, 25); } auto ss1 = factory.get_generator("southeria")->generate(sex_type::female, 2017, 12, 25); auto ss2 = factory.get_generator("southeria")->generate(sex_type::female, 2017, 12, 25); auto ss3 = factory.get_generator("southeria")->generate(sex_type::male, 2017, 12, 25); 70. System zatwierdzania Rozwiązanie tego zadania można wyrazić przy użyciu serii instrukcji if ... else if ... else ... endif. Jego zorientowana obiektowo wersja jest wzorcem projektowym o nazwie Łańcuch Zobowiązań. Ten wzorzec definiuje łańcuch obiektów odbiorczych, na których spoczywa odpowiedzialność za obsługę żądania lub przekazywanie go do następnego odbiorcy, o ile taki istnieje. Poniższy diagram klas pokazuje możliwą implementację wzorca dla tego zadania: 155 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Klasa employee reprezentuje pracownika w firmie. Pracownik może mieć bezpośredniego kierownika, którego definiuje się za pomocą wywołania metody set_direct_manager(). Każdy pracownik ma nazwę i rolę, która określa jego obowiązki oraz uprawnienia. Klasa role jest abstrakcyjną klasą bazową dla możliwych ról i zawiera czysto wirtualną metodę approval_limit(), którą przesłaniają klasy takie jak employee_role, team_manager_role, department_manager_role i president_role w celu zdefiniowania limitu, do którego pracownik może zatwierdzać wydatki. Metoda approve() z klasy employee pozwala pracownikowi na zatwierdzenie wydatku. Jeśli rola pracownika umożliwia takie działanie, operacja zostaje wykonana; w przeciwnym razie wniosek jest przekazywany do bezpośredniego przełożonego (jeśli taki istnieje): class role { public: virtual double approval_limit() const noexcept = 0; virtual ~role() {} }; class employee_role : public role { public: virtual double approval_limit() const noexcept override { return 1000; } }; class team_manager_role : public role { public: virtual double approval_limit() const noexcept override { return 10000; } }; class department_manager_role : public role { public: virtual double approval_limit() const noexcept override { return 100000; } }; class president_role : public role { public: virtual double approval_limit() const noexcept override { return std::numeric_limits<double>::max(); } }; struct expense 156 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe { double amount; std::string description; expense(double const amount, std::string_view desc): amount(amount), description(desc) { } }; class employee { public: explicit employee(std::string_view name, std::unique_ptr<role> ownrole) : name(name), own_role(std::move(ownrole)) { } void set_direct_manager(std::shared_ptr<employee> manager) { direct_manager = manager; } void approve(expense const & e) { if(e.amount <= own_role->approval_limit()) std::cout << name << " approved expense '" << e.description << "', cost=" << e.amount << std::endl; else if(direct_manager != nullptr) direct_manager->approve(e); } private: std::string name; std::unique_ptr<role> own_role; std::shared_ptr<employee> direct_manager; }; W poniższym przykładzie pokazano, w jaki sposób można użyć utworzonego kodu do zatwierdzania wydatków: int main() { setlocale(LC_ALL, "polish"); auto jan = std::make_shared<employee>("Jan Kowalski", std::make_unique<employee_role>()); auto robert = std::make_shared<employee>("Robert Bucik", std::make_unique<team_manager_role>()); auto dawid = std::make_shared<employee>("Dawid Jankowski", std::make_unique<department_manager_role>()); 157 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów auto cezary = std::make_shared<employee>("Cezary Nowak", std::make_unique<president_role>()); jan->set_direct_manager(robert); robert->set_direct_manager(dawid); dawid->set_direct_manager(cezary); jan->approve(expense{500, "czasopisma"}); jan->approve(expense{5000, "pobyt w hotelu"}); jan->approve(expense{50000, "koszty konferencji"}); jan->approve(expense{200000, "nowa ciężarówka"}); } 71. Obserwowany kontener typu wektorowego Obserwowany wektor przedstawiony w tym zadaniu jest typowym przykładem obiektu obserwowanego pochodzącego ze wzorca projektowego Obserwator. Ten wzorzec definiuje obiekt obserwowany, który zarządza listą obiektów zależnych zwanych obserwatorami i powiadamia je o wszelkich zmianach stanu, wywołując jedną z ich metod. Na poniższym diagramie klas pokazano możliwą implementację wzorca dla proponowanego zadania: Klasa observable_vector opakowuje kontener std::vector i udostępnia wymagane operacje. Utrzymuje także listę wskaźników do obiektów typu collection_observer. Jest to klasa podstawowa dla obiektów, które chcą być informowane o wszelkich zmianach stanu w obserwowanym obiekcie observable_vector. Zawiera ona metodę wirtualną o nazwie collection_changed() z argumentem typu collection_changed_notification, który przekazuje informacje o zmianie. Gdy nastąpi jakakolwiek zmiana stanu wewnętrznego obserwowanego wektora, ta metoda jest wywoływana dla wszystkich zarejestrowanych obserwatorów. Obserwatorów można dodać do wektora za pomocą metody add_observer() lub usunąć z niego, wywołując metodę remove_observer(): 158 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe enum class collection_action { add, remove, clear, assign }; std::string to_string(collection_action const action) { switch(action) { case collection_action::add: return "dodanie"; case collection_action::remove: return "usunięcie"; case collection_action::clear: return "wyczyszczenie"; case collection_action::assign: return "przypisanie"; } } struct collection_change_notification { collection_action action; std::vector<size_t> item_indexes; }; class collection_observer { public: virtual void collection_changed( collection_change_notification notification) = 0; virtual ~collection_observer() {} }; template <typename T, class Allocator = std::allocator<T>> class observable_vector final { typedef typename std::vector<T, Allocator>::size_type size_type; public: observable_vector() noexcept(noexcept(Allocator())) : observable_vector( Allocator() ) {} explicit observable_vector( const Allocator& alloc ) noexcept : data(alloc){} observable_vector( size_type count, const T& value, const Allocator& alloc = Allocator()) : data(count, value, alloc){} explicit observable_vector( size_type count, const Allocator& alloc = Allocator() ) :data(count, alloc){} observable_vector(observable_vector&& other) noexcept :data(other.data){} observable_vector(observable_vector&& other, const Allocator& alloc) :data(other.data, alloc){} observable_vector(std::initializer_list<T> init, 159 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów const Allocator& alloc = Allocator()) :data(init, alloc){} template<class InputIt> observable_vector(InputIt first, InputIt last, const Allocator& alloc = Allocator()) :data(first, last, alloc){} observable_vector& operator=(observable_vector const & other) { if(this != &other) { data = other.data; for(auto o : observers) { if(o != nullptr) { o->collection_changed({ collection_action::assign, std::vector<size_t> {} }); } } } return *this; } observable_vector& operator=(observable_vector&& other) { if(this != &other) { data = std::move(other.data); for(auto o : observers) { if(o != nullptr) { o->collection_changed({ collection_action::assign, std::vector<size_t> {} }); } } } return *this; } void push_back(T&& value) { data.push_back(value); for(auto o : observers) 160 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe { if(o != nullptr) { o collection_changed({ collection_action::add, std::vector<size_t> {data.size()-1} }); } } } void pop_back() { data.pop_back(); for(auto o : observers) { if(o != nullptr) { o->collection_changed({ collection_action::remove, std::vector<size_t> {data.size()+1} }); } } } void clear() noexcept { data.clear(); for(auto o : observers) { if(o != nullptr) { o->collection_changed({ collection_action::clear, std::vector<size_t> {} }); } } } size_type size() const noexcept { return data.size(); } [[nodiscard]] bool empty() const noexcept { return data.empty(); } void add_observer(collection_observer * const o) { 161 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów observers.push_back(o); } void remove_observer(collection_observer const * const o) { observers.erase(std::remove(std::begin(observers), std::end(observers), o), std::end(observers)); } private: std::vector<T, Allocator> data; std::vector<collection_observer*> observers; }; class observer : public collection_observer { public: virtual void collection_changed( collection_change_notification notification) override { std::cout << "działanie: " << to_string(notification.action); if(!notification.item_indexes.empty()) { std::cout << ", indeksy: "; for(auto i : notification.item_indexes) std::cout << i << ' '; } std::cout << std::endl; } }; Oto przykłady użycia klasy observable_vector i otrzymywania powiadomień o zmianach w jej stanie wewnętrznym: int main() { setlocale(LC_ALL, "polish"); observable_vector<int> v; observer o; v.add_observer(&o); v.push_back(1); v.push_back(2); v.pop_back(); v.clear(); v.remove_observer(&o); v.push_back(3); 162 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe v.push_back(4); v.add_observer(&o); observable_vector<int> v2 {1,2,3}; v = v2; v = observable_vector<int> {7,8,9}; } W kolejnym ćwiczeniu mógłbyś poszerzyć funkcjonalność klasy observable_vector, na przykład zapewnić dostęp do elementów za pomocą iteratorów. 72. Obliczanie ceny zamówienia z rabatami Przedstawione zadanie można rozwiązać za pomocą wzorca projektowego Strategia. Definiuje on rodzinę algorytmów i sprawia, że są one wymienne w jej obrębie. W tym konkretnym przypadku zarówno rabaty, jak i wyliczenia ostatecznych cen zamówień mogą zostać wdrożone na podstawie wzorca Strategia. Poniższy diagram opisuje hierarchię typów rabatów i ich wymienne użycie w ramach innych klas: customer, article, order_line i order: 163 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Oto implementacja rodzajów rabatów: struct discount_type { virtual double discount_percent( double const price, double const quantity) const noexcept = 0; virtual ~discount_type() {} }; struct fixed_discount final : public discount_type { explicit fixed_discount(double const discount) noexcept : discount(discount) {} virtual double discount_percent( double const, double const) const noexcept {return discount;} private: double discount; }; struct volume_discount final : public discount_type { explicit volume_discount(double const quantity, double const discount) noexcept : discount(discount), min_quantity(quantity) {} virtual double discount_percent( double const, double const quantity) const noexcept {return quantity >= min_quantity ? discount : 0;} private: double discount; double min_quantity; }; struct price_discount : public discount_type { explicit price_discount(double const price, double const discount) noexcept : discount(discount), min_total_price(price) {} virtual double discount_percent( double const price, double const quantity) const noexcept {return price*quantity >= min_total_price ? discount : 0;} private: double discount; double min_total_price; }; struct amount_discount : public discount_type { explicit amount_discount(double const price, double const discount) noexcept 164 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe : discount(discount), min_total_price(price) {} virtual double discount_percent( double const price, double const) const noexcept {return price >= min_total_price ? discount : 0;} private: double discount; double min_total_price; }; Klasy modelujące klientów, artykuły i zamówienia mają minimalną strukturę w celu uproszczenia rozwiązania. Zostały one przedstawione na poniższym listingu: struct customer { std::string name; discount_type* discount; }; enum class article_unit { piece, kg, meter, sqmeter, cmeter, liter }; struct article { int std::string double article_unit discount_type* }; id; name; price; unit; discount; struct order_line { article product; int quantity; discount_type* discount; }; struct order { int customer* std::vector<order_line> discount_type* }; id; buyer; lines; discount; 165 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Aby ustalić ostateczną cenę zamówienia, moglibyśmy użyć różnych algorytmów liczących. Można w tym celu kolejny raz wykorzystać wzorzec Strategia: Klasa price_calculator jest abstrakcyjną klasą bazową, która zawiera czysto wirtualną metodę calcul_price(). Klasy dziedziczące z price_calculator, takie jak cumulative_price_calculator, udostępniają faktyczną implementację algorytmu przez przesłonięcie metody calcul_price(). W tej implementacji, dla uproszczenia, przewidziano tylko jedną konkretną strategię obliczania ceny. W kolejnym ćwiczeniu mógłbyś zaimplementować inne algorytmy: struct price_calculator { virtual double calculate_price(order const & o) = 0; }; struct cumulative_price_calculator : public price_calculator { virtual double calculate_price(order const & o) override { double price = 0; for(auto ol : o.lines) { double line_price = ol.product.price * ol.quantity; if(ol.product.discount != nullptr) line_price *= (1.0 - ol.product.discount->discount_percent( ol.product.price, ol.quantity)); if(ol.discount != nullptr) line_price *= (1.0 - ol.discount->discount_percent( ol.product.price, ol.quantity)); if(o.buyer != nullptr && o.buyer->discount != nullptr) line_price *= (1.0 - o.buyer->discount->discount_percent( ol.product.price, ol.quantity)); price += line_price; } 166 ecb84badecb8c394873734f1e9bfb90f e Rozdział 8. • Wzorce projektowe if(o.discount != nullptr) price *= (1.0 - o.discount->discount_percent(price, 0)); return price; } }; Oto przykłady obliczania ostatecznej ceny zamówienia za pomocą klasy cumulative_price_ calculator: inline bool are_equal(double const d1, double const d2, double const diff = 0.001) { return std::abs(d1 - d2) <= diff; } int main() { setlocale(LC_ALL, "polish"); fixed_discount d1(0.1); volume_discount d2(10, 0.15); price_discount d3(100, 0.05); amount_discount d4(100, 0.05); customer c1 {"domyślny", nullptr}; customer c2 {"Jan", &d1}; customer c3 {"Joanna", &d3}; article a1 {1, "długopis", 5, article_unit::piece, nullptr}; article a2 {2, "drogi długopis", 15, article_unit::piece, &d1}; article a3 {3, "nożyczki", 10, article_unit::piece, &d2}; cumulative_price_calculator calc; order o1 {101, &c1, {{a1, 1, nullptr}}, nullptr}; assert(are_equal(calc.calculate_price(o1), 5)); order o3 {103, &c1, {{a2, 1, nullptr}}, nullptr}; assert(are_equal(calc.calculate_price(o3), 13.5)); order o6 {106, &c1, {{a3, 15, nullptr}}, nullptr}; assert(are_equal(calc.calculate_price(o6), 127.5)); order o9 {109, &c3, {{a2, 20, &d1}}, &d4}; assert(are_equal(calc.calculate_price(o9), 219.3075)); } 167 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 168 ecb84badecb8c394873734f1e9bfb90f e 9 Serializacja danych Zadania 73. Serializacja danych do pliku XML i deserializacja ich z niego Napisz program, który będzie serializować listę filmów do pliku XML, a następnie ją z niego deserializować. Każdy opis filmu będzie zawierać identyfikator numeryczny, tytuł, rok wydania, czas trwania w minutach, listę reżyserów, listę autorów oraz listę ról z imionami i nazwiskami aktorów oraz nazwami postaci. Przykładowy plik XML może wyglądać tak: <?xml version="1.0"?> <movies> <movie id="9871" title="Forrest Gump" year="1994" length="202"> <cast> <role star="Tom Hanks" name="Forrest Gump" /> <role star="Sally Field" name="Pani Gump" /> <role star="Robin Wright" name="Jenny Curran" /> <role star="Mykelti Williamson" name="Bubba Blue" /> </cast> <directors> <director name="Robert Zemeckis" /> </directors> <writers> <writer name="Winston Groom" /> <writer name="Eric Roth" /> </writers> </movie> <!-- więcej filmów --> </movies> ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 74. Pobieranie danych z pliku XML przy użyciu języka XPath Weź pod uwagę plik XML z listą filmów utworzony zgodnie z opisem poprzedniego zadania. Napisz program, który może pobierać i wyświetlać: tytuły wszystkich filmów wydanych po danym roku, imię i nazwisko ostatniego aktora na liście ról dla każdego filmu zawartego w pliku. 75. Serializacja danych do formatu JSON Utwórz program, który do pliku w formacie JSON będzie serializować listę filmów utworzoną zgodnie z definicjami z poprzednich zadań. Każdy film będzie zawierać identyfikator numeryczny, tytuł, rok wydania, czas trwania w minutach, listę reżyserów, listę autorów i listę ról z imionami i nazwiskami aktorów oraz nazwami postaci. Poniżej przedstawiono przykładowy format JSON: { } "movies": [{ "id": 9871, "title": "Forrest Gump", "year": 1994, "length": 202, "cast": [{ "star": "Tom Hanks", "name": "Forrest Gump" }, { "star": "Sally Field", "name": "Pani Gump" }, { "star": "Robin Wright", "name": "Jenny Curran" }, { "star": "Mykelti Williamson", "name": "Bubba Blue" } ], "directors": ["Robert Zemeckis"], "writers": ["Winston Groom", "Eric Roth"] }] 76. Deserializacja danych z formatu JSON Weź pod uwagę plik JSON z listą filmów utworzony zgodnie z opisem poprzedniego zadania. Napisz program, który może deserializować zawartość tego pliku. 170 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych 77. Tworzenie pliku PDF z listą filmów Napisz program, który może utworzyć plik PDF zawierający listę filmów w formie tabelarycznej. Oto wymagania dotyczące tego programu: Nagłówek listy powinien mieć tytuł Lista filmów. Musi się on pojawić tylko na pierwszej stronie dokumentu. Dla każdego filmu powinna zostać podana informacja o jego tytule, roku wydania i czasie trwania. Tytuł, po którym następuje rok wydania w nawiasach, musi zostać wyrównany do lewej. Czas trwania filmu w godzinach i minutach (na przykład 2:12) musi zostać wyrównany do prawej. Na każdej stronie musi zostać umieszczona linia nad i pod listą filmów. Oto wygląd przykładowego pliku PDF: 78. Tworzenie pliku PDF na podstawie zbioru obrazów Napisz program, który może utworzyć dokument PDF zawierający obrazy z katalogu określonego przez użytkownika. Obrazy muszą zostać umieszczone w pliku jeden po drugim. Jeśli obraz nie mieści się na pozostałej części strony, należy go ulokować na następnej stronie. Poniżej przedstawiono przykład takiego pliku PDF utworzonego na podstawie kilku zdjęć prezentujących Alberta Einsteina (zdjęcia zostały umieszczone wraz z kodem źródłowym dołączonym do książki): 171 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Rozwiązania 73. Serializacja danych do pliku XML i deserializacja ich z niego Standardowa biblioteka C++ nie obsługuje formatu XML, lecz istnieje wiele bibliotek wieloplatformowych o otwartym kodzie, z których można skorzystać. Niektóre z nich są lekkie i obsługują tylko podstawowe funkcje XML, inne zaś są bardziej złożone i bogate w funkcje. Ty zdecydujesz, która biblioteka będzie najbardziej odpowiednia dla danego projektu. Pod uwagę możesz wziąć następujące biblioteki: Xerces-C++, libxml++, tinyxml lub tinyxml2, pugixml, gSOAP i RapidXml. Do rozwiązania tego zadania wybrałem bibliotekę pugixml. Jest to wieloplatformowa, lekka biblioteka z szybkim analizatorem składni XML, który jednak nie sprawdza jej poprawności. Ma ona interfejs podobny do obiektowego modelu dokumentu (DOM) z bogatymi możliwościami przemieszczania się po pliku XML i jego modyfikacji, a także wspiera Unicode i XPath 1.0. Jej ograniczeniem jest brak sprawdzania poprawności schematu. Biblioteka pugixml jest dostępna pod adresem https://pugixml.org/. Aby przechowywać filmy zgodnie z opisem zadania, użyjemy następujących struktur: struct casting_role { std::string actor; std::string role; }; struct movie { unsigned int std::string unsigned int unsigned int std::vector<casting_role> std::vector<std::string> std::vector<std::string> }; id; title; year; length; cast; directors; writers; using movie_list = std::vector<movie>; Aby utworzyć dokument XML, musisz użyć klasy pugi::xml_document. Po skonstruowaniu drzewa DOM możesz je zapisać w pliku, wywołując metodę save_file(). Za pomocą metody append_child() można dodawać węzły, a przy użyciu append_attribute() tworzyć atrybuty. Poniższa metoda serializuje listę filmów w żądanym formacie: void serialize(movie_list const & movies, std::string_view filepath) { pugi::xml_document doc; auto root = doc.append_child("movies"); 172 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych for (auto const & m : movies) { auto movie_node = root.append_child("movie"); movie_node.append_attribute("id").set_value(m.id); movie_node.append_attribute("title").set_value(m.title.c_str()); movie_node.append_attribute("year").set_value(m.year); movie_node.append_attribute("length").set_value(m.length); auto cast_node = movie_node.append_child("cast"); for (auto const & c : m.cast) { auto node = cast_node.append_child("role"); node.append_attribute("star").set_value(c.actor.c_str()); node.append_attribute("name").set_value(c.role.c_str()); } auto directors_node = movie_node.append_child("directors"); for (auto const & director : m.directors) { directors_node.append_child("director") .append_attribute("name") .set_value(director.c_str()); } auto writers_node = movie_node.append_child("writers"); for (auto const & writer : m.writers) { writers_node.append_child("writer") .append_attribute("name") .set_value(writer.c_str()); } } doc.save_file(filepath.data()); } W przypadku odwrotnej operacji możesz wczytać zawartość pliku XML do struktury pugi::xml_document, wywołując metodę load_file(). Dostęp do węzłów można uzyskać, wywołując metody takie jak child() i next_sibling(). Odpowiedni dostęp do atrybutów uzyskuje się po wywołaniu metody attribute(). Przedstawiona poniżej metoda deserialize() wczytuje drzewo DOM i tworzy listę filmów: movie_list deserialize(std::string_view filepath) { pugi::xml_document doc; movie_list movies; auto result = doc.load_file(filepath.data()); if (result) { auto root = doc.child("movies"); for (auto movie_node = root.child("movie"); movie_node; 173 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów movie_node = movie_node.next_sibling("movie")) { movie m; m.id = movie_node.attribute("id").as_uint(); m.title = movie_node.attribute("title").as_string(); m.year = movie_node.attribute("year").as_uint(); m.length = movie_node.attribute("length").as_uint(); for (auto role_node : movie_node.child("cast").children("role")) { m.cast.push_back(casting_role{ role_node.attribute("star").as_string(), role_node.attribute("name").as_string() }); } for (auto director_node : movie_node.child("directors").children("director")) { m.directors.push_back( director_node.attribute("name").as_string()); } for (auto writer_node : movie_node.child("writers").children("writer")) { m.writers.push_back( writer_node.attribute("name").as_string()); } movies.push_back(m); } } return movies; } Na poniższym listingu pokazano przykład użycia utworzonych wcześniej funkcji: int main() { movie_list movies { { 11001, "Matrix",1999, 196, { {"Keanu Reeves", "Neo"}, {"Laurence Fishburne", "Morfeusz"}, {"Carrie-Anne Moss", "Trinity"}, {"Hugo Weaving", "Agent Smith"} }, {"Lana Wachowski", "Lilly Wachowski"}, {"Lana Wachowski", "Lilly Wachowski"}, }, { 9871, "Forrest Gump", 1994, 202, { {"Tom Hanks", "Forrest Gump"}, 174 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych {"Sally Field", "Pani Gump"}, {"Robin Wright","Jenny Curran"}, {"Mykelti Williamson","Bubba Blue"} }, {"Robert Zemeckis"}, {"Winston Groom", "Eric Roth"}, } }; serialize(movies, "movies.xml"); auto result = deserialize("movies.xml"); assert(result.size() == 2); assert(result[0].title == "Matrix"); assert(result[1].title == "Forrest Gump"); } 74. Pobieranie danych z pliku XML przy użyciu języka XPath Nawigowanie po elementach i atrybutach pliku XML może się odbywać przy użycia języka XPath. Stosowane są w tym celu wyrażenia XPath. Biblioteka pugixml obsługuje wyrażenia XPath i zawiera długą listę wbudowanych funkcji. Aby rozwiązać nasze zadanie, można użyć metody select_nodes() z klasy xml_document. Zwróć uwagę na to, że w przypadku, gdy podczas wyboru XPath wystąpi błąd, wygenerowany zostanie wyjątek xpath_exception. Do wybierania węzłów zgodnie z wymaganiami zadania mogą zostać użyte następujące wyrażenia XPath: dla wszystkich filmów wydanych po danym roku (w naszym przykładzie chodzi o rok 1995): /movies/movie[@year>1995], dla ostatniej roli w każdym z filmów: /movies/movie/cast/role[last()]. Poniższy program wczytuje dokument XML z bufora znakowego, a następnie dokonuje wyboru węzła za pomocą wymienionych wcześniej wyrażeń XPath. Dokument XML został zdefiniowany w następujący sposób: std::string text = R"( <?xml version="1.0"?> <movies> <movie id="11001" title="The Matrix" year="1999" length="196"> <cast> <role star="Keanu Reeves" name="Neo" /> <role star="Laurence Fishburne" name="Morfeusz" /> <role star="Carrie-Anne Moss" name="Trinity" /> <role star="Hugo Weaving" name="Agent Smith" /> </cast> <directors> <director name="Lana Wachowski" /> <director name="Lilly Wachowski" /> </directors> <writers> <writer name="Lana Wachowski" /> <writer name="Lilly Wachowski" /> 175 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów </writers> </movie> <movie id="9871" title="Forrest Gump" year="1994" length="202"> <cast> <role star="Tom Hanks" name="Forrest Gump" /> <role star="Sally Field" name="Pani Gump" /> <role star="Robin Wright" name="Jenny Curran" /> <role star="Mykelti Williamson" name="Bubba Blue" /> </cast> <directors> <director name="Robert Zemeckis" /> </directors> <writers> <writer name="Winston Groom" /> <writer name="Eric Roth" /> </writers> </movie> </movies> )"; Wybór żądanych danych może nastąpić w poniższy sposób: pugi::xml_document doc; if (doc.load_string(text.c_str())) { try { auto titles = doc.select_nodes("/movies/movie[@year>1995]"); for (auto it : titles) { std::cout << it.node().attribute("title").as_string() << std::endl; } } catch (pugi::xpath_exception const & e) { std::cout << e.result().description() << std::endl; } try { auto titles = doc.select_nodes("/movies/movie/cast/role[last()]"); for (auto it : titles) { std::cout << it.node().attribute("star").as_string() << std::endl; } } catch (pugi::xpath_exception const & e) { std::cout << e.result().description() << std::endl; } } 176 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych 75. Serializacja danych do formatu JSON Podobnie jak w przypadku XML, w języku C++ nie istnieje standardowa obsługa formatu JSON. Dostępnych jest jednak dużo bibliotek wieloplatformowych przeznaczonych do tego celu. W chwili pisania tego rozdziału projekt nativejson-benchmark, dostępny pod adresem https://github.com/miloyip/nativejson-benchmark, zawierał listę ponad 40 bibliotek. Projekt ten jest testem porównawczym, który dla bibliotek C i C++ o otwartym kodzie źródłowym sprawdza zgodność i wydajność (szybkość, zajętość pamięci i wielkość kodu) dotyczące parsowania i generowania formatu JSON. Powoduje to, że wybór odpowiedniej biblioteki może być trochę trudniejszym zadaniem, chociaż najlepszymi kandydatami mogłyby być takie produkty jak RapidJSON, NLohmann, taocpp/json, Configuru, json_spirit czy jsoncpp. Do rozwiązania niniejszego zadania wykorzystamy bibliotekę nlohmann/json. Jest to wyłącznie nagłówkowa biblioteka wieloplatformowa utworzona dla języka C++ 11, z intuicyjną składnią i dobrą dokumentacją. Jest ona dostępna pod adresem https://github.com/nlohmann/json. Do przechowywania filmów użyjemy tych samych struktur danych, które wykorzystaliśmy do rozwiązania zadania „Serializacja danych do pliku XML i deserializacja ich z niego”. Biblioteka nlohmann używa klasy nlohmann::json jako głównego typu danych przeznaczonego do reprezentowania obiektów JSON. Chociaż możesz tworzyć wartości JSON z bardziej oczywistą składnią, istnieją również niejawne konwersje w obie strony na typy skalarne i standardowe kontenery. Ponadto można również umożliwić użycie tej niejawnej konwersji w typach niestandardowych poprzez udostępnienie metod to_json() i from_json() w przestrzeni nazw przekształcanego typu. Istnieją pewne wymagania dotyczące tych funkcji, o których to wymaganiach można przeczytać w dokumentacji. Powyższe podejście zostało zaimplementowane w zaprezentowanym kodzie. Ponieważ typy movie i casting_role zostały zdefiniowane w globalnej przestrzeni nazw, są tam też obecne przeciążenia funkcji to_json(), które służą do ich serializacji. Z drugiej strony typ movie_list jest faktycznie aliasem typu std::vector<movie> i może być bezpośrednio serializowany oraz deserializowany, ponieważ jak wspomniano wcześniej, biblioteka obsługuje niejawne konwersje dwukierunkowe dla standardowych kontenerów: using json = nlohmann::json; void to_json(json& j, casting_role const & c) { j = json{ {"star", c.actor}, {"name", c.role} }; } void to_json(json& j, movie const & m) { j = json::object({ {"id", m.id}, {"title", m.title}, {"year", m.year}, {"length", m.length}, {"cast", m.cast }, {"directors", m.directors}, 177 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów {"writers", m.writers} }); } void serialize(movie_list const & movies, std::string_view filepath) { json jdata{ { "movies", movies } }; std::ofstream ofile(filepath.data()); if (ofile.is_open()) { ofile << std::setw(2) << jdata << std::endl; } } Funkcja serialize() może zostać użyta w sposób przedstawiony w następującym przykładzie: int main() { movie_list movies { { 11001, "The Matrix", 1999, 196, { {"Keanu Reeves", "Neo"}, {"Laurence Fishburne", "Morfeusz"}, {"Carrie-Anne Moss", "Trinity"}, {"Hugo Weaving", "Agent Smith"} }, {"Lana Wachowski", "Lilly Wachowski"}, {"Lana Wachowski", "Lilly Wachowski"}, }, { 9871, "Forrest Gump", 1994, 202, { {"Tom Hanks", "Forrest Gump"}, {"Sally Field", "Pani Gump"}, {"Robin Wright","Jenny Curran"}, {"Mykelti Williamson","Bubba Blue"} }, {"Robert Zemeckis"}, {"Winston Groom", "Eric Roth"}, } }; serialize(movies, "movies.json"); } 76. Deserializacja danych z formatu JSON W celu rozwiązania tego zadania ponownie użyjemy biblioteki nlohmann/json. Zamiast tworzyć funkcje from_json(), co zastosowano w rozwiązaniu poprzedniego zadania, przyjmiemy bardziej jednoznaczne podejście. Zawartość pliku JSON można wczytać do obiektu nlohmann::json za pomocą przeciążonego operatora >>. Aby uzyskać dostęp do wartości obiektu, należy zamiast operatora [] użyć metody at(), ponieważ zgłasza ona wyjątek (który możesz obsłużyć) 178 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych w przypadku, gdy klucz nie istnieje. Zastosowanie operatora [] wykazuje w tej sytuacji niezdefiniowane zachowanie. Aby pobrać wartość obiektu w postaci określonego obiektu T, użyj metody get<T>(). Wymaga to jednak, aby typ T był domyślnie konstruowalny. Przedstawiona poniżej funkcja deserialize() zwraca obiekt std::vector<movie> utworzony na podstawie zawartości określonego pliku JSON: using json = nlohmann::json; movie_list deserialize(std::string_view filepath) { movie_list movies; std::ifstream ifile(filepath.data()); if (ifile.is_open()) { json jdata; try { ifile >> jdata; if (jdata.is_object()) { for (auto & element : jdata.at("movies")) { movie m; m.id = element.at("id").get<unsigned int>(); m.title = element.at("title").get<std::string>(); m.year = element.at("year").get<unsigned int>(); m.length = element.at("length").get<unsigned int>(); for (auto & role : element.at("cast")) { m.cast.push_back(casting_role{ role.at("star").get<std::string>(), role.at("name").get<std::string>() }); } for (auto & director : element.at("directors")) { m.directors.push_back(director); } for (auto & writer : element.at("writers")) { m.writers.push_back(writer); } movies.push_back(m); } } } 179 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów catch (std::exception const & ex) { std::cout << ex.what() << std::endl; } } return movies; } Ta funkcja deserializacyjna może zostać użyta w następujący sposób: int main() { auto movies = deserialize("movies.json"); assert(movies.size() == 2); assert(movies[0].title == "Matrix"); assert(movies[1].title == "Forrest Gump"); } 77. Tworzenie pliku PDF z listą filmów Istnieje wiele bibliotek C++ służących do pracy z plikami PDF. Wśród nich można wymienić HaHu, PoDoFo, JagPDF i PDF-Writer (znaną również jako Hummus) — to przykłady wieloplatformowych bibliotek o otwartym kodzie, które można wykorzystać w celu rozwiązania naszego zadania. W tej książce użyłem produktu PDF-Writer, dostępnego pod adresem https://github.com/galkahana/PDF-Writer. Jest to bezpłatna, szybka i rozszerzalna biblioteka z podstawowym zestawem funkcji obejmującym obsługę tekstu, obrazów i kształtów za pomocą zarówno operatorów PDF, jak i funkcji wyższego poziomu (których użyję do rozwiązania tego zadania). Przedstawiona poniżej funkcja print_pdf() implementuje następujący algorytm: Tworzy nowy dokument PDF przy użyciu metody PDFWriter::StartPDF(). Umieszcza maksymalnie 25 tytułów filmów na każdej stronie dokumentu. Każda strona jest reprezentowana przez obiekt PDFPage(). Zawiera ona obiekt PageContentContext, który jest tworzony za pomocą metody PDFPage::StartPage ContentContext() i służy do rysowania elementów na stronie. Na pierwszej stronie umieszcza nagłówek o treści Lista filmów. Tekst jest zapisywany na stronie za pomocą funkcji PageContentContext::WriteText(). Informacje o filmie są zapisywane przy użyciu różnych czcionek. Linie wyświetlane na górze i dole listy filmów dla każdej ze stron są tworzone przy użyciu metody PageContentContext::DrawPath(). Metody PDFWriter::EndPageContentContext() i PDFWriter::WritePageAndRelease() muszą zostać wywołane po zakończeniu zapisywania treści na stronie. Funkcja PDFWriter::EndPDF() musi zostać wywołana po zakończeniu tworzenia dokumentu PDF. 180 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych Aby uzyskać informacje o typach i metodach używanych w poniższym kodzie, a także więcej wiedzy na temat tworzenia dokumentów PDF i pracy z tekstem, kształtami oraz obrazami, zapoznaj się z dokumentacją projektu dostępną (w języku angielskim) na stronie https://github.com/galkahana/PDF-Writer/wiki. #ifdef _WIN32 static const std::string fonts_dir = R"(c:\windows\fonts\)"; #elif defined (__APPLE__) static const std::string fonts_dir = R"(/Library/Fonts/)"; #else static const std::string fonts_dir = R"(/usr/share/fonts)"; #endif void print_pdf(movie_list const & movies, std::string_view path) { const int height = 842; const int width = 595; const int left = 60; const int top = 770; const int right = 535; const int bottom = 60; const int line_height = 28; PDFWriter pdf; pdf.StartPDF(path.data(), ePDFVersion13); auto font = pdf.GetFontForFile(fonts_dir + "arial.ttf"); AbstractContentContext::GraphicOptions pathStrokeOptions( AbstractContentContext::eStroke, AbstractContentContext::eRGB, 0xff000000, 1); PDFPage* page = nullptr; PageContentContext* context = nullptr; int index = 0; for (size_t i = 0; i < movies.size(); ++i) { index = i % 25; if (index == 0) { if (page != nullptr) { DoubleAndDoublePairList pathPoints; pathPoints.push_back(DoubleAndDoublePair(left, bottom)); pathPoints.push_back(DoubleAndDoublePair(right, bottom)); context->DrawPath(pathPoints, pathStrokeOptions); pdf.EndPageContentContext(context); pdf.WritePageAndRelease(page); } page = new PDFPage(); page->SetMediaBox(PDFRectangle(0, 0, width, height)); 181 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów context = pdf.StartPageContentContext(page); { DoubleAndDoublePairList pathPoints; pathPoints.push_back(DoubleAndDoublePair(left, top)); pathPoints.push_back(DoubleAndDoublePair(right, top)); context->DrawPath(pathPoints, pathStrokeOptions); } } if (i == 0) { AbstractContentContext::TextOptions const textOptions( font, 26, AbstractContentContext::eGray, 0); context->WriteText(left, top + 15, "List of movies", textOptions); } auto textw = 0; { AbstractContentContext::TextOptions const textOptions( font, 20, AbstractContentContext::eGray, 0); context->WriteText(left, top - 20 - line_height * index, movies[i].title, textOptions); auto textDimensions = font->CalculateTextDimensions( movies[i].title, 20); textw = textDimensions.width; } { AbstractContentContext::TextOptions const textOptions( font, 16, AbstractContentContext::eGray, 0); context->WriteText(left + textw + 5, top - 20 - line_height * index, " (" + std::to_string(movies[i].year) + ")", textOptions); std::stringstream s; s << movies[i].length / 60 << ':' << std::setw(2) << std::setfill('0') << movies[i].length % 60; context->WriteText(right - 30, top - 20 - line_height * index, s.str(), textOptions); } } DoubleAndDoublePairList pathPoints; pathPoints.push_back( DoubleAndDoublePair(left, top - line_height * (index + 1))); pathPoints.push_back( DoubleAndDoublePair(right, top - line_height * (index + 1))); 182 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych context->DrawPath(pathPoints, pathStrokeOptions); if (page != nullptr) { pdf.EndPageContentContext(context); pdf.WritePageAndRelease(page); } pdf.EndPDF(); } Funkcja print_pdf() może zostać użyta w następujący sposób: int main() { movie_list movies { { 1, "Matrix", 1999, 136}, { 2, "Forrest Gump", 1994, 142}, // …inne filmy { 28, "Tajemnice Los Angeles", 1997, 138 }, { 29, "Wyspa tajemnic", 2010, 138 }, }; print_pdf(movies, "movies.pdf"); } 78. Tworzenie pliku PDF na podstawie zbioru obrazów Aby rozwiązać to zadanie, użyjemy tej samej biblioteki PDF-Writer, którą wykorzystaliśmy już wcześniej. Proponuję więc, abyś, zanim będziesz kontynuować czytanie tej książki, najpierw przeanalizował i zaimplementował rozwiązanie poprzedniego zadania, jeśli jeszcze tego nie zrobiłeś. Poniższa funkcja get_images() zwraca wektor łańcuchów reprezentujących ścieżki do wszystkich obrazów JPG znajdujących się w określonym katalogu: namespace fs = std::experimental::filesystem; std::vector<std::string> get_images(fs::path const & dirpath) { std::vector<std::string> paths; for (auto const & p : fs::directory_iterator(dirpath)) { if (p.path().extension() == ".jpg") paths.push_back(p.path().string()); } return paths; } 183 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Funkcja print_pdf() tworzy dokument PDF zawierający wszystkie obrazy JPG z określonego katalogu. Implementuje ona następujący algorytm: Tworzy nowy dokument PDF przy użyciu metody PDFWriter::StartPDF(). Tworzy stronę, a następnie umieszcza na niej tyle obrazów, ile się zmieści, układając je pionowo jeden pod drugim. Gdy nowy obraz nie mieści się już na bieżącej stronie, kończy jej modyfikowanie za pomocą metod PDFWriter::EndPageContentContext() i PDFWriter::SavePage AndRelease(), a następnie rozpoczyna generowanie nowej strony. Zapisuje obrazy na stronie za pomocą metody PageContentContext::DrawImage(). Kończy tworzenie dokumentu, wywołując metodę PDFWriter::EndPDF(). void print_pdf(fs::path const & pdfpath, fs::path const & dirpath) { const int height = 842; const int width = 595; const int margin = 20; auto image_paths = get_images(dirpath); PDFWriter pdf; pdf.StartPDF(pdfpath.string(), ePDFVersion13); PDFPage* page = nullptr; PageContentContext* context = nullptr; auto top = height - margin; for (size_t i = 0; i < image_paths.size(); ++i) { auto dims = pdf.GetImageDimensions(image_paths[i]); if (i == 0 || top - dims.second < margin) { if (page != nullptr) { pdf.EndPageContentContext(context); pdf.WritePageAndRelease(page); } page = new PDFPage(); page->SetMediaBox(PDFRectangle(0, 0, width, height)); context = pdf.StartPageContentContext(page); top = height - margin; } context->DrawImage(margin, top - dims.second, image_paths[i]); top -= dims.second + margin; } 184 ecb84badecb8c394873734f1e9bfb90f e Rozdział 9. • Serializacja danych if (page != nullptr) { pdf.EndPageContentContext(context); pdf.WritePageAndRelease(page); } pdf.EndPDF(); } Funkcja print_pdf() może zostać użyta tak, jak przedstawiono w poniższym przykładzie, przy czym sample.pdf jest nazwą pliku wyjściowego, a res nazwą folderu zawierającego obrazy: int main() { print_pdf("sample.pdf", "res"); } 185 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 186 ecb84badecb8c394873734f1e9bfb90f e 10 Archiwa, obrazy i bazy danych Zadania 79. Wyszukiwanie plików w archiwum ZIP Utwórz program wyszukujący i wyświetlający wszystkie pliki zawarte w archiwum ZIP, których nazwy pasują do wyrażenia regularnego dostarczonego przez użytkownika (na przykład użyj wyrażenia ^.*\.jpg$, aby znaleźć wszystkie pliki z rozszerzeniem .jpg). 80. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum Napisz program, który wykonuje następujące działania: Pakuje do archiwum ZIP pojedynczy plik lub w sposób rekurencyjny — zawartość katalogu określonego przez użytkownika. Wypakowuje zawartość archiwum ZIP w określonym przez użytkownika katalogu docelowym. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 81. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum z zastosowaniem hasła Napisz program, który wykonuje następujące działania: Pakuje do zabezpieczonego hasłem archiwum ZIP pojedynczy plik lub w sposób rekurencyjny — zawartość katalogu określonego przez użytkownika. Wypakowuje zawartość zabezpieczonego hasłem archiwum ZIP w określonym przez użytkownika katalogu docelowym. 82. Tworzenie pliku PNG z flagą narodową Napisz program, który generuje plik PNG z pokazaną poniżej polską flagą narodową. Rozmiar obrazu w pikselach, a także ścieżka do pliku docelowego powinny zostać przekazane przez użytkownika: 83. Tworzenie obrazu PNG zawierającego tekst weryfikacyjny Utwórz program, który może tworzyć obrazy PNG podobne do używanych przez aplikację Captcha w celu weryfikacji użytkowników w systemie. Taki obraz powinien się charakteryzować następującymi cechami: posiadać tło wypełnione płynnym przejściem kolorów w postaci gradientu; zawierać serię losowych liter wyświetlanych pod różnymi kątami zarówno w prawej, jak i w lewej części obrazu; zawierać kilka losowych linii o różnych kolorach (nałożonych na tekst). Oto przykład takiego obrazu: 188 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych 84. Generator kodów kreskowych EAN-13 Napisz program, który dla dowolnego międzynarodowego numeru towaru może wygenerować obraz PNG z kodem kreskowym EAN-13 w 13. wersji standardu. Dla uproszczenia obraz powinien zawierać tylko kod kreskowy, więc można w nim pominąć numer EAN-13 znajdujący się oryginalnie pod nim. Oto przykład oczekiwanego wyniku dla numeru 5901234123457: 85. Odczytywanie informacji o filmach z bazy SQLite Napisz program, który odczytuje informacje o filmach z bazy danych SQLite i wyświetla je w konsoli. Każdy film musi się charakteryzować identyfikatorem numerycznym, tytułem, rokiem wydania, czasem trwania w minutach, listą reżyserów, listą autorów i obsadą zawierającą zarówno imiona i nazwiska aktorów, jak i nazwy postaci. Poniżej przedstawiono schemat bazy danych, który powinien zostać użyty w tym celu: 86. Wstawianie w sposób transakcyjny informacji o filmach do bazy danych SQLite Rozwiń program napisany w poprzednim zadaniu, by mógł dodawać nowe filmy do bazy danych. Filmy można wczytywać z konsoli lub alternatywnie z pliku tekstowego. Wstawienie danych filmowych do kilku tabel w bazie danych musi być wykonywane w sposób transakcyjny. 189 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 87. Obsługa multimediów w bazie danych SQLite Zmodyfikuj program napisany w poprzednim zadaniu, aby umożliwić dodawanie plików multimedialnych (takich jak obrazy, pliki wideo) do opisu filmów. Pliki te muszą być przechowywane w oddzielnej tabeli bazy danych, która powinna zawierać unikatowy identyfikator numeryczny, identyfikator filmu, nazwę (zazwyczaj nazwę pliku), opcjonalny opis oraz faktyczną zawartość multimedialną, przechowywaną jako obiekt typu blob. Poniżej przedstawiono diagram ze strukturą tabeli, która musi zostać dodana do istniejącej bazy danych: Program będący rozwiązaniem zadania musi obsługiwać następujące polecenia: wyświetlać wszystkie filmy spełniające kryteria wyszukiwania (w szczególności chodzi o tytuł); wyświetlać informacje o wszystkich istniejących plikach multimedialnych związanych z danym filmem; dodawać nowy plik multimedialny do filmu; usuwać istniejący plik multimedialny. 190 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych Rozwiązania 79. Wyszukiwanie plików w archiwum ZIP Istnieje wiele bibliotek, które zapewniają wsparcie dla pracy z archiwami ZIP. Spośród darmowych bibliotek najbardziej popularnymi są ZipLib, Info-Zip, MiniZip i LZMA SDK z 7z. Istnieją także produkty komercyjne. Do zadań związanych z archiwami ZIP dla potrzeb tej książki wybrałem bibliotekę ZipLib. Jest to lekka, wieloplatformowa biblioteka C++ 11 o otwartych źródłach, wykorzystująca strumienie biblioteki standardowej i niezawierająca dodatkowych zależności. Wraz z dokumentacją jest ona dostępna na stronie https://bitbucket.org/ wbenny/ziplib. Aby zaimplementować wymaganą funkcjonalność, musisz: Otworzyć archiwum ZIP za pomocą metody ZipFile::Open(). Wyszukać wszystkie wpisy w archiwum za pomocą metod ZipArchive::GetEntry() i ZipArchive::GetEntryCount(). W przypadku wszystkich pozycji reprezentujących pliki sprawdzić, czy nazwa pasuje do podanego wyrażenia regularnego, używając metody ZipArchiveEntry::GetName(). Dla wszystkich pozycji pasujących do wyrażenia regularnego dodać ich pełne nazwy do listy wyników za pomocą metody ZipArchiveEntry::GetFullName(). Implementacją opisanego algorytmu jest przedstawiona poniżej funkcja find_in_archive(): namespace fs = std::experimental::filesystem; std::vector<std::string> find_in_archive( fs::path const & archivepath, std::string_view pattern) { std::vector<std::string> results; if (fs::exists(archivepath)) { try { auto archive = ZipFile::Open(archivepath.string()); for (size_t i = 0; i < archive->GetEntriesCount(); ++i) { auto entry = archive->GetEntry(i); if (entry) { if (!entry->IsDirectory()) { auto name = entry->GetName(); if (std::regex_match(name, 191 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów std::regex{ pattern.data() })) { results.push_back(entry->GetFullName()); } } } } } catch (std::exception const & ex) { std::cout << ex.what() << std::endl; } } return results; } W poniższym przykładzie pokazano, jak można wyszukać wszystkie pliki z rozszerzeniem .jpg znajdujące się w archiwum ZIP o nazwie sample79.zip. Plik ten jest dostępny wraz z kodem źródłowym dołączonym do książki: int main() { auto results = find_in_archive("sample79.zip", R"(^.*\.jpg$)")); for(auto const & name : results) { std::cout << name << std::endl; } } 80. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum Aby rozwiązać to dwuczęściowe zadanie, użyjemy tej samej biblioteki ZipLib, którą stosowaliśmy do rozwiązania poprzedniego problemu. Rozwiązanie tego zadania składa się z dwóch funkcji: jednej, która będzie w stanie przeprowadzić pakowanie do archiwum ZIP, oraz drugiej, która będzie wykonywać wypakowywanie z archiwum ZIP. Aby zaimplementować wymaganą funkcję pakującą, wykonaj następujące czynności: Jeśli ścieżka źródłowa reprezentuje zwykły plik, dodaj go do archiwum ZIP za pomocą metody ZipFile::AddFile(). Jeśli ścieżka źródłowa reprezentuje rekurencyjny katalog, to: Przetwórz rekurencyjnie wszystkie wpisy w katalogu. Jeśli pozycja jest katalogiem, utwórz wpis katalogu w archiwum ZIP, używając metody ZipArchive::CreateEntry(). Jeśli pozycja jest zwykłym plikiem, dodaj go do archiwum ZIP za pomocą metody ZipFile::AddFile(). 192 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych Podany algorytm implementuje przedstawiona poniżej funkcja compress(). Ma ona trzy parametry: pierwszy to ścieżka do pliku lub folderu przeznaczonego do spakowania, drugi to ścieżka do docelowego archiwum ZIP, natomiast trzeci jest obiektem funkcji używanym do raportowania postępu operacji (na przykład funkcją, która wyświetla komunikat w konsoli): namespace fs = std::experimental::filesystem; void compress(fs::path const & source, fs::path const & archive, std::function<void(std::string_view)> reporter) { if (fs::is_regular_file(source)) { if (reporter) reporter("Pakowanie " + source.string()); ZipFile::AddFile(archive.string(), source.string(), LzmaMethod::Create()); } else { for (auto const & p : fs::recursive_directory_iterator(source)) { if (reporter) reporter("Pakowanie " + p.path().string()); if (fs::is_directory(p)) { auto zipArchive = ZipFile::Open(archive.string()); auto entry = zipArchive->CreateEntry(p.path().string()); entry->SetAttributes(ZipArchiveEntry::Attributes::Directory); ZipFile::SaveAndClose(zipArchive, archive.string()); } else if (fs::is_regular_file(p)) { ZipFile::AddFile(archive.string(), p.path().string(), LzmaMethod::Create()); } } } } Aby zaimplementować operację odwrotną, czyli wypakowywanie, musimy wykonać następujące czynności: Otworzyć archiwum ZIP za pomocą metody ZipFile::Open(). Przetworzyć wszystkie wpisy w archiwum za pomocą metod ZipArchive::GetEntriesCount() i ZipArchive::GetEntry(). Jeśli pozycja jest katalogiem, utworzyć go rekurencyjnie w ścieżce docelowej. Jeśli pozycja jest plikiem, utworzyć odpowiedni wpis w miejscu docelowym, a następnie skopiować zawartość spakowanego pliku za pomocą metody ZipArchiveEntry::GetDecompressionStream(). 193 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Omówiony algorytm implementuje pokazana poniżej funkcja decompress(). Jej parametry są podobne do metody compress(): pierwszy to ścieżka do katalogu docelowego, drugi to ścieżka do archiwum ZIP przeznaczonego do rozpakowania, trzeci zaś jest obiektem funkcji używanym do raportowania postępu operacji: void decompress(fs::path const & destination, fs::path const & archive, std::function<void(std::string_view)> reporter) { ensure_directory_exists(destination); auto zipArchive = ZipFile::Open(archive.string()); for (int i = 0; i < zipArchive->GetEntriesCount(); ++i) { auto entry = zipArchive->GetEntry(i); if (entry) { auto filepath = destination / fs::path{ entry->GetFullName() }.relative_path(); if (reporter) reporter("Tworzenie " + filepath.string()); if (entry->IsDirectory()) { ensure_directory_exists(filepath); } else { ensure_directory_exists(filepath.parent_path()); std::ofstream destFile; destFile.open(filepath.string().c_str(), std::ios::binary | std::ios::trunc); if (!destFile.is_open()) { if(reporter) reporter("Nie można utworzyć pliku docelowego!"); } auto dataStream = entry->GetDecompressionStream(); if (dataStream) { utils::stream::copy(*dataStream, destFile); } } } } } 194 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych Powyższa funkcja używa metody ensure_directory_exists(), aby rekurencyjnie utworzyć ścieżkę do katalogu, jeśli taka jeszcze nie istnieje. Implementacja tej metody jest następująca: void ensure_directory_exists(fs::path const & dir) { if (!fs::exists(dir)) { std::error_code err; fs::create_directories(dir, err); } } Poniższy program pozwala użytkownikowi wybrać polecenie do wykonania (pakowanie lub wypakowywanie), a także ścieżkę źródłową i docelową. Używa on pokazanych powyżej funkcji compress() i decompress(), dostarczając im funkcję lambda, która będzie wywoływana w celu wyświetlenia postępu w konsoli: int main() { setlocale(LC_ALL, "polish"); char option = 0; std::cout << "Wybierz [p]akowanie/[w]ypakowywanie?"; std::cin >> option; if (option == 'p') { std::string archivepath; std::string inputpath; std::cout << "Podaj nazwę pliku lub katalogu do spakowania:"; std::cin >> inputpath; std::cout << "Podaj ścieżkę do archiwum:"; std::cin >> archivepath; compress(inputpath, archivepath, [](std::string_view message) {std::cout << message << std::endl; }); } else if (option == 'w') { std::string archivepath; std::string outputpath; std::cout << "Podaj katalog do wypakowania:"; std::cin >> outputpath; std::cout << "Podaj ścieżkę do archiwum:"; std::cin >> archivepath; decompress(outputpath, archivepath, [](std::string_view message) {std::cout << message << std::endl; }); } else { std::cout << "błędna opcja" << std::endl; } } 195 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 81. Pakowanie plików do archiwum ZIP i wypakowywanie ich z tego archiwum z zastosowaniem hasła To zadanie jest bardzo podobne do poprzedniego — jedyna różnica polega na tym, że pliki muszą zostać zaszyfrowane. Biblioteka ZipLib obsługuje tylko szyfrowanie PKWare. Jeśli musisz użyć innej metody szyfrowania, powinieneś wykorzystać inną bibliotekę. Przedstawione poniżej funkcje compress() i decompress() zostały zaimplementowane podobnie jak w rozwiązaniu poprzedniego zadania. Różnią się one jednak kilkoma elementami, między innymi dodatkowym parametrem reprezentującym hasło do szyfrowania i odszyfrowywania plików: Dodawanie zaszyfrowanych plików do archiwum odbywa się za pomocą metody ZipFile::AddEncryptedFile() zamiast ZipFile::AddFile(). Jeśli pozycja jest chroniona hasłem, podczas wypakowywania musi ono zostać przekazane za pomocą metody ZipArchiveEntry::SetPassword(). Funkcja compress() z wyżej wymienionymi zmianami została zaimplementowana w następujący sposób: namespace fs = std::experimental::filesystem; void compress(fs::path const & source, fs::path const & archive, std::string_view password, std::function<void(std::string_view)> reporter) { if (fs::is_regular_file(source)) { if (reporter) reporter("Pakowanie " + source.string()); ZipFile::AddEncryptedFile( archive.string(), source.string(), source.filename().string(), password.data(), LzmaMethod::Create()); } else { for (auto const & p : fs::recursive_directory_iterator(source)) { if (reporter) reporter("Pakowanie " + p.path().string()); if (fs::is_directory(p)) { auto zipArchive = ZipFile::Open(archive.string()); auto entry = zipArchive->CreateEntry(p.path().string()); entry->SetAttributes(ZipArchiveEntry::Attributes::Directory); ZipFile::SaveAndClose(zipArchive, archive.string()); } else if (fs::is_regular_file(p)) { ZipFile::AddEncryptedFile( 196 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych archive.string(), p.path().string(), p.path().filename().string(), password.data(), LzmaMethod::Create()); } } } } Funkcja decompress() musi określać hasło dla każdej pozycji archiwum przed użyciem strumienia wypakowującego, służącego do skopiowania zawartości pliku do miejsca docelowego. Przedstawiono ją na poniższym listingu: void decompress(fs::path const & destination, fs::path const & archive, std::string_view password, std::function<void(std::string_view)> reporter) { ensure_directory_exists(destination); auto zipArchive = ZipFile::Open(archive.string()); for (size_t i = 0; i < zipArchive->GetEntriesCount(); ++i) { auto entry = zipArchive->GetEntry(i); if (entry) { auto filepath = destination / fs::path{ entry->GetFullName() }.relative_path(); if (reporter) reporter("Tworzenie " + filepath.string()); if(entry->IsPasswordProtected()) entry->SetPassword(password.data()); if (entry->IsDirectory()) { ensure_directory_exists(filepath); } else { ensure_directory_exists(filepath.parent_path()); std::ofstream destFile; destFile.open(filepath.string().c_str(), std::ios::binary | std::ios::trunc); if (!destFile.is_open()) { if (reporter) reporter("Nie można utworzyć pliku docelowego!"); } auto dataStream = entry->GetDecompressionStream(); 197 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów if (dataStream) { utils::stream::copy(*dataStream, destFile); } } } } } Funkcja pomocnicza ensure_directory_exists() jest identyczna z funkcją o tej samej nazwie z poprzedniego zadania, więc nie będzie tu ponownie prezentowana. Możesz korzystać z przedstawionych funkcji w taki sam sposób jak w poprzednim zadaniu, a poza tym powinieneś również podać hasło. 82. Tworzenie pliku PNG z flagą narodową Najbardziej rozbudowaną pod względem funkcjonalności biblioteką przeznaczoną do pracy z plikami PNG jest libpng. To niezależna od platformy biblioteka o otwartych źródłach, która została napisana w języku C. Istnieją również biblioteki C++, przy czym niektóre z nich po prostu opakowują libpng — są to przykładowo png++, lodepng i PNGWriter. W celu rozwiązania zadań zaprezentowanych w tej książce użyjemy ostatniej z nich, czyli PNGWriter. Jest to biblioteka o otwartych źródłach, która działa w systemach Linux, Unix, macOS i Windows. Wśród jej funkcjonalności można wymienić otwieranie istniejących obrazów PNG, rysowanie i odczytywanie pikseli w przestrzeniach kolorów RGB, HSV i CMYK, tworzenie podstawowych kształtów, skalowanie, interpolację dwuliniową, pełną obsługę antyaliasingu i obracania tekstu TrueType, a także krzywe Béziera. Biblioteka ta jest opakowaniem biblioteki libpng, a oprócz tego wymaga również użycia biblioteki FreeType2 służącej do obsługi tekstu. Kod źródłowy tej biblioteki wraz z dokumentacją jest dostępny na stronie https://github.com/ pngwriter/pngwriter. Aby zainstalować bibliotekę PNGWriter, postępuj zgodnie z podaną instrukcją. Klasa pngwriter reprezentuje obraz PNG. Jej konstruktor pozwala zdefiniować szerokość i wysokość obrazu w pikselach, a także kolor tła i ścieżkę do pliku, w którym ma on zostać zapisany. Istnieje wiele funkcji składowych, które mogą zapisywać piksele, kształty lub tekst. W naszym przypadku powinniśmy wypełnić trzy prostokąty w różnych kolorach. W tym celu możemy użyć funkcji filledsquare(). Po zakończeniu tworzenia obrazu w pamięci musimy wywołać metodę close(), aby zapisać go w pliku na dysku. Poniższa funkcja tworzy dwukolorową flagę z wąską ramką wokół w kolorze szarym, przy czym rozmiar obrazu i ścieżka do pliku docelowego są dostarczane jako argumenty: void create_flag(int const width, int const height, std::string_view filepath) { pngwriter flag{ width, height, 0, filepath.data() }; 198 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych int const size = height / 2; // szara ramka flag.filledsquare(0, 0, width, height, 256, 256, 256); // czerwony prostokąt flag.filledsquare(2, 2, width - 1, size, 65535, 0, 0); // biały prostokąt flag.filledsquare(2, size + 1, width - 1, 2 * size - 1, 65535, 65535, 65535); flag.close(); } Poniżej przedstawiony program pozwala użytkownikowi na wprowadzenie szerokości i wysokości obrazu, a także ścieżki do pliku wyjściowego. Do wygenerowania obrazu PNG używa on funkcji create_flag(): int main() { setlocale(LC_ALL, "polish"); int width = 0, height = 0; std::string filepath; std::cout << "Szerokość: "; std::cin >> width; std::cout << "Wysokość: "; std::cin >> height; std::cout << "Nazwa pliku wyjściowego: "; std::cin >> filepath; create_flag(width, height, filepath); } 83. Tworzenie obrazu PNG zawierającego tekst weryfikacyjny To zadanie ma rozwiązanie podobne do poprzedniego. Jeśli więc jeszcze nie rozwiązałeś wcześniejszego zadania, zalecam, abyś to uczynił, zanim zaczniesz się zajmować bieżącym. Zasadniczo istnieją trzy elementy, które musi zawierać obraz: Tło w postaci gradientu. Można to osiągnąć, rysując linie (pionowo lub poziomo) o zmieniających się kolorach — od jednej krawędzi obrazu do przeciwnej. Rysowanie linii można wykonać za pomocą funkcji pngwriter::line(). Dostępnych jest kilka przeciążeń tej funkcji. Ta wersja, którą użyto w poniższym kodzie, przyjmuje jako argumenty pozycję początkową i końcową oraz trzy wartości dla kanałów koloru czerwonego, zielonego i niebieskiego w przestrzeni kolorów RGB. 199 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Losowy tekst z literami pochylonymi w lewo lub prawo pod różnymi przypadkowymi kątami. Wyświetlanie tekstu odbywa się przy użyciu funkcji pngwriter::plot_text(). Ta operacja jest zależna od biblioteki FreeType2. Przeciążona funkcja, którą tu wykorzystano, pozwala określić plik czcionki i jej rozmiar, miejsce, w którym tekst ma zostać zapisany, kąt w radianach, treść i kolor napisu. Losowe linie nałożone na tekst, znajdujące się na powierzchni całego obrazu. Są one generowane ponownie przy użyciu funkcji pngwritter::line(). Aby wyświetlić losowy tekst, kolory i pozycje linii, w poniższym kodzie użyto generatora liczb pseudolosowych std::mt19937 i kilku rozkładów jednostajnych dla liczb całkowitych: void create_image( int const width, std::string_view std::string_view { pngwriter image{ int const height, font, int const font_size, filepath) width, height, 0, filepath.data() }; std::random_device rd; std::mt19937 mt; auto seed_data = std::array<int, std::mt19937::state_size> {}; std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd)); std::seed_seq seq(std::begin(seed_data), std::end(seed_data)); mt.seed(seq); std::uniform_int_distribution<> udx(0, width); std::uniform_int_distribution<> udy(0, height); std::uniform_int_distribution<> udc(0, 65535); std::uniform_int_distribution<> udt(0, 25); // tło w postaci gradientu for (int iter = 0; iter <= width; iter++) { image.line( iter, 0, iter, height, 65535 - int(65535 * ((double)iter) / (width)), int(65535 * ((double)iter) / (width)), 65535); } // losowy tekst std::string font_family = font.data(); for (int i = 0; i < 6; ++i) { image.plot_text( // czcionka font_family.data(), font_size, // położenie i*width / 6 + 10, height / 2 - 10, // kąt (i % 2 == 0 ? -1 : 1)*(udt(mt) * 3.14) / 180, 200 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych // tekst std::string(1, char('A' + udt(mt))).data(), // kolor 0, 0, 0); } // losowe linie for (int i = 0; i < 4; ++i) { image.line(udx(mt), 0, udx(mt), height, udc(mt), udc(mt), udc(mt)); image.line(0, udy(mt), width, udy(mt), udc(mt), udc(mt), udc(mt)); } image.close(); } Przykład użycia funkcji przedstawiono poniżej. Zwróć uwagę na to, że ścieżka do pliku czcionki (w tym przypadku Arial) jest stała dla systemów Windows i Apple, jednakże dla innych platform musi zostać podana przez użytkownika: int main() { setlocale(LC_ALL, "polish"); std::string font_path; #ifdef _WIN32 font_path = R"(c:\windows\fonts\arial.ttf)"; #elif defined (__APPLE__) font_path = R"(/Library/Fonts/Arial.ttf)"; #else std::cout << "Ścieżka do pliku czcionki: "; std::cin >> font_path; #endif create_image(200, 50, font_path, 18, "validation.png"); } Schemat kolorów tła użyty w funkcji create_image() zawsze tworzy identyczny rodzaj gradientu dla obrazów o tej samej szerokości. W kolejnym ćwiczeniu mógłbyś tak zmodyfikować funkcję, aby losowo zmieniała kolory gradientu oraz tekstu. 201 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 84. Generator kodów kreskowych EAN-13 Międzynarodowy Kod Towarowy (znany również jako Europejski Kod Towarowy lub EAN), opisany w Wikipedii, jest standardem definiującym system symboliki kodu kreskowego oraz sposób tworzenia numerów. Wykorzystuje się go w globalnej wymianie towarów w celu identyfikacji określonego rodzaju produktu detalicznego znajdującego się w konkretnej konfiguracji opakowania i pochodzącego od określonego producenta. Najczęściej stosowanym standardem EAN jest 13-cyfrowy EAN-13. Jego opis, zawierający informacje na temat generowania kodu kreskowego, można znaleźć w Wikipedii na stronie https://pl.wikipedia.org/wiki/EAN, dlatego też nie będzie on szczegółowo analizowany w tej książce. Poniżej znajduje się kod kreskowy EAN-13 dla numeru 5901234123457 , podany jako przykład w opisie zadania (źródło: Wikipedia): Klasa ean13 przedstawiona w poniższym kodzie reprezentuje numer w standardzie EAN-13. Można go utworzyć z łańcucha lub liczby typu unsigned long long, a następnie z powrotem przekonwertować na ciąg znaków lub tablicę cyfr. Klasa pozwala na obliczenie 13. cyfry reprezentującej sumę kontrolną, jeśli argument przekazany w konstruktorze składa się z 12 cyfr. Można również sprawdzić, czy 13. cyfra jest poprawną sumą kontrolną numeru, jeśli podany argument ma 13 cyfr. Suma kontrolna to liczba, którą należy dodać do ważonej sumy pierwszych 12 cyfr, aby uczynić ją wielokrotnością liczby 10: struct ean13 { public: ean13(std::string_view code) { if (code.length() == 13) { if (code[12] != '0' + get_crc(code.substr(0,12))) throw std::runtime_error("Nie jest to format EAN-13."); number = code; } else if (code.length() == 12) { number = code.data() + std::string(1, '0' + get_crc(code)); } } 202 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych ean13(unsigned long long code) :ean13(std::to_string(code)) { } std::array<unsigned char, 13> to_array() const { std::array<unsigned char, 13> result; for (int i = 0; i < 13; ++i) result[i] = static_cast<unsigned char>(number[i] - '0'); return result; } std::string to_string() const noexcept { return number; } private: unsigned char get_crc(std::string_view code) { unsigned char weights[12] = { 1,3,1,3,1,3,1,3,1,3,1,3 }; size_t index = 0; auto sum = std::accumulate( std::begin(code), std::end(code), 0, [&weights, &index](int const total, char const c) { return total + weights[index++] * (c - '0'); }); return 10 - sum % 10; } std::string number; }; Jak podano w Wikipedii, kod kreskowy składa się z 95 modułów o stałej szerokości. Licząc od lewej do prawej, w kodzie zawarte są następujące moduły: 3 moduły znaku startu. 42 moduły dla lewej grupy 6 cyfr. Te moduły można podzielić na 6 grup po 7 modułów, kodujących cyfry od 2. do 7. Cyfry są kodowane z parzystością lub bez niej. Ze wzorca kodowania wynika też wybór pierwszej cyfry EAN-13. 5 modułów dla znaku rozdzielającego w środku. 42 moduły dla prawej grupy 6 cyfr. Te moduły można podzielić na 6 grup po 7 modułów, kodujących cyfry od 8. do 13. Kodowanie cyfr zawsze uwzględnia parzystość. Cyfra 13. jest znakiem kontrolnym. 3 moduły znaku stopu. Poniższe tabele, pochodzące z Wikipedii, przedstawiają kodowanie 2 grup składających się z 6 cyfr (pierwsza tabela), a także kodowanie samych cyfr na podstawie wartości pierwszej cyfry (druga tabela): 203 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Klasa ean13_barcode_generator pozwala na tworzenie kodu kreskowego EAN-13 na podstawie reprezentacji numeru ean13. Kod ten zostaje zapisany na dysku w postaci pliku graficznego PNG. Klasa składa się z kilku elementów: Metoda create() jest jedyną publiczną funkcją klasy. Jej argumenty to numer EAN-13, ścieżka do pliku wyjściowego, szerokość każdego bitu w pikselach, wysokość paska kodu kreskowego oraz marginesy dla obszaru kodu kreskowego. Funkcja rysuje w odpowiedniej kolejności znak startu, pierwsze 6 cyfr, środkowy znak rozdzielający, ostatnie 6 cyfr i znak końcowy, a następnie zapisuje obraz w pliku. 204 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych Metoda draw_digit() jest prywatną funkcją pomocniczą, która za pomocą metody pngwriter::filledsquare() tworzy siedmiobitowe cyfry oraz znaki startu, środkowy i stopu. Tabele kodowania oraz wartości znaków zostały zdefiniowane w prywatnych zmiennych klasy encodings, eandigits, marker_start, marker_end i marker_center. Klasa ean13_barcode_generator została przedstawiona na poniższym listingu: struct ean13_barcode_generator { void create(ean13 const & code, std::string_view filename, int const digit_width = 3, int const height = 50, int const margin = 10); private: int draw_digit(unsigned char code, unsigned int size, pngwriter& image, int const x, int const y, int const digit_width, int const height) { std::bitset<7> bits(code); int pos = x; for (int i = size - 1; i >= 0; --i) { if (bits[i]) { image.filledsquare(pos, y, pos + digit_width, y + height, 0, 0, 0); } pos += digit_width; } return pos; } unsigned char encodings[10][3] = { { 0b0001101, 0b0100111, 0b1110010 { 0b0011001, 0b0110011, 0b1100110 { 0b0010011, 0b0011011, 0b1101100 { 0b0111101, 0b0100001, 0b1000010 { 0b0100011, 0b0011101, 0b1011100 { 0b0110001, 0b0111001, 0b1001110 { 0b0101111, 0b0000101, 0b1010000 { 0b0111011, 0b0010001, 0b1000100 { 0b0110111, 0b0001001, 0b1001000 { 0b0001011, 0b0010111, 0b1110100 }; }, }, }, }, }, }, }, }, }, }, unsigned char eandigits[10][6] = { 205 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów }; }; { { { { { { { { { { 0,0,0,0,0,0 0,0,1,0,1,1 0,0,1,1,0,1 0,0,1,1,1,0 0,1,0,0,1,1 0,1,1,0,0,1 0,1,1,1,0,0 0,1,0,1,0,1 0,1,0,1,1,0 0,1,1,0,1,0 }, }, }, }, }, }, }, }, }, }, unsigned char marker_start = 0b101; unsigned char marker_end = 0b101; unsigned char marker_center = 0b01010; Metoda create() została zaimplementowana w następujący sposób: void ean13_barcode_generator::create(ean13 const & code, std::string_view filename, int const digit_width = 3, int const height = 50, int const margin = 10) { pngwriter image( margin * 2 + 95 * digit_width, height + margin * 2, 65535, filename.data()); std::array<unsigned char, 13> digits = code.to_array(); int x = margin; x = draw_digit(marker_start, 3, image, x, margin, digit_width, height); for (int i = 0; i < 6; ++i) { int code = encodings[digits[1 + i]][eandigits[digits[0]][i]]; x = draw_digit(code, 7, image, x, margin, digit_width, height); } x = draw_digit(marker_center, 5, image, x, margin, digit_width, height); for (int i = 0; i < 6; ++i) { int code = encodings[digits[7 + i]][2]; x = draw_digit(code, 7, image, x, margin, digit_width, height); } x = draw_digit(marker_end, 3, image, x, margin, digit_width, height); } image.close(); 206 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych Klasa może zostać użyta w przykładowy sposób: int main() { ean13_barcode_generator generator; generator.create(ean13("5901234123457"), "5901234123457.png", 5, 150, 30); } W kolejnym ćwiczeniu możesz umieścić numer EAN-13 pod wygenerowanym kodem kreskowym. 85. Odczytywanie informacji o filmach z bazy SQLite SQLite jest biblioteką zarządzającą relacyjną bazą danych, napisaną w języku C (chociaż duża liczba języków programowania udostępnia połączenia do niej) i niewymagającą uruchamiania osobnego procesu RDBMS. SQLite nie jest silnikiem bazy danych klient-serwer, lecz systemem wbudowanym w aplikację. Cała baza danych, w tym tabele, indeksy, wyzwalacze i widoki, jest zawarta w jednym pliku na dysku. Ponieważ dostęp do bazy danych oznacza dostęp do lokalnego pliku na dysku bez potrzeby komunikacji międzyprocesowej, SQLite ma lepszą wydajność w porównaniu z innymi silnikami relacyjnych baz danych. SQLite, jak sama nazwa wskazuje, używa języka SQL, chociaż nie implementuje wszystkich funkcji (takich jak RIGHT OUTER JOIN). Biblioteka SQLite jest używana nie tylko w przeglądarkach internetowych (kilka najważniejszych z nich umożliwia przechowywanie i pobieranie danych z bazy SQLite za pomocą technologii Web SQL Database), programistycznych środowiskach internetowych (takich jak Bugzilla, Django, Drupal lub Ruby on Rails) i systemach operacyjnych (domyślnie w systemach Android, Windows 10, FreeBSD, OpenBSD, Symbian OS i innych), ale także w aplikacjach i grach mobilnych. SQLite ma również ograniczenia, z których najbardziej znaczącym jest brak jakiegokolwiek zarządzania użytkownikami. Dodatkowe rozszerzenie o nazwie SQLCipher zapewnia możliwość uruchomienia niewidocznego dla odbiorców 256-bitowego szyfrowania AES. Omawiana biblioteka jest dostępna na stronie https://www.sqlite.org/. Biblioteka SQLite zawiera wiele plików źródłowych i skryptów, jednakże producent udostępnia także kompaktową wersję zwaną amalgamation, która jest zalecana do stosowania we wszystkich aplikacjach. Wersja ta zawiera tylko dwa pliki, sqlite3.h i sqlite3.c, które można skompilować z aplikacją. Pakiet amalgamation, a także inne pakiety bibliotek, w tym narzędzia, można pobrać ze strony https://www.sqlite.org/download.html. Jak wspomniano wcześniej, biblioteka SQLite została napisana w języku C. Istnieje jednak wiele bibliotek, które zapewniają opakowanie dla języka C++, w tym SQLiteCPP, CppSQLite, sqlite3cc i sqlite_modern_cpp. W tej książce użyjemy tej ostatniej, czyli sqlite_modern_cpp, ponieważ jest lekka i została utworzona przy użyciu nowoczesnego języka C++ z obsługą funkcji C++ 17, a także jest kompatybilna z SQLCipher. Biblioteka ta jest dostępna pod adresem https://github.com/SqliteModernCpp/sqlite_modern_cpp. Aby z niej skorzystać, należy do plików źródłowych dołączyć nagłówek sqlite_modern_cpp.h. 207 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Zanim zaczniemy pisać kod, by rozwiązać nasze zadanie, powinniśmy utworzyć bazę danych. Struktura bazy danych została podana w opisie zadania, dlatego by ją utworzyć, możemy użyć dostępnego narzędzia sqlite3 działającego w wierszu poleceń. W celu utworzenia nowej bazy danych lub otwarcia istniejącej należy wykonać następujące polecenie: sqlite3 <nazwa_pliku_bazy> W kodzie źródłowym dołączonym do tej książki można znaleźć utworzoną już bazę danych o nazwie cppchallenger85.db. Możesz jednak utworzyć tę bazę danych samodzielnie, otwierając nowy plik i wykonując następujące polecenia: create table movies(title text not null, year integer not null, length integer not null); create table persons(name text not null); create table directors(movieid integer not null, personid integer not null); create table writers(movieid integer not null, personid integer not null); create table casting(movieid integer not null, personid integer not null, role text not null); Zauważ, że oprócz kolumn zdefiniowanych powyżej SQLite dodaje niejawną kolumnę o nazwie rowid, która zawiera automatycznie zwiększaną 64-bitową liczbę całkowitą ze znakiem, jednoznacznie identyfikującą wiersz w tabeli. Baza danych cppchallenger85.db składa się z kilku opisów filmów, które zostały już dodane za pomocą następujących poleceń: insert into movies values ('Matrix', 1999, 196); insert into movies values ('Forrest Gump', 1994, 202); insert insert insert insert insert insert insert insert insert insert insert insert insert into into into into into into into into into into into into into persons persons persons persons persons persons persons persons persons persons persons persons persons values('Keanu Reeves'); values('Laurence Fishburne'); values('Carrie-Anne Moss'); values('Hugo Weaving'); values('Lana Wachowski'); values('Lilly Wachowski'); values('Tom Hanks'); values('Sally Field'); values('Robin Wright'); values('Mykelti Williamson'); values('Robert Zemeckis'); values('Winston Groom'); values('Eric Roth'); insert insert insert insert into into into into directors values(1, 5); directors values(1, 6); directors values(2, 11); writers values(1, 5); 208 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych insert into writers values(1, 6); insert into writers values(2, 12); insert into writers values(2, 13); insert insert insert insert insert insert insert insert into into into into into into into into casting casting casting casting casting casting casting casting values(1, values(1, values(1, values(1, values(2, values(2, values(2, values(2, 1, 'Neo'); 2, 'Morfeusz'); 3, 'Trinity'); 4, 'Agent Smith'); 7, 'Forrest Gump'); 8, 'Pani Gump'); 9, 'Jenny Curran'); 10, 'Bubba Blue'); Po utworzeniu bazy danych i wypełnieniu jej danymi możemy przejść do kolejnej fazy rozwiązywania zadania. Użyjemy następujących klas w celu reprezentowania filmów i rozwiązania problemu: struct casting_role { std::string actor; std::string role; }; struct movie { unsigned int std::string int unsigned int std::vector<casting_role> std::vector<std::string> std::vector<std::string> }; id; title; year; length; cast; directors; writers; using movie_list = std::vector<movie>; Główną klasą z biblioteki sqlite_modern_cpp, którą będziemy wykorzystywać, jest sqlite:: database. Pozwala ona na połączenie się z bazą danych, przygotowywanie i wykonywanie instrukcji, wiązanie parametrów, wykonywanie wywołań zwrotnych oraz obsługę transakcji. Możesz w prosty sposób otworzyć bazę danych, podając w konstruktorze sqlite::database ścieżkę do pliku. W przypadku wystąpienia wyjątków podczas operacji SQLite jest tworzony obiekt sqlite::sqlite_exception. Poniższy kod przedstawia funkcję main() programu, która łączy się z plikiem bazy danych o nazwie cppchallenger85.db (z bieżącego folderu). Jeśli połączenie się powiedzie, program zacznie pobierać wszystkie opisy filmów z bazy danych i je wyświetlać: int main() { try { sqlite::database db(R"(cppchallenger85.db)"); auto movies = get_movies(db); 209 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów for (auto const & m : movies) print_movie(m); } catch (sqlite::sqlite_exception const & e) { std::cerr << e.get_code() << ": " << e.what() << " podczas " << e.get_sql() << std::endl; } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } } Poniżej zaprezentowana funkcja print_movie() wyświetla opis filmu w konsoli: void print_movie(movie const & m) { std::cout << "[" << m.id << "] " << m.title << " (" << m.year << ") " << m.length << " min" << std::endl; std::cout << " reżyseria: "; for (auto const & d : m.directors) std::cout << d << ","; std::cout << std::endl; std::cout << " scenariusz: "; for (auto const & w : m.writers) std::cout << w << ","; std::cout << std::endl; std::cout << " obsada: "; for (auto const & r : m.cast) std::cout << r.actor << " (" << r.role << "),"; std::cout << std::endl << std::endl; } Klasa sqlite::database zawiera przeciążone operatory << i >>. Pierwszy z nich przygotowuje instrukcje i wiąże parametry oraz wykonuje inne operacje wejściowe w bazie danych, a drugi pobiera z niej dane. Aby powiązać parametr, użyj w instrukcji SQL symbolu ? dla nazwy parametru, a następnie wprowadź wartość za pomocą przeciążonego operatora <<. Parametry są wiązane w kolejności, w jakiej zostały zapisane do obiektu sqlite::database. Dla każdego wiersza, który otrzymujemy dzięki wykonaniu instrukcji SQL, uruchamiana jest funkcja wywołania zwrotnego. W bibliotece sqlite_modern_cpp możesz zdefiniować funkcję lambda, która zawiera parametr (o odpowiednim typie) dla każdej kolumny w wierszu. Dla kolumn, które mogą posiadać wartości null , możesz użyć konstrukcji std::unique_ptr<T> lub std::optional<T>, jeśli Twój kompilator obsługuje tę funkcję C++ 17. Poniższa funkcja o nazwie get_directors() wyszukuje wszystkich reżyserów filmu w tabelach directors i persons. Zwróć uwagę na to, że w instrukcjach SQL zaprezentowanych tutaj i w dalszej części będziemy używać niejawnie dodanej kolumny rowid: std::vector<std::string> get_directors(sqlite3_int64 const movie_id, sqlite::database & db) { std::vector<std::string> result; 210 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych db << R"(select p.name from directors as d join persons as p on d.personid = p.rowid where d.movieid = ?;)" << movie_id >> [&result](std::string const name) { result.emplace_back(name); }; return result; } W bardzo podobny sposób funkcja get_writers() odczytuje scenarzystów z tabeli writers, jak pokazano na poniższym listingu: std::vector<std::string> get_writers(sqlite3_int64 const movie_id, sqlite::database & db) { std::vector<std::string> result; db << R"(select p.name from writers as w join persons as p on w.personid = p.rowid where w.movieid = ?;)" << movie_id >> [&result](std::string const name) { result.emplace_back(name); }; return result; } Obsada filmu jest pobierana z tabeli casting za pomocą funkcji get_cast(): std::vector<casting_role> get_cast(sqlite3_int64 const movie_id, sqlite::database & db) { std::vector<casting_role> result; db << R"(select p.name, c.role from casting as c join persons as p on c.personid = p.rowid where c.movieid = ?;)" << movie_id >> [&result](std::string const name, std::string role) { result.emplace_back(casting_role{ name, role }); }; return result; } Wszystkie powyższe funkcje są używane w funkcji get_movies(), która zwraca listę wszystkich filmów zawartych w bazie danych. Może ona zostać zaimplementowana w następujący sposób: 211 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów movie_list get_movies(sqlite::database & db) { movie_list movies; db << R"(select rowid, * from movies;)" >> [&movies, &db](sqlite3_int64 const rowid, std::string const & title, int const year, int const length) { movies.emplace_back(movie{ static_cast<unsigned int>(rowid), title, year, static_cast<unsigned int>(length), get_cast(rowid, db), get_directors(rowid, db), get_directors(rowid, db) }); }; return movies; } Po zaimplementowaniu wszystkich funkcji rozwiązanie zadania jest już gotowe. Na poniższym zrzucie ekranu pokazano wyjście programu wyświetlającego dane wprowadzone we wcześniejszym kodzie: 86. Wstawianie w sposób transakcyjny informacji o filmach do bazy danych SQLite Musisz rozwiązać poprzednie zadanie, zanim zajmiesz się bieżącym, ponieważ jego rozwiązanie opiera się na wcześniejszym kodzie. Funkcja split() użyta w programie jest taka sama jak w przypadku zadania 27. „Dzielenie łańcucha na tokeny z listą możliwych separatorów”, zawartego w rozdziale 3. „Łańcuchy i wyrażenia regularne”. Z tego powodu nie będziemy 212 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych jej tutaj ponownie analizować. W kodzie źródłowym dołączonym do tej książki znajdziesz plik bazy danych o nazwie cppchallenger86.db, który został przygotowany dla zaprezentowania rozwiązania tego problemu. Poniższa funkcja read_movie() wczytuje informacje o filmie z konsoli (tytuł, rok wydania, czas trwania w minutach, reżyserzy, scenarzyści i obsada), tworzy obiekt movie, a następnie go zwraca. Obsada powinna być listą elementów oddzielonych przecinkami w formacie nazwa aktora=nazwa roli. Na przykład obsada filmu Matrix (znana już z poprzednich zadań) musi zostać podana jako pojedynczy wiersz w postaci Keanu Reeves=Neo,Laurence Fishburne=Morfeusz,Carrie-Anne Moss=Trinity,Hugo Weaving=Agent Smith. Aby odczytać wiersze tekstu zawierające białe znaki, musimy użyć funkcji std::getline(); wczytywanie przy użyciu obiektu std::cin zakończy się po pierwszym napotkanym białym znaku: movie read_movie() { movie m; std::cout << "Wprowadź film" << std::endl; std::cout << "Tytuł: "; std::getline(std::cin, m.title); std::cout << "Rok: "; std::cin >> m.year; std::cout << "Czas trwania: "; std::cin >> m.length; std::cin.ignore(); std::string directors; std::cout << "Reżyserzy: "; std::getline(std::cin, directors); m.directors = split(directors, ','); std::string writers; std::cout << "Scenarzyści: "; std::getline(std::cin, writers); m.writers = split(writers, ','); std::string cast; std::cout << "Obsada: "; std::getline(std::cin, cast); auto roles = split(cast, ','); for (auto const & r : roles) { auto pos = r.find_first_of('='); casting_role cr; cr.actor = r.substr(0, pos); cr.role = r.substr(pos + 1, r.size() - pos - 1); m.cast.push_back(cr); } return m; } Poniżej przedstawiona funkcja get_person_id() zwraca identyfikator numeryczny osoby, który jest automatycznie zwiększanym polem rowid, dodanym niejawnie przez SQLite podczas tworzenia tabeli (chyba że określono inaczej). Typ kolumny rowid to sqlite_int64, co oznacza 64-bitową liczbę całkowitą ze znakiem: 213 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów sqlite_int64 get_person_id(std::string const & name, sqlite::database & db) { sqlite_int64 id = 0; db << "select rowid from persons where name=?;" << name >> [&id](sqlite_int64 const rowid) {id = rowid; }; return id; } Funkcje insert_person(), insert_directors(), insert_writers() i insert_cast() pozwalają na wstawianie nowych pozycji do tabel osób, reżyserów, scenarzystów i obsady. W tym celu używamy obiektu sqlite::database przekazanego jako argument z funkcji main(), co będzie widoczne w dalszej części. Wstawiając reżysera, scenarzystę lub aktora, sprawdzamy najpierw, czy dana osoba już istnieje w bazie danych — jeśli nie, dodajemy ją: sqlite_int64 insert_person(std::string_view name, sqlite::database & db) { db << "insert into persons values(?);" << name.data(); return db.last_insert_rowid(); } void insert_directors(sqlite_int64 const movie_id, std::vector<std::string> const & directors, sqlite::database & db) { for (auto const & director : directors) { auto id = get_person_id(director, db); if (id == 0) id = insert_person(director, db); db << "insert into directors values(?, ?);" << movie_id << id; } } void insert_writers(sqlite_int64 const movie_id, std::vector<std::string> const & writers, sqlite::database & db) { for (auto const & writer : writers) { auto id = get_person_id(writer, db); if (id == 0) id = insert_person(writer, db); db << "insert into writers values(?, ?);" << movie_id << id; } 214 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych } void insert_cast(sqlite_int64 const movie_id, std::vector<casting_role> const & cast, sqlite::database & db) { for (auto const & cr : cast) { auto id = get_person_id(cr.actor, db); if (id == 0) id = insert_person(cr.actor, db); } } db << << << << "insert into casting values(?,?,?);" movie_id id cr.role; Funkcja insert_movie() wstawia nową pozycję do tabeli filmów, a następnie wywołuje zaprezentowane wcześniej funkcje, aby również dodać reżyserów, scenarzystów i obsadę. Wszystkie te operacje są wykonywane w pojedynczej transakcji. Transakcje są obsługiwane przez obiekt sqlite::database przy użyciu poleceń begin;, commit; i rollback; (zwróć uwagę na średnik kończący każde z poleceń). Polecenia są wykonywane przy użyciu przeciążonego operatora << w klasie sqlite::database. Transakcja jest uruchamiana na początku funkcji i zatwierdzana na jej końcu. Jeśli podczas wykonywania poleceń SQL wystąpi wyjątek, transakcja zostanie wycofana: void insert_movie(movie& m, sqlite::database & db) { try { db << "begin;"; db << << << << "insert into movies values(?,?,?);" m.title m.year m.length; auto movieid = db.last_insert_rowid(); insert_directors(movieid, m.directors, db); insert_writers(movieid, m.writers, db); insert_cast(movieid, m.cast, db); m.id = static_cast<unsigned int>(movieid); } db << "commit;"; } catch (std::exception const &) { db << "rollback;"; } 215 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Po zdefiniowaniu wszystkich funkcji możemy utworzyć poniżej przestawiony program, który otwiera bazę danych SQLite o nazwie cppchallenger86.db, wczytuje opis filmu z konsoli, wstawia go do bazy danych, a następnie wyświetla w konsoli całą listę filmów: int main() { try { sqlite::database db(R"(cppchallenger86.db)"); auto movie = read_movie(); insert_movie(movie, db); auto movies = get_movies(db); for (auto const & m : movies) print_movie(m); } } catch (sqlite::sqlite_exception const & e) { std::cerr << e.get_code() << ": " << e.what() << " podczas " << e.get_sql() << std::endl; } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } 87. Obsługa multimediów w bazie danych SQLite Jeśli jeszcze tego nie zrobiłeś, powinieneś rozwiązać dwa poprzednie zadania, zanim zajmiesz się obecnym. W tym przypadku musimy rozszerzyć model bazy danych o dodatkową tabelę służącą do przechowywania obrazów i ewentualnie innych plików multimedialnych, takich jak ujęcia wideo. Faktyczna zawartość plików multimedialnych musi być przechowywana w polu typu blob, jednakże należy zapisywać także inne atrybuty, takie jak opis i nazwa pliku. Podczas używania dużych obiektów możliwe są dwie opcje: przechowywanie ich bezpośrednio w bazie danych w postaci obiektów blob lub zapisywanie w osobnych plikach, a w samej bazie danych umieszczanie tylko ścieżek do nich. Zgodnie z testami wykonanymi przez twórców SQLite wczytywanie obiektów mniejszych niż 100 kB jest szybsze, gdy są one przechowywane bezpośrednio w bazie danych. W przypadku obiektów większych niż 100 kB odczyty są szybsze, gdy obiekty są zapisywane w oddzielnych plikach. Tę cechę powinieneś uwzględniać podczas projektowania modelu bazy danych. W tej książce jednak zignorujemy aspekty wydajnościowe i przechowamy pliki multimedialne w bazie danych. Aby utworzyć dodatkową tabelę dla plików multimedialnych (którą nazwiemy po prostu media), otwórz plik bazy danych w narzędziu wiersza polecenia sqlite3, którego już użyto podczas rozwiązywania zadania 85., a następnie wykonaj poniższe polecenie. Zwróć uwagę na to, że w kodzie dostarczonym z tą książką możesz znaleźć plik bazy danych o nazwie cppchallenger87.db, który już zawiera rozszerzony model bazy danych: 216 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych create table media(movieid integer not null, name text not null, description text, content blob not null); Pole description może zawierać wartości null. Podczas pracy z biblioteką sqlite_modern_cpp możesz użyć zapisu std::optional<T>, jeśli Twój kompilator obsługuje tę funkcjonalność języka C++ 17. Aby to zrobić, musisz zdefiniować makro MODERN_SQLITE_STD_OPTIONAL_SUPPORT. W przeciwnym razie możesz użyć konstrukcji std::unique_ptr<T>. Aby obsługiwać obiekty z tabeli media, użyjemy typów zaprezentowanych poniżej. Chociaż typem pola rowid jest sqlite3_int64, obecnie użyjemy unsigned int, a to wyłącznie w celu zachowania spójności z typem movie stosowanym w poprzednich dwóch rozwiązaniach i kilku innych zadaniach z tej książki: struct media { unsigned int unsigned int std::string std::optional<std::string> std::vector<char> }; id; movie_id; name; text; blob; using media_list = std::vector<media>; Funkcje add_media(), get_media() i delete_media() dodają, odczytują i usuwają pliki multimedialne związane z danym filmem. Powinny być one łatwe do zrozumienia, ponieważ możesz wykorzystać zdobyte już w poprzednich ćwiczeniach doświadczenie związane z interfejsem programistycznym biblioteki sqlite_modern_cpp. Należy zwrócić uwagę na to, że przy wyborze pól z tabeli — w tym przypadku tabeli media — pole rowid musi zostać jawnie określone, ponieważ nie obejmuje go wyrażenie * używane w celu wybierania wszystkich pól: bool add_media(sqlite_int64 const movieid, std::string_view name, std::string_view description, std::vector<char> content, sqlite::database & db) { try { db << "insert into media values(?,?,?,?)" << movieid << name.data() << description.data() << content; return true; } catch (...) { return false; } } media_list get_media(sqlite_int64 const movieid, 217 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów sqlite::database & db) { media_list list; db << "select rowid, * from media where movieid = ?;" << movieid >> [&list](sqlite_int64 const rowid, sqlite_int64 const movieid, std::string const & name, std::optional<std::string> const text, std::vector<char> const & blob ) { list.emplace_back(media{ static_cast<unsigned int>(rowid), static_cast<unsigned int>(movieid), name, text, blob}); }; return list; } bool delete_media(sqlite_int64 const mediaid, sqlite::database & db) { try { db << "delete from media where rowid = ?;" << mediaid; return true; } catch (...) { return false; } } Pliki multimedialne są powiązane z filmami dzięki wykorzystaniu ich identyfikatorów. Aby wyświetlić identyfikator filmu o określonym tytule, używamy poniższej funkcji get_movies(). Pobiera ona listę wszystkich filmów odpowiadających danemu tytułowi. Jeśli jest ich więcej, możemy wybrać, do których z nich chcemy dodać plik multimedialny: movie_list get_movies(std::string_view title, sqlite::database & db) { movie_list movies; db << R"(select rowid, * from movies where title=?;)" << title.data() >> [&movies, &db](sqlite3_int64 const rowid, std::string const & title, int const year, int const length) { movies.emplace_back(movie{ static_cast<unsigned int>(rowid), title, 218 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych year, static_cast<unsigned int>(length), {}, {}, {} }); }; return movies; } Główny program został zaimplementowany jako proste narzędzie, które akceptuje jednoliterowe polecenia i wyświetla wyniki w konsoli. Polecenia obejmują wyszukiwanie filmów, a także dodawanie, wyświetlanie oraz usuwanie plików multimedialnych. Poniżej przedstawiona funkcja print_commands() wyświetla dostępne polecenia: void print_commands() { std::cout << "[f]ilm <tytuł> << "[o]brazy <id filmu> << "[d]odaj <id filmu>,<ścieżka>,<opis> << "[u]suń <id obrazu> << "[p]omoc << "[w]yjście } wyświetla informacje o filmie\n" wyświetla obrazy związane z filmem\n" dodaje nowy obraz\n" usuwa obraz\n" wyświetla możliwe polecenia\n" kończy aplikację\n"; Na poniższym listingu pokazano implementację funkcji main(). Zaczynamy od otwarcia bazy danych SQLite o nazwie cppchallenger87.db. Następnie wchodzimy w nieskończoną pętlę odczytującą dane wprowadzane przez użytkownika do konsoli i wykonującą polecenia. Pętla i główny program kończą swoje działanie, gdy użytkownik wprowadzi polecenie w (wyjście): int main() { setlocale(LC_ALL, "polish"); try { sqlite::database db(R"(cppchallenger87.db)"); while (true) { std::string line; std::getline(std::cin, line); if (line == "p") print_commands(); else if (line == "w") break; else { if (starts_with(line, "f")) run_find(line, db); else if (starts_with(line, "o")) run_list(line, db); else if (starts_with(line, "d")) 219 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów run_add(line, db); else if (starts_with(line, "u")) run_del(line, db); else std::cout << "polecenie nieznane" << std::endl; } std::cout << std::endl; } } catch (sqlite::sqlite_exception const & e) { std::cerr << e.get_code() << ": " << e.what() << " podczas " << e.get_sql() << std::endl; } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } } Każde z obsługiwanych poleceń zostało zaimplementowane w oddzielnej funkcji. Funkcje run_find(), run_list(), run_add() i run_del() analizują dane wejściowe użytkownika, wywołują odpowiednie funkcje dostępu do bazy danych, które omawialiśmy wcześniej, a następnie wyświetlają wyniki w konsoli. Nie sprawdzają one dokładnie danych wprowadzanych przez użytkownika. W poleceniach rozróżniana jest wielkość znaków, należy więc używać małych liter. Funkcja run_find() wyodrębnia tytuł filmu na podstawie danych wejściowych podanych przez użytkownika, wywołuje funkcję get_movie() w celu pobrania listy wszystkich filmów o tym tytule i wyświetla wynik w konsoli: void run_find(std::string_view line, sqlite::database & db) { auto title = trim(line.substr(2)); auto movies = get_movies(title, db); if(movies.empty()) std::cout << "nic nie znaleziono" << std::endl; else { for (auto const m : movies) { std::cout << m.id << " | " << m.title << " | " << m.year << " | " << m.length << " min" << std::endl; } } } 220 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych Funkcja run_list() wyodrębnia numeryczny identyfikator filmu na podstawie danych wprowadzonych przez użytkownika, wywołuje funkcję get_media(), aby pobrać listę wszystkich plików multimedialnych związanych z danym filmem, a następnie wyświetla ją w konsoli. Ta funkcja prezentuje tylko długość pola blob, a nie obiekt, który się w nim znajduje: void run_list(std::string_view line, sqlite::database & db) { auto movieid = std::stoi(trim(line.substr(2))); if (movieid > 0) { auto list = get_media(movieid, db); if (list.empty()) { std::cout << "nic nie znaleziono" << std::endl; } else { for (auto const & m : list) { std::cout << m.id << " | " << m.movie_id << " | " << m.name << " | " << m.text.value_or("(null)") << " | " << m.blob.size() << " bajtów" << std::endl; } } } else std::cout << "błąd wejścia" << std::endl; } Dodawanie pliku do filmu odbywa się za pomocą funkcji run_add(). Wyodrębnia ona z ciągu znaków wprowadzonych przez użytkownika i rozdzielonych przecinkami (zgodnie z opisem polecenia [d]odaj <id filmu>,<ścieżka>,<opis>) identyfikator filmu, ścieżkę do pliku oraz jego opis. Następnie wczytuje zawartość pliku z dysku za pomocą funkcji pomocniczej load_image(), a wreszcie dodaje ją jako nowy rekord do tabeli media. Wdrożona tu implementacja nie sprawdza typów, co umożliwia dodanie do filmu dowolnego pliku, a nie tylko obrazów lub filmów. W kolejnym ćwiczeniu mógłbyś uzupełnić program o dodatkowe sprawdzanie poprawności: std::vector<char> load_image(std::string_view filepath) { std::vector<char> data; std::ifstream ifile(filepath.data(), std::ios::binary | std::ios::ate); if (ifile.is_open()) { auto size = ifile.tellg(); ifile.seekg(0, std::ios::beg); 221 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów } } data.resize(static_cast<size_t>(size)); ifile.read(reinterpret_cast<char*>(data.data()), size); return data; void run_add(std::string_view line, sqlite::database & db) { auto parts = split(trim(line.substr(2)), ','); if (parts.size() == 3) { auto movieid = std::stoi(parts[0]); auto path = fs::path{parts[1]}; auto desc = parts[2]; auto content = load_image(parts[1]); auto name = path.filename().string(); auto success = add_media(movieid, name, desc, content, db); if (success) std::cout << "dodano" << std::endl; else std::cout << "wystąpił błąd" << std::endl; } } else std::cout << "błąd wejścia" << std::endl; Ostatnim poleceniem do zaimplementowania jest usuwanie pliku multimedialnego. Funkcja run_del() pobiera z tabeli media identyfikator rekordu, który ma zostać usunięty, a następnie wywołuje funkcję delete_media(), by go usunąć: void run_del(std::string_view line, sqlite::database & db) { auto mediaid = std::stoi(trim(line.substr(2))); if (mediaid > 0) { auto success = delete_media(mediaid, db); if (success) std::cout << "usunięto" << std::endl; else std::cout << "wystąpił błąd" << std::endl; } else std::cout << "błąd wejścia" << std::endl; } We wcześniejszym kodzie użyto kilku funkcji pomocniczych: split(), która dzieli tekst na tokeny oddzielone określonym znakiem separatora; starts_with(), która sprawdza, czy dany ciąg znaków zaczyna się od określonego podłańcucha; trim(), która usuwa wszystkie spacje z początku i końca łańcucha. 222 ecb84badecb8c394873734f1e9bfb90f e Rozdział 10. • Archiwa, obrazy i bazy danych Oto listing zawierający powyżej wspomniane funkcje: std::vector<std::string> split(std::string text, char const delimiter) { auto sstr = std::stringstream{ text }; auto tokens = std::vector<std::string>{}; auto token = std::string{}; while (std::getline(sstr, token, delimiter)) { if (!token.empty()) tokens.push_back(token); } return tokens; } inline bool starts_with(std::string_view text, std::string_view part) { return text.find(part) == 0; } inline std::string trim(std::string_view text) { auto first{ text.find_first_not_of(' ') }; auto last{ text.find_last_not_of(' ') }; return text.substr(first, (last - first + 1)).data(); } Poniżej pokazano zrzut ekranu zawierający wyniki użycia opisanych wcześniej poleceń. Zaczynamy od wyświetlenia wszystkich filmów o nazwie Matrix, jednakże znajdujemy tylko jeden. Następnie wyświetlamy pliki multimedialne związane z tym filmem, lecz w tym momencie nie znajdujemy żadnego. Dodajemy plik o nazwie the_matrix.jpg z folderu res i ponownie wyświetlamy listę plików multimedialnych. Wreszcie usuwamy ostatnio dodany plik multimedialny i ponownie wyświetlamy pliki, aby upewnić się, że lista jest pusta: 223 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 224 ecb84badecb8c394873734f1e9bfb90f e 11 Kryptografia Zadania 88. Szyfr Cezara Napisz program, który może szyfrować i odszyfrowywać wiadomości za pomocą szyfru Cezara z rotacją w prawo i dowolną wartością przesunięcia. Dla uproszczenia program powinien uwzględniać i kodować jedynie wielkie litery, ignorując cyfry, symbole i inne typy znaków. 89. Szyfr Vigenère’a Napisz program, który może szyfrować i odszyfrowywać wiadomości za pomocą szyfru Vigenère’a. Dla uproszczenia przyjmijmy założenie, że wiadomości tekstowe przeznaczone do zaszyfrowania powinny się składać tylko z wielkich liter. 90. Kodowanie i dekodowanie base64 Utwórz program, który może kodować i dekodować dane binarne przy użyciu schematu kodowania base64. Powinieneś zaimplementować własne funkcje kodowania i dekodowania i nie korzystać z zewnętrznej biblioteki. Tabela używana do kodowania powinna być zgodna ze specyfikacją MIME. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 91. Sprawdzanie poprawności uwierzytelniania użytkowników Napisz program symulujący sposób uwierzytelniania użytkowników w zabezpieczonym systemie. Aby się zalogować, użytkownik musi być już zarejestrowany w systemie. Użytkownik wprowadza nazwę i hasło, a program sprawdza, czy wprowadzone dane pasują do którejkolwiek z zarejestrowanych osób; jeśli tak, użytkownik uzyskuje dostęp — w przeciwnym razie operacja się nie powiedzie. Ze względów bezpieczeństwa system nie powinien zapisywać haseł w postaci jawnej, lecz zamiast tego używać funkcji skrótu SHA. 92. Wyznaczanie skrótów dla plików Utwórz program, który mając podaną ścieżkę do pliku, oblicza i wyświetla w konsoli wartości skrótów SHA1, SHA256 i MD5 dla zawartości tego pliku. 93. Szyfrowanie i deszyfrowanie plików Utwórz program, który może szyfrować i deszyfrować pliki przy użyciu algorytmu Advanced Encryption Standard (AES; nazwa oryginalna: Rijndael). Powinno być możliwe podanie ścieżek do pliku źródłowego oraz docelowego, a także hasła. 94. Podpisywanie plików Napisz program, który przy użyciu kryptografii RSA jest w stanie podpisywać pliki i sprawdzać, czy podpisany w taki sposób plik nie został zmodyfikowany. Podczas procesu podpisywania pliku jego podpis powinien zostać zapisany w osobnym pliku, a następnie użyty w trakcie weryfikacji. Program powinien zawierać co najmniej dwie funkcje: jedną, która podpisuje plik (przyjmującą jako argumenty ścieżkę do pliku wejściowego, ścieżkę do klucza prywatnego RSA i ścieżkę do pliku, w którym zostanie zapisany podpis), i taką, która go weryfikuje (przyjmującą jako argumenty ścieżkę do pliku, ścieżkę do klucza publicznego RSA i ścieżkę do pliku podpisu). 226 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia Rozwiązania 88. Szyfr Cezara Szyfr Cezara, zwany też szyfrem przesuwającym, kodem Cezara lub przesunięciem cezariańskim, jest bardzo starą, prostą i powszechnie znaną techniką szyfrowania, w której każda litera tekstu jawnego jest zastępowana inną literą, oddaloną od niej o stałą liczbę pozycji w alfabecie. Metoda ta została wykorzystana przez Juliusza Cezara do ochrony wiadomości o znaczeniu wojskowym. Użył on przesunięcia o trzy litery, zastępując A przez D, B przez E itd. W tym kodowaniu tekst WYZWANIECPP zostaje zamieniony na ZBCZDQLHFSS. Szyfr jest szczegółowo opisany w Wikipedii pod adresem https://pl.wikipedia.org/wiki/Szyfr_Cezara. Chociaż szyfr Cezara nie jest stosowany w nowoczesnej kryptografii, ponieważ jego złamanie jest trywialne, wciąż używa się go na forach internetowych lub w grupach dyskusyjnych jako sposobu służącego do zaszyfrowania tekstu w celu ukrycia spoilerów, obraźliwych słów, rozwiązań łamigłówek itd. Obecne zadanie ma być jedynie prostym ćwiczeniem związanym z szyfrowaniem. Nie powinieneś używać tak prostego szyfru podstawieniowego do jakichkolwiek poważniejszych zastosowań kryptograficznych. Aby rozwiązać przedstawione zadanie, musimy zaimplementować dwie funkcje: jedną, która wykonuje szyfrowanie jawnego tekstu, oraz drugą, która go następnie odszyfrowuje. Oto charakterystyki tych funkcji, które znajdują się na listingu umieszczonym poniżej: Funkcja caesar_encrypt() stosuje łańcuch typu string_view reprezentujący jawny tekst i wartość przesunięcia, która wskazuje, o ile pozycji w alfabecie należy się przemieścić, aby użyć nowej litery. Ta funkcja uwzględnia i zastępuje tylko wielkie litery, natomiast pozostałe znaki wprowadzonego tekstu pozostają niezmienione. Alfabet został zdefiniowany jako sekwencja cykliczna, co oznacza, że w przypadku przesunięcia w prawo o wartości 3 litera X staje się A, Y staje się B, a Z staje się C. Funkcja caesar_decrypt() pobiera łańcuch typu string_view reprezentujący zaszyfrowany tekst, a także wartość przesunięcia, która wskazuje, o ile liter w dół alfabetu (rotacja w prawo) nastąpiło podstawienie znaków. Podobnie jak jego szyfrujący odpowiednik, funkcja ta przekształca tylko wielkie litery, a pozostałe znaki pozostawia nietknięte. std::string caesar_encrypt(std::string_view text, int const shift) { std::string str; str.reserve(text.length()); for (auto const c : text) { if (isalpha(c) && isupper(c)) str += 'A' + (c - 'A' + shift) % 26; else 227 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów str += c; } return str; } std::string caesar_decrypt(std::string_view text, int const shift) { std::string str; str.reserve(text.length()); for (auto const c : text) { if (isalpha(c) && isupper(c)) str += 'A' + (26 + c - 'A' - shift) % 26; else str += c; } return str; } Poniżej przedstawiono przykład wykorzystania tych funkcji. Szyfrowany tekst jest w rzeczywistości całym alfabetem angielskim, a proces jest wykonywany dla każdej możliwej wartości przesunięcia: int main() { auto text = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for (int i = 1; i <= 26; ++i) { auto enc = caesar_encrypt(text, i); auto dec = caesar_decrypt(enc, i); assert(text == dec); } } 89. Szyfr Vigenère’a Szyfr Vigenère’a jest techniką szyfrowania, która wykorzystuje szereg splecionych szyfrów Cezara. Został opisany już w 1553 roku przez Giovana Batistę Belaso, ale w XIX wieku został błędnie przypisany Blaise’owi de Vigenèr’owi i nazwany jego imieniem. Szyfr ten jest szczegółowo opisany w Wikipedii pod adresem https://pl.wikipedia.org/wiki/Szyfr_Vigen%C3%A8re%E2%80%99a. Tutaj przedstawiono tylko krótkie podsumowanie. Chociaż złamanie szyfru Vigenère’a trwało trzy stulecia, to w dzisiejszych czasach jest ono banalnie proste — podobnie jak przypadku szyfru Cezara, na którym się on opiera. To zadanie, tak jak poprzednie, zostało zaproponowane jedynie jako zabawne i proste ćwiczenie, a nie argument przemawiający za użyciem wykorzystanego w nim szyfru do poważniejszych celów kryptograficznych. 228 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia Szyfr wykorzystuje tabelę o nazwie tabula recta lub tabela Vigenère’a. W przypadku alfabetu angielskiego ta tabela ma 26 wierszy i 26 kolumn, a każdy wiersz jest całym alfabetem przesuniętym cyklicznie za pomocą szyfru Cezara. Poniższy rysunek pochodzi ze wspomnianego artykułu z Wikipedii i pokazuje zawartość tej tabeli: Do prawidłowego zaszyfrowania i odszyfrowania jest niezbędny klucz. Klucz zapisuje się, dopóki nie dopasuje się go do długości tekstu do zaszyfrowania i odszyfrowania (oba ciągi mają ten sam rozmiar). Szyfrowanie odbywa się poprzez pobranie każdej litery z tekstu wejściowego oraz odpowiadającej jej litery z klucza, a następnie wyszukanie znaku znajdującego się na przecięciu wiersza wyznaczanego przez literę tekstu jawnego i kolumny wyznaczanej przez literę słowa kluczowego. Odszyfrowanie odbywa się poprzez przejście do wiersza odpowiadającego literze klucza, wyszukanie zaszyfrowanej litery tekstu w wierszu i użycie nazwy kolumny jako litery tekstu jawnego. Funkcja realizująca szyfrowanie została nazwana vigenere_encrypt(). Pobiera ona tekst jawny i klucz, szyfruje zgodnie z metodą opisaną wcześniej, a następnie zwraca zaszyfrowany tekst: std::string vigenere_encrypt(std::string_view text, std::string_view key) { std::string result; result.reserve(text.length()); 229 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów static auto table = build_vigenere_table(); for (size_t i = 0; i < text.length(); ++i) { auto row = key[i%key.length()] - 'A'; auto col = text[i] - 'A'; result += table[row * 26 + col]; } return result; } Jej odpowiednikiem jest funkcja vigenere_decrypt(). Wykorzystuje ona tekst zaszyfrowany oraz klucz używany do jego zaszyfrowania, a następnie dokonuje deszyfracji za pomocą metody opisanej powyżej, zwracając w wyniku tekst jawny: std::string vigenere_decrypt(std::string_view text, std::string_view key) { std::string result; result.reserve(text.length()); static auto table = build_vigenere_table(); for (size_t i = 0; i < text.length(); ++i) { auto row = key[i%key.length()] - 'A'; for (size_t col = 0; col < 26; col++) { if (table[row * 26 + col] == text[i]) { result += 'A' + col; break; } } } return result; } Powyższe funkcje używają trzeciej funkcji, zwanej build_vigenere_table(), która tworzy tabelę Vigenère’a, wykonując 26 razy szyfrowanie Cezara dla całego alfabetu, za każdym razem z nową wartością przesunięcia. Tabela jest reprezentowana jako pojedynczy łańcuch: std::string build_vigenere_table() { std::string table; table.reserve(26*26); for (int i = 0; i < 26; ++i) table += caesar_encrypt("ABCDEFGHIJKLMNOPQRSTUVWXYZ", i); return table; } 230 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia Powyższe funkcje szyfrowania i odszyfrowywania można wykorzystać w następujący sposób: int main() { auto text = "WYZWANIECPP"; auto enc = vigenere_encrypt(text, "TESTUJEMY"); auto dec = vigenere_decrypt(enc, "TESTUJEMY"); assert(text == dec); } 90. Kodowanie i dekodowanie base64 Base64 to schemat kodowania stosowany do reprezentowania danych binarnych w formacie ASCII przy użyciu alfabetu składającego się z 64 znaków. Mimo że wszystkie implementacje używają tych samych początkowych 62 znaków (A – Z, a – z i 0 – 9), dwie ostatnie wartości mogą się różnić. W specyfikacji MIME stosowane są symbole + i /. Pojedyncza jednostka w formacie base64 reprezentuje 6 bitów danych, a cztery jednostki base64 kodują dokładnie 3 bajty danych binarnych. Gdy liczba jednostek nie jest podzielna przez 3, przed konwersją na base64 dodawane są dodatkowe bajty o wartości zero. Dopełnienie zakodowanego tekstu za pomocą znaków == lub = może zostać użyte do wskazania, że ostatnia grupa 3 bajtów pochodząca z tekstu jawnego faktycznie zawierała tylko 1 lub 2 bajty. Oto przykład kodowania tekstu cpp. W wyniku otrzymujemy ciąg Y3Bw: Źródłowy tekst ASCII cpp Źródłowe bajty 0x63 0x70 0x70 Źródłowe dane binarne 01100011 01110000 01110000 Indeks binarny base64 011000 110111 000001 110000 Indeks dziesiętny base64 24 55 1 48 Tekst zaszyfrowany w base64 Y3Bw Algorytm jest dokładniej opisany w Wikipedii pod adresem https://pl.wikipedia.org/wiki/Base64. Aby sprawdzić, czy otrzymane wyniki kodowania i dekodowania w schemacie base64 są poprawne, możesz użyć kodera dostępnego w sieci, na przykład https://www.base64encode.org/. Poniżej zaprezentowana klasa encoder ma dwie publiczne metody: to_base64() koduje wektor bajtów do formatu base64 i zwraca wynik jako łańcuch, a from_base64() dekoduje do wektora bajtów łańcuch zakodowany w base64, a następnie go zwraca. Do kodowania i dekodowania wykorzystywane są dwie odrębne tabele. Tabela kodowania jest w rzeczywistości jawnym łańcuchem o nazwie table_enc, który zawiera alfabet base64. Tabela używana do dekodowania nazywa się table_dec i jest tablicą składającą się z 256 liczb całkowitych, reprezentujących indeks w tabeli szyfrowania (table_enc) dla każdej 6-bitowej jednostki base64: 231 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów class encoder { std::string const table_enc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; char const padding_symbol = '='; char const table_dec[256] = { -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,64,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,65,-1,-1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 }; char const invalid_char = -1; char const padding_char = 65; public: std::string to_base64(std::vector<unsigned char> const & data); std::vector<unsigned char> from_base64(std::string data); }; Poniżej pokazano implementację metody to_base64(). Funkcja ta umieszcza znaki = lub == na końcu zakodowanego łańcucha, aby udostępnić rzeczywistą długość ciągu z danymi jawnymi: std::string encoder::to_base64(std::vector<unsigned char> const & data) { std::string result; result.resize((data.size() / 3 + ((data.size() % 3 > 0) ? 1 : 0)) * 4); auto result_ptr = &result[0]; size_t i = 0; size_t j = 0; while (j++ < data.size() / 3) { unsigned int value = (data[i] << 16) | (data[i+1] << 8) | data[i+2]; i += 3; *result_ptr++ *result_ptr++ *result_ptr++ *result_ptr++ = = = = table_enc[(value table_enc[(value table_enc[(value table_enc[(value & & & & }; auto rest = data.size() - i; 232 ecb84badecb8c394873734f1e9bfb90f 0x00fc0000) >> 18]; 0x0003f000) >> 12]; 0x00000fc0) >> 6]; 0x0000003f)]; e Rozdział 11. • Kryptografia if (rest == 1) { *result_ptr++ = table_enc[(data[i] & 0x000000fc) >> 2]; *result_ptr++ = table_enc[(data[i] & 0x00000003) << 4]; *result_ptr++ = padding_symbol; *result_ptr++ = padding_symbol; } else if (rest == 2) { unsigned int value = (data[i] << 8) | data[i + 1]; *result_ptr++ *result_ptr++ *result_ptr++ *result_ptr++ = = = = table_enc[(value & 0x0000fc00) >> 10]; table_enc[(value & 0x000003f0) >> 4]; table_enc[(value & 0x0000000f) << 2]; padding_symbol; } return result; } Poniżej przedstawiono również metodę from_base64(). Ta funkcja może dekodować zarówno łańcuchy zawierające uzupełnienie, jak i jego pozbawione: std::vector<unsigned char> encored::from_base64(std::string data) { size_t padding = data.size() % 4; if (padding == 0) { if (data[data.size() - 1] == padding_symbol) padding++; if (data[data.size() - 2] == padding_symbol) padding++; } else { data.append(2, padding_symbol); } std::vector<unsigned char> result; result.resize((data.length() / 4) * 3 - padding); auto result_ptr = &result[0]; size_t i = 0; size_t j = 0; while (j++ < data.size() / 4) { unsigned char c1 = table_dec[static_cast<int>(data[i++])]; unsigned char c2 = table_dec[static_cast<int>(data[i++])]; unsigned char c3 = table_dec[static_cast<int>(data[i++])]; unsigned char c4 = table_dec[static_cast<int>(data[i++])]; if (c1 == invalid_char || c2 == invalid_char || c3 == invalid_char || c4 == invalid_char) throw std::runtime_error("błędne kodowanie base64"); if (c4 == padding_char && c3 == padding_char) { 233 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów unsigned int value = (c1 << 6) | c2; *result_ptr++ = (value >> 4) & 0x000000ff; } else if (c4 == padding_char) { unsigned int value = (c1 << 12) | (c2 << 6) | c3; *result_ptr++ = (value >> 10) & 0x000000ff; *result_ptr++ = (value >> 2) & 0x000000ff; } else { unsigned int value = (c1 << 18) | (c2 << 12) | (c3 << 6) | c4; *result_ptr++ = (value >> 16) & 0x000000ff; *result_ptr++ = (value >> 8) & 0x000000ff; *result_ptr++ = value & 0x000000ff; } } return result; } Ponieważ klasa encoder koduje dane binarne do formatu base64 i także przeprowadza odpowiednie dekodowanie, została dostarczona klasa pomocnicza służąca do konwersji łańcucha na sekwencję bajtów i odwrotnie. Klasa ta, zwana converter, ma dwie statyczne metody: jedną o nazwie from_string(), która pobiera łańcuch string_view i zwraca wektor bajtów z zawartością łańcucha, a drugą o nazwie from_range(), która tworzy łańcuch na podstawie wektora bajtów: struct converter { static std::vector<unsigned char> from_string(std::string_view data) { std::vector<unsigned char> result; std::copy( std::begin(data), std::end(data), std::back_inserter(result)); return result; } static std::string from_range(std::vector<unsigned char> const & data) { std::string result; std::copy( std::begin(data), std::end(data), std::back_inserter(result)); return result; } }; 234 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia Klasy encoder i converter zostały użyte w poniższym przykładzie do zakodowania i zdekodowania ciągów znaków o różnych długościach w formacie base64. Program potwierdza, że wynik zdekodowania zakodowanego tekstu jest równy pierwotnemu tekstowi: int main() { setlocale(LC_ALL, "polish"); std::vector<std::vector<unsigned char>> data { { 't' }, { 't','e' }, { 't','e','s' }, { 't','e','s','t' }, { 't','e','s','t','u' }, { 't','e','s','t','u','j' }, { 't','e','s','t','u','j','e' }, { 't','e','s','t','u','j','e','m' }, { 't','e','s','t','u','j','e','m','y' }, }; encoder enc; for (auto const & v : data) { auto encv = enc.to_base64(v); auto decv = enc.from_base64(encv); assert(v == decv); } auto text = "wyzwaniecpp"; auto textenc = enc.to_base64(converter::from_string(text)); auto textdec = converter::from_range(enc.from_base64(textenc)); assert(text == textdec); } Mimo że przedstawiona tutaj implementacja kodowania i dekodowania base64 jest kompletna, nie ma najlepszej wydajności. Opierając się na testach, które przeprowadziłem, mogę stwierdzić, że działa ona podobnie jak implementacja dostępna w bibliotece Boost.Beast. Jednak niekoniecznie zalecam używanie jej w kodzie produkcyjnym. Zamiast tego powinieneś wykorzystać bardziej sprawdzone i szeroko stosowane implementacje zawarte w bibliotekach Boost.Beast, Crypto++ i innych. 235 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 91. Sprawdzanie poprawności uwierzytelniania użytkowników Dobrą darmową i wieloplatformową biblioteką C++ obsługującą kryptografię jest Crypto++. Ze względu na sprawdzoną w branży implementację funkcji kryptograficznych jest ona szeroko wykorzystywana zarówno w projektach niekomercyjnych, jak i komercyjnych, a także na uniwersytetach, w projektach studenckich i innych. Biblioteka ta zapewnia obsługę algorytmów AES i kandydatów do AES, a także innych szyfrów blokowych, kodów uwierzytelniania wiadomości, funkcji skrótu, kryptografii kluczy publicznych i wielu innych opcji, w tym funkcjonalności niekryptograficznych takich jak generatory liczb pseudolosowych, generowanie liczb pierwszych i ich weryfikacja, kompresja i dekompresja DEFLATE, schematy kodowania czy funkcje sum kontrolnych. Wspomniana biblioteka jest dostępna pod adresem https://www.cryptopp.com/ i będzie wykorzystywana w tym rozdziale do rozwiązywania zadań związanych z kryptografią. Po pobraniu biblioteki Crypto++ znajdziesz w niej kilka projektów odpowiadających różnym jej konfiguracjom. Należy użyć projektu o nazwie cryptolib, który tworzy bibliotekę statyczną. Dynamiczna wersja biblioteki (cryptodll) została sprawdzona przez organizacje NIST i CSE pod kątem zgodności z poziomem 1. standardu FIPS 140-2. FIPS 140-2 to zbiór amerykańskich rządowych standardów bezpieczeństwa komputerowego, które określają wymagania dla modułów kryptograficznych. Z powodu tej zgodności wersja cryptodll nie zawiera niczego, co nie spełnia wymagań standardów, w tym implementacji algorytmów DES i MD5. Aby rozwiązać zadanie, stworzymy model systemu, który zarządza bazą danych użytkowników. Użytkownik będzie mieć identyfikator numeryczny, nazwę, wartość skrótu do swojego hasła, a także opcjonalnie imię i nazwisko. W celu zaimplementowania powyższej funkcjonalności jest używana następująca klasa o nazwie user: struct user { int std::string std::string std::string std::string }; id; username; password; firstname; lastname; Obliczanie wartości skrótu dla hasła odbywa się w funkcji get_hash(). Pobiera ona wartość string_view, która reprezentuje hasło (lub w naszym przypadku dowolny tekst) i zwraca odpowiednią wartość skrótu SHA512. Biblioteka Crypto++ zawiera wiele funkcji skrótu, w tym SHA-1, SHA-2 (SHA-224, SHA-256, SHA-384 i SHA-512), SHA-3, Tiger, WHIRLPOOL, RIPEMD-128, RIPEMD-256, RIPEMD-160 i RIPEMD-320 — wszystkie zawarte w przestrzeni nazw CryptoPP — a także MD5 — w przestrzeni nazw CryptoPP::Weak, jeśli używasz wersji statycznej. Wszystkie te skróty pochodzą z klasy HashTransformation i są wzajemnie wymienne. Aby obliczyć wartość skrótu, musimy wykonać następujące działania: 236 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia Utworzyć obiekt dziedziczący z klasy HashTransformation, taki jak SHA512. Zdefiniować tablicę bajtów o wielkości wystarczającej do uzyskania wartości skrótu. Wywołać funkcję CalculateDigest(), przekazując bufor wyjściowy, tekst do przekształcenia i jego długość. Wartość skrótu wynikająca z przetworzenia oryginalnego tekstu ma postać binarną. Może ona zostać zakodowana w postaci bardziej czytelnej dla człowieka jako ciąg znaków zawierający cyfry szesnastkowe. Taką operację można wykonać za pomocą klasy HexEncoder. W celu zgromadzenia danych wyjściowych można wykorzystać ujście danych, takie jak StringSink lub FileSink. Biblioteka Crypto++ wykorzystuje koncepcję potoku do przesyłania danych ze źródła do ujścia. W ramach tego przepływu dane mogą napotkać filtry, które przekształcają je, zanim dotrą do ujścia. Obiekty w ramach potoku przejmują na własność inne obiekty przekazywane do nich za pomocą wskaźnika w konstruktorze i automatycznie niszczą je, gdy same zostaną zniszczone. Poniższy cytat pochodzi z dokumentacji biblioteki: „Jeśli konstruktor dla A przyjmuje wskaźnik do obiektu B (z wyjątkiem typów pierwotnych takich jak int i char), to A staje się właścicielem B i usunie B w destruktorze A. Jeśli konstruktor dla A odwołuje się do obiektu B, to obiekt wywołujący zachowuje prawo własności do obiektu B i nie powinien go niszczyć do momentu, gdy A nie będzie go już potrzebować”. Poniżej znajduje się implementacja funkcji get_hash(): std::string get_hash(std::string_view password) { CryptoPP::SHA512 sha; CryptoPP::byte digest[CryptoPP::SHA512::DIGESTSIZE]; sha.CalculateDigest( digest, reinterpret_cast<CryptoPP::byte const*>(password.data()), password.length()); CryptoPP::HexEncoder encoder; std::string result; encoder.Attach(new CryptoPP::StringSink(result)); encoder.Put(digest, sizeof(digest)); encoder.MessageEnd(); } return result; Poniższy program używa klasy user i funkcji get_hash() w celu stworzenia modelu systemu logowania. Zmienna users, jak sama nazwa wskazuje, to lista użytkowników. Chociaż jest ona zakodowana na stałe, może zostać również odczytana z bazy danych. Kolejnym ćwiczeniem mogłoby więc być zaimplementowanie przechowywania użytkowników w bazie SQLite i pobierania ich z bazy. Po wprowadzeniu przez użytkownika jego nazwy i hasła program obliczy skrót SHA512 dla hasła, sprawdzi listę użytkowników pod kątem dokładnego dopasowania nazwy użytkownika oraz skrótu hasła, a następnie wyświetli odpowiedni komunikat1: 1 Aby się poprawnie zalogować, należy podać hasło równe nazwie użytkownika — przyp. tłum. 237 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów int main() { setlocale(LC_ALL, "polish"); std::vector<user> users { { 101, "scarface", "07A8D53ADAB635ADDF39BAEACFB799FD7C5BFDEE365F3AA721B7E25B54A4E87D419ADDE A34BC3073BAC472DCF4657E50C0F6781DDD8FE883653D10F7930E78FF", "Tony", "Montana" }, { 202, "neo", "C2CC277BCC10888ECEE90F0F09EE9666199C2699922EFB41EA7E88067B2C075F3DD3FBF 3CFE9D0EC6173668DD83C111342F91E941A2CADC46A3A814848AA9B05", "Thomas", "Anderson" }, { 303, "godfather", "0EA7A0306FE00CD22DF1B835796EC32ACC702208E0B052B15F9393BCCF5EE9ECD8BAAF2 7840D4D3E6BCC3BB3B009259F6F73CC77480C065DDE67CD9BEA14AA4D", "Vito", "Corleone" } }; std::string username, password; std::cout << "Nazwa użytkownika: "; std::cin >> username; std::cout << "Hasło: "; std::cin >> password; auto hash = get_hash(password); auto pos = std::find_if( std::begin(users), std::end(users), [username, hash](user const & u) { return u.username == username && u.password == hash; }); if (pos != std::end(users)) std::cout << "Logowanie poprawne!" << std::endl; else std::cout << "Błędna nazwa użytkownika lub hasło" << std::endl; } 238 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia 92. Wyznaczanie skrótów dla plików Wartości skrótów do plików są często używane w celu zapewnienia integralności treści, na przykład w przypadku pobierania danych z internetu. Chociaż implementacje funkcji skrótów SHA1 i MD5 można znaleźć w różnych bibliotekach, ponownie wykorzystamy bibliotekę Crypto++. Jeśli nie przestudiowałeś jeszcze poprzedniego zadania „Sprawdzanie poprawności uwierzytelniania użytkowników” i jego rozwiązania, powinieneś to zrobić przed rozpoczęciem lektury tego punktu, ponieważ ogólne informacje o bibliotece Crypto++ nie będą powtarzane. Przy użyciu biblioteki Crypto++ obliczanie wartości skrótu dla pliku jest stosunkowo proste. W poniższym kodzie wykorzystano kilka komponentów, którymi są: Klasa FileSource, która umożliwia odczyt danych z pliku przy użyciu obiektu BufferedTransformation. Domyślnie przesyła on dane w blokach lub fragmentach o rozmiarze 4096 bajtów, chociaż możliwe jest również definiowanie własnych rozmiarów. Użyty tutaj konstruktor wykorzystuje ścieżkę do pliku wejściowego, wartość logiczną, która wskazuje, czy wszystkie dane powinny zostać przesłane, a także obiekt BufferedTransformation. Klasa HashFilter, która używa określonego algorytmu tworzenia skrótu do obliczenia wartości skrótu dla wszystkich danych wejściowych przesłanych do pierwszego pojawienia się sygnału MessageEnd, co spowoduje wyprowadzenie wynikowej wartości skrótu do dołączonej do niej transformacji. Klasa HexEncoder, która koduje bajty w systemie szesnastkowym przy użyciu znaków 0123456789ABCDEF. Klasa StringSink, która reprezentuje miejsce docelowe łańcucha danych w potoku. Odwołuje się ona do obiektu będącego łańcuchem, w którym mają zostać przechowane dane. Obiekt BufferedTransformation to podstawowa jednostka przepływu danych w bibliotece Crypto++. Jest on uogólnieniem klas BlockTransformation, StreamTransformation i HashTransformation. Taki obiekt pobiera strumień bajtów jako dane wejściowe (można to zrobić etapami), wykonuje na nich pewne obliczenia, a następnie umieszcza wynik w buforze wewnętrznym w celu późniejszego pobrania. Wszelkie częściowe wyniki znajdujące się już w buforze wyjściowym nie są modyfikowane przez kolejne dane wejściowe. Obiekty dziedziczące z klasy BufferedTransformation mogą uczestniczyć w tworzeniu potoków, co pozwala na przepływ danych ze źródła do ujścia. template <class Hash> std::string compute_hash(fs::path const & filepath) { std::string digest; Hash hash; CryptoPP::FileSource source( filepath.c_str(), true, 239 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów new CryptoPP::HashFilter(hash, new CryptoPP::HexEncoder( new CryptoPP::StringSink(digest)))); return digest; } Powyższy szablon funkcji compute_hash() może zostać użyty w następujący sposób do ustalania różnych wartości skrótów: int main() { setlocale(LC_ALL, "polish"); std::string path; std::cout << "Ścieżka: "; std::cin >> path; try { std::cout << "SHA1: " << compute_hash<CryptoPP::SHA1>(path) << std::endl; std::cout << "SHA256: " << compute_hash<CryptoPP::SHA256>(path) << std::endl; std::cout << "MD5: " << compute_hash<CryptoPP::Weak::MD5>(path) << std::endl; } catch (std::exception const & ex) { std::cerr << ex.what() << std::endl; } } Należy zauważyć, że funkcja skrótu MD5 jest przestarzała i niezabezpieczona, dlatego też została uwzględniona jedynie w celu zapewnienia zgodności wstecznej. Aby jej użyć, musisz w następujący sposób zdefiniować makro CRYPTOPP_ENABLE_NAMESPACE_WEAK przed dodaniem nagłówka md5.h: #define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1 #include "md5.h" 93. Szyfrowanie i deszyfrowanie plików Aby rozwiązać to zadanie przy użyciu biblioteki Crypto++, musimy wykorzystać poniższe komponenty. Są to: Klasa FileSource, która umożliwia odczyt danych z pliku przy użyciu obiektu BufferedTransformation. Domyślnie przesyła on dane w blokach lub fragmentach o rozmiarze 4096 bajtów, chociaż możliwe jest również definiowanie własnych rozmiarów. 240 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia Klasa FileSink, która umożliwia zapisywanie danych w pliku przy użyciu obiektu BufferedTransformation. Jest to klasa ujścia towarzysząca obiektowi źródłowemu FileSource. Klasy DefaultEncryptorWithMAC i DefaultDecryptorWithMAC, które szyfrują i odszyfrowują łańcuchy oraz pliki za pomocą etykiety uwierzytelniającej, aby wykryć manipulację. Używają one AES jako domyślnego szyfru blokowego, a SHA256 jako domyślnej funkcji skrótu dla uwierzytelnienia. Każde uruchomienie tych dwóch klas daje inny wynik ze względu na użycie parametru soli opartego na czasie. Zarówno do szyfrowania, jak i odszyfrowywania istnieją dwie funkcje przeciążone: Pierwsza funkcja przeciążona pobiera ścieżkę do pliku źródłowego, ścieżkę do pliku docelowego i hasło. Szyfruje lub odszyfrowuje plik źródłowy, a wynik jest zapisywany w pliku docelowym. Druga funkcja przeciążona wykorzystuje ścieżkę do pliku i hasło. Szyfruje lub odszyfrowuje dane, zapisując wynik w pliku tymczasowym, usuwa oryginalny, a następnie przenosi plik tymczasowy w położenie pliku źródłowego. Jej implementacja opiera się na pierwszej funkcji. Poniżej zaprezentowano funkcje realizujące szyfrowanie plików: void encrypt_file(fs::path const & sourcefile, fs::path const & destfile, std::string_view password) { CryptoPP::FileSource source( sourcefile.c_str(), true, new CryptoPP::DefaultEncryptorWithMAC( (CryptoPP::byte*)password.data(), password.size(), new CryptoPP::FileSink( destfile.c_str()) ) ); } void encrypt_file(fs::path const & filepath, std::string_view password) { auto temppath = fs::temp_directory_path() / filepath.filename(); encrypt_file(filepath, temppath, password); fs::remove(filepath); fs::rename(temppath, filepath); } 241 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Równoważne funkcje deszyfrujące są zasadniczo identyczne z poprzednimi, ale zamiast używać metody DefaultEncryptorWithMAC dla transformacji buforowanej, wykorzystują Default DecryptorWithMAC. Poniżej przedstawiono dwa wcześniej wspomniane przeciążenia funkcji: void decrypt_file(fs::path const & sourcefile, fs::path const & destfile, std::string_view password) { CryptoPP::FileSource source( sourcefile.c_str(), true, new CryptoPP::DefaultDecryptorWithMAC( (CryptoPP::byte*)password.data(), password.size(), new CryptoPP::FileSink( destfile.c_str()) ) ); } void decrypt_file(fs::path const & filepath, std::string_view password) { auto temppath = fs::temp_directory_path() / filepath.filename(); decrypt_file(filepath, temppath, password); fs::remove(filepath); fs::rename(temppath, filepath); } Wymienione funkcje mogą zostać użyte w następujący sposób: int main() { encrypt_file("sample.txt", "sample.txt.enc", "wyzwaniecpp"); decrypt_file("sample.txt.enc", "sample.txt.dec", "wyzwaniecpp"); encrypt_file("sample.txt", "wyzwaniecpp"); decrypt_file("sample.txt", "wyzwaniecpp"); } 94. Podpisywanie plików Procesy podpisywania i weryfikacji są podobne do szyfrowania i deszyfrowania, jednakże różnią się od siebie w sposób zasadniczy: szyfrowanie odbywa się za pomocą klucza publicznego, a odszyfrowywanie za pomocą klucza prywatnego, podczas gdy podpisywanie odbywa się za pomocą klucza prywatnego, a weryfikacja za pomocą klucza publicznego. Podpisywanie pozwala odbiorcy będącemu właścicielem klucza publicznego na sprawdzenie, czy oryginalny podpisany plik nie został zmodyfikowany. Posiadanie klucza publicznego nie jest jednak wystarczające, aby zmienić plik i podpisać go ponownie. Do rozwiązania niniejszego zadania również zastosujemy bibliotekę Crypto++. 242 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia Chociaż do wykonania procesu podpisywania i weryfikacji można użyć dowolnej pary publiczno-prywatnych kluczy RSA, w przedstawionej tutaj implementacji klucze są losowo generowane podczas uruchamiania programu. Oczywiście w praktyce generowałbyś klucze niezależnie od realizacji procesu podpisywania i weryfikacji, a nie za każdym razem, gdy go wykonujesz. Funkcja generate_keys(), znajdująca się na końcu poniższego listingu, tworzy parę 3072-bitowych, publiczno-prywatnych kluczy RSA. Do tego celu służy kilka zaprezentowanych funkcji pomocniczych: void encode(fs::path const & filepath, CryptoPP::BufferedTransformation const & bt) { CryptoPP::FileSink file(filepath.c_str()); bt.CopyTo(file); file.MessageEnd(); } void encode_private_key(fs::path const & filepath, CryptoPP::RSA::PrivateKey const & key) { CryptoPP::ByteQueue queue; key.DEREncodePrivateKey(queue); encode(filepath, queue); } void encode_public_key(fs::path const & filepath, CryptoPP::RSA::PublicKey const & key) { CryptoPP::ByteQueue queue; key.DEREncodePublicKey(queue); encode(filepath, queue); } void decode(fs::path const & filepath, CryptoPP::BufferedTransformation& bt) { CryptoPP::FileSource file(filepath.c_str(), true); file.TransferTo(bt); bt.MessageEnd(); } void decode_private_key(fs::path const & filepath, CryptoPP::RSA::PrivateKey& key) { CryptoPP::ByteQueue queue; decode(filepath, queue); key.BERDecodePrivateKey(queue, false, queue.MaxRetrievable()); } void decode_public_key(fs::path const & filepath, CryptoPP::RSA::PublicKey& key) { CryptoPP::ByteQueue queue; decode(filepath, queue); 243 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów key.BERDecodePublicKey(queue, false, queue.MaxRetrievable()); } void generate_keys(fs::path const & privateKeyPath, fs::path const & publicKeyPath, CryptoPP::RandomNumberGenerator& rng) { try { CryptoPP::RSA::PrivateKey rsaPrivate; rsaPrivate.GenerateRandomWithKeySize(rng, 3072); CryptoPP::RSA::PublicKey rsaPublic(rsaPrivate); encode_private_key(privateKeyPath, rsaPrivate); encode_public_key(publicKeyPath, rsaPublic); } catch (CryptoPP::Exception const & e) { std::cerr << e.what() << std::endl; } } W celu przeprowadzenia procesu podpisywania używamy potoku, który zaczyna się od obiektu FileSource, a kończy się na obiekcie FileSink i zawiera filtr o nazwie SignerFilter tworzący podpis dla wiadomości. Do transformacji danych źródłowych wykorzystujemy metodę RSASSA_PKCS1v15_SHA_Signer: void rsa_sign_file(fs::path const & filepath, fs::path const & privateKeyPath, fs::path const & signaturePath, CryptoPP::RandomNumberGenerator& rng) { CryptoPP::RSA::PrivateKey privateKey; decode_private_key(privateKeyPath, privateKey); CryptoPP::RSASSA_PKCS1v15_SHA_Signer signer(privateKey); CryptoPP::FileSource fileSource( filepath.c_str(), true, new CryptoPP::SignerFilter( rng, signer, new CryptoPP::FileSink( signaturePath.c_str()))); } Odwrotny proces weryfikacji jest realizowany w podobny sposób. W tym przypadku używamy filtru SignatureVerificationFilter, który jest odpowiednikiem SignerFilter, natomiast weryfikatorem staje się RSASSA_PKCS1v15_SHA_Verifier, który jest odpowiednikiem RSASSA_PKCS1v15_SHA_Signer: 244 ecb84badecb8c394873734f1e9bfb90f e Rozdział 11. • Kryptografia bool rsa_verify_file(fs::path const & filepath, fs::path const & publicKeyPath, fs::path const & signaturePath) { CryptoPP::RSA::PublicKey publicKey; decode_public_key(publicKeyPath.c_str(), publicKey); CryptoPP::RSASSA_PKCS1v15_SHA_Verifier verifier(publicKey); CryptoPP::FileSource signatureFile(signaturePath.c_str(), true); if (signatureFile.MaxRetrievable() != verifier.SignatureLength()) return false; CryptoPP::SecByteBlock signature(verifier.SignatureLength()); signatureFile.Get(signature, signature.size()); auto* verifierFilter = new CryptoPP::SignatureVerificationFilter(verifier); verifierFilter->Put(signature, verifier.SignatureLength()); CryptoPP::FileSource fileSource( filepath.c_str(), true, verifierFilter); return verifierFilter->GetLastResult(); } Poniższy program generuje parę publiczno-prywatnych kluczy RSA oraz wykorzystuje klucz prywatny do podpisania pliku przy użyciu funkcji rsa_sign_file(), a następnie używa klucza publicznego i pliku sygnatury, aby zweryfikować plik za pomocą odpowiedniej funkcji rsa_verify_file(): int main() { CryptoPP::AutoSeededRandomPool rng; generate_keys("rsa-private.key", "rsa-public.key", rng); rsa_sign_file("sample.txt", "rsa-private.key", "sample.sign", rng); auto success = rsa_verify_file("sample.txt", "rsa-public.key", "sample.sign"); assert(success); } 245 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 246 ecb84badecb8c394873734f1e9bfb90f e 12 Praca w sieci i usługi Zadania 95. Znajdowanie adresu IP dla hosta Napisz program, który potrafi pobrać i wyświetlić adres IPv4 dla hosta. Jeśli znalezionych zostanie więcej adresów, każdy z nich powinien zostać wyświetlony. Program powinien działać na wszystkich platformach. 96. Gra Fizz-Buzz klient-serwer Utwórz aplikację klient-serwer, która może zostać wykorzystana do grania w grę Fizz-Buzz. Klient przekazuje liczby do serwera, który odpowiada słowami „fizz”, „buzz”, „fizz-buzz” lub odsyła te liczby zgodnie z poniższymi zasadami gry. Komunikacja między klientem a serwerem musi się odbywać za pośrednictwem protokołu TCP. Serwer powinien działać w pętli nieskończonej. Klient powinien działać dopóty, dopóki użytkownik wprowadza liczby od 1 do 99. Fizz-Buzz to gra dla dzieci, przeznaczona do nauki dzielenia arytmetycznego. Pierwszy gracz wypowiada liczbę, a drugi odpowiada w następujący sposób: „fizz”, jeśli liczba jest podzielna przez 3; „buzz”, jeśli liczba jest podzielna przez 5; „fizz-buzz”, jeśli liczba jest podzielna przez 3 i 5; wypowiada tę samą liczbę w każdym innym przypadku. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 97. Kursy wymiany bitcoinów Napisz program wyświetlający kursy wymiany bitcoinów dla najważniejszych walut (takich jak USD, EUR lub GBP). Kursy walut muszą być pobierane z usługi online takiej jak https://blockchain.info/. 98. Pobieranie wiadomości e-mailowych przy użyciu protokołu IMAP Napisz program, który może pobierać informacje z serwera pocztowego przy użyciu protokołu IMAP. Program powinien być w stanie wykonać następujące działania: uzyskać listę folderów ze skrzynki pocztowej; odebrać nieprzeczytane wiadomości e-mailowe z określonego folderu. 99. Tłumaczenie tekstu na dowolny język Utwórz program, który może tłumaczyć tekst z jednego języka na inny za pomocą usługi online. Powinno być możliwe określenie tekstu, który ma być tłumaczony, a także języka źródłowego i docelowego. 100. Wykrywanie twarzy na obrazie Utwórz program, który rozpoznaje twarze osób na obrazach. Program musi przynajmniej wykryć obszar twarzy i płeć danej osoby. Uzyskane informacje powinny zostać wyświetlone w konsoli. Obrazy muszą być wczytywane z dysku. 248 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi Rozwiązania 95. Znajdowanie adresu IP dla hosta Informacje o hoście, w tym adresy IP, można pobierać za pomocą specyficznych dla systemu narzędzi sieciowych takich jak gethostbyname(). Chociaż taka funkcja jest dostępna na wszystkich platformach, jej sposób użycia dla każdej z nich jest inny, dlatego należy utworzyć program uniwersalny. Istnieją różne biblioteki wieloplatformowe o otwartym kodzie służące do zarządzania siecią, takie jak POCO i Asio/Boost.Asio. POCO jest bardziej złożoną biblioteką, obsługującą nie tylko sieć, ale także dostęp do danych, kryptografię, XML, JSON, Zip i inne zagadnienia. Asio jest samodzielną, nagłówkową biblioteką ze spójnym modelem asynchronicznym wejścia i wyjścia służącym do programowania sieciowego. Jest ona również dostępna jako część biblioteki Boost. Rozważana jest propozycja utworzenia standardu opartego na niej. W tej książce będę używać samodzielnej wersji biblioteki Asio, ponieważ jest ona wyłącznie nagłówkowa i nie zawiera dodatkowych zależności, a zatem jest łatwiejsza w użyciu. Ta biblioteka może zostać zastosowana do rozwiązania niniejszego zadania. Samodzielną bibliotekę Asio można znaleźć na stronie https://think-async.com/, chociaż jej najnowsza wersja wydaje się być dostępna tylko na GitHubie pod adresem https://github.com/ chriskohlhoff/asio/. Aby jej użyć, należy najpierw ją sklonować lub pobrać, a następnie rozpakować repozytorium i dołączyć nagłówek asio.hpp do źródeł programu. Jeśli nie chcesz żadnych zależności z biblioteką Boost, upewnij się, że przed dołączeniem nagłówka zdefiniowałeś makro ASIO_STANDALONE. Funkcja get_ip_address(), przedstawiona w poniższym fragmencie kodu, pobiera nazwę hosta i zwraca odpowiednią listę łańcuchów reprezentujących jego adresy IPv4. Działanie tej funkcji opiera się na następujących komponentach biblioteki Asio: Klasa asio::io_context zapewnia podstawową funkcjonalność dla asynchronicznych obiektów wejścia i wyjścia. Klasa asio::ip::tcp::resolver zapewnia możliwość rozwiązania zapytania w formie listy węzłów końcowych. Jej funkcja członkowska o nazwie resolve() służy do konwersji nazw hostów i usług na listę węzłów końcowych. Chociaż istnieją różne przeciążenia tej funkcji, to przeciążenie, którego używamy w tym zadaniu, pobiera wersję protokołu (w tym przypadku IPv4, ale może również używać IPv6), identyfikator hosta (jest to nazwa lub adres numeryczny w postaci łańcucha), a także identyfikator usługi (może być numerem portu). W przypadku powodzenia funkcja zwraca listę węzłów końcowych, a w przeciwnym razie zgłasza wyjątek. Klasa asio::ip::tcp::endpoint reprezentuje węzeł końcowy, z którym można skojarzyć gniazdo TCP. 249 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Funkcja get_ip_address() została zaimplementowana w następujący sposób: #define ASIO_STANDALONE #include "asio.hpp" std::vector<std::string> get_ip_address(std::string_view hostname) { std::vector<std::string> ips; try { asio::io_context context; asio::ip::tcp::resolver resolver(context); auto endpoints = resolver.resolve(asio::ip::tcp::v4(), hostname.data(), ""); for (auto e = endpoints.begin(); e != endpoints.end(); ++e) ips.push_back( ((asio::ip::tcp::endpoint)*e).address().to_string()); } catch (std::exception const & e) { std::cerr << "wyjątek: " << e.what() << std::endl; } return ips; } Na poniższym listingu zaprezentowano przykład użycia funkcji get_ip_address(): int main() { setlocale(LC_ALL, "polish"); auto ips = get_ip_address("helion.pl"); for (auto const & ip : ips) std::cout << ip << std::endl; } 96. Gra Fizz-Buzz klient-serwer Aby rozwiązać to zadanie, ponownie skorzystamy z biblioteki Asio. Tym razem jednak musimy napisać dwa programy: serwer i klienta. Serwer akceptuje połączenia TCP na określonym porcie, otwiera podłączone gniazdo, a następnie rozpoczyna odczytywanie z niego. Dane odczytane z gniazda interpretuje jako liczbę podaną w grze Fizz-Buzz, wysyła odpowiedź i czeka na kolejne informacje wejściowe. Klient łączy się z hostem na określonym porcie, wysyła liczbę wprowadzoną w konsoli oraz czeka na odpowiedź z serwera, którą po odebraniu wyświetla. 250 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi Implementacja gry Fizz-Buzz po stronie serwera jest dość prosta i nie powinna wymagać dodatkowych wyjaśnień. Funkcja fizzbuzz() przedstawiona w poniższym fragmencie kodu wykorzystuje liczbę i zwraca wynik jako łańcuch: std::string fizzbuzz(int const number) { if(number != 0) { auto m3 = number % 3; auto m5 = number % 5; if(m3 == 0 && m5 == 0) return "fizzbuzz"; else if(m5 == 0) return "buzz"; else if(m3 == 0) return "fizz"; } return std::to_string(number); } Istnieją dwa główne komponenty, które będziemy implementować po stronie serwera. Pierwszy z nich nazywa się session. Jego celem jest odczytywanie z podłączonego gniazda i zapisywanie do niego. Został on zbudowany z obiektu asio::ip::tcp::socket i używa jego metod: async_read_some() oraz async_write_some() do odczytywania i zapisywania danych. Jak sama nazwa wskazuje, są to operacje asynchroniczne, dlatego po ich zakończeniu jest wywoływana odpowiednia procedura obsługi. Po pomyślnym odczytaniu danych z gniazda następuje zapisanie do niego wyniku funkcji fizzbuzz() dla odebranej liczby. Gdy zapis do gniazda zakończy się pomyślnie, rozpoczyna się ponowne odczytywanie. Implementacja klasy session została przedstawiona na poniższym listingu: #define ASIO_STANDALONE #include "asio.hpp" class session : public std::enable_shared_from_this<session> { public: session(asio::ip::tcp::socket socket) : tcp_socket(std::move(socket)) { } void start() { read(); } private: void read() { auto self(shared_from_this()); tcp_socket.async_read_some( asio::buffer(data, data.size()), [this, self](std::error_code const ec, std::size_t const length){ if (!ec) { 251 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów auto number = std::string(data.data(), length); auto result = fizzbuzz(std::atoi(number.c_str())); std::cout << number << " -> " << result << std::endl; write(result); } }); } void write(std::string_view response) { auto self(shared_from_this()); tcp_socket.async_write_some( asio::buffer(response.data(), response.length()), [this, self](std::error_code const ec, std::size_t const) { if (!ec) read(); }); } std::array<char, 1024> data; asio::ip::tcp::socket tcp_socket; }; Drugi komponent, który tworzymy, będzie używany do akceptowania połączeń przychodzących. Nazywa się on server i wykorzystuje klasę asio::ip::tcp::acceptor do akceptowania nowych połączeń dla hosta lokalnego i określonego portu. Po pomyślnym otwarciu nowego gniazda na podstawie niego jest tworzony obiekt session, a następnie zostaje wywołana metoda start() w celu rozpoczęcia odczytu danych z klienta. Klasę server zaprezentowano poniżej: class server { public: server(asio::io_context& context, short const port) : tcp_acceptor(context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)) , tcp_socket(context) { std::cout << "serwer działający na porcie " << port << std::endl; accept(); } private: void accept() { tcp_acceptor.async_accept(tcp_socket, [this](std::error_code ec) { if (!ec) std::make_shared<session>(std::move(tcp_socket))->start(); accept(); }); 252 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi } asio::ip::tcp::acceptor tcp_acceptor; asio::ip::tcp::socket tcp_socket; }; Poniższa funkcja run_server() tworzy obiekt asio::io_context i instancję klasy server, która natychmiast rozpoczyna przyjmowanie połączeń przychodzących i wywołuje metodę run() dla zmiennej context. Powoduje to uruchomienie pętli przetwarzania zdarzeń, blokującej działania do momentu zakończenia wszystkich operacji i zaprzestania wywoływania procedur obsługi lub do chwili, gdy obiekt asio::io_context zostanie zatrzymany po wykonaniu metody stop(). Funkcja run_server() działa przez czas nieokreślony aż do wystąpienia wyjątku: void run_server(short const port) { try { asio::io_context context; server srv(context, port); context.run(); } catch (std::exception& e) { std::cerr << "wyjątek: " << e.what() << std::endl; } } int main() { run_server(11234); } Po stronie klienta implementacja jest nieco prostsza. Metoda asio::connect() służy do ustanowienia na określonym porcie połączenia TCP z hostem. Po nawiązaniu połączenia do wysyłania danych na serwer i ich odbierania używa się synchronicznych metod write_some() i read_some() pochodzących z klasy asio::ip::tcp::socket. Działają one w pętli na podstawie danych wprowadzonych w konsoli przez użytkownika dopóty, dopóki liczby zawierają się w zakresie od 1 do 99. Oto funkcja run_client(), która implementuje opisane wymagania: void run_client(std::string_view host, short const port) { try { asio::io_context context; asio::ip::tcp::socket tcp_socket(context); asio::ip::tcp::resolver resolver(context); asio::connect(tcp_socket, resolver.resolve({ host.data(), std::to_string(port) })); while (true) { 253 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów std::cout << "liczba [1-99]: "; int number; std::cin >> number; if (std::cin.fail() || number < 1 || number > 99) break; auto request = std::to_string(number); tcp_socket.write_some(asio::buffer(request, request.length())); std::array<char, 1024> reply; auto reply_length = tcp_socket.read_some(asio::buffer(reply, reply.size())); std::cout << "odpowiedź: "; std::cout.write(reply.data(), reply_length); std::cout << std::endl; } } catch (std::exception const & e) { std::cerr << "wyjątek: " << e.what() << std::endl; } } int main() { setlocale(LC_ALL, "polish"); run_client("localhost", 11234); } Poniższy rysunek przedstawia zrzut ekranów serwera (po lewej) i klienta (po prawej): 254 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi 97. Kursy wymiany bitcoinów W ramach różnych usług online udostępniane są interfejsy API do sprawdzania cen rynkowych bitcoinów i kursów ich wymiany. Usługa, z której możesz skorzystać za darmo, jest dostępna pod adresem https://blockchain.info/ticker. Żądanie HTTP typu GET zwraca obiekt JSON zawierający ceny rynkowe różnych walut. Dokumentacja API znajduje się pod adresem https://blockchain.info/api/exchange_rates_api. Oto fragment otrzymanego obiektu JSON: { "USD": { "15m": 8196.491155299998, "last": 8196.491155299998, "buy": 8196.491155299998, "sell": 8196.491155299998, "symbol": "$" }, "GBP": { "15m": 5876.884158350099, "last": 5876.884158350099, "buy": 5876.884158350099, "sell": 5876.884158350099, "symbol": "£" } } Istnieją różne biblioteki, które można wykorzystać do przesyłania danych poprzez sieć. Szeroko stosowanym produktem jest curl. Jest to projekt zawierający narzędzie wiersza poleceń (cURL) oraz bibliotekę (libcurl). Oba składniki zostały napisane w języku C obsługującym wiele protokołów, w tym HTTP/HTTPS, FTP/FTPS, Gopher, LDAP/LDAPS, POP3/POP3S i SMTP/SMTPS. Środowisko jest dostępne pod adresem https://curl.haxx.se/. Istnieje kilka bibliotek C++ opakowujących bibliotekę libcurl. Takim właśnie wieloplatformowym produktem o otwartym kodzie źródłowym jest biblioteka curlcpp, napisana przez Giuseppe Persico i dostępna pod adresem https://github.com/JosephP91/curlcpp. Do rozwiązania tego i kolejnych zadań użyjemy wspomnianych bibliotek. Instrukcje dotyczące budowania bibliotek libcurl i curlcpp można znaleźć w dokumentacji obu projektów. Jeśli używasz kodu źródłowego dołączonego do książki, to wiedz, że dzięki skryptom CMake wszystko zostało już dla Ciebie odpowiednio skonfigurowane. Jeśli chciałbyś jednak skompilować biblioteki samodzielnie i używać ich w innych projektach, musisz wykonać różne działania w zależności od platformy, dla której budujesz. Poniżej przedstawiono instrukcje dotyczące stworzenia konfiguracji debugującej dla systemów Windows i macOS. W systemie Windows, używając Visual Studio 2017, musisz wykonać następujące czynności: 1. Pobierz bibliotekę cURL (spod adresu https://curl.haxx.se/download.html), rozpakuj ją i odszukaj solucję Visual Studio (powinna się znajdować w lokalizacji projects\Windows\VC15\curl-all.sln). Otwórz solucję i zbuduj konfigurację LIB Debug - DLL Windows SSPI dla docelowej platformy, której potrzebujesz (Win32 lub x64). Rezultatem będzie statyczny plik biblioteki o nazwie libcurl.lib. 255 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów 2. Pobierz bibliotekę curlcpp (spod adresu https://github.com/JosephP91/curlcpp), utwórz folder o nazwie build i uruchom w nim narzędzie CMake, definiując zmienne CURL_LIBRARY i CURL_INCLUDE_DIR. Pierwsza z nich musi wskazywać na plik libcurl.lib, a druga na folder z nagłówkami CURL. Otwórz wygenerowany projekt i go skompiluj. Wynikiem będzie statyczny plik biblioteki o nazwie curlcpp.lib. 3. W projekcie Visual Studio, w którym chcesz użyć biblioteki curlcpp, należy dodać nazwę CURL_STATICLIB do definicji preprocesora, ścieżki do folderów curl\include i curlcpp\include — do listy Additional Include Directories (dodatkowe katalogi nagłówków), a foldery wyjściowe obu bibliotek — do listy Additional Library Directories (dodatkowe katalogi bibliotek). Na koniec musisz połączyć swój projekt z następującymi bibliotekami statycznymi: libcurl.lib, curlcpp.lib, Crypt32.lib, ws2_32.lib, winmm.lib i wldap32.lib. Z drugiej strony w systemie macOS i środowisku Xcode powinieneś wykonać następujące czynności: 1. Pobierz bibliotekę openssl (spod adresu https://www.openssl.org/), rozpakuj ją, po czym uruchom następujące polecenia, aby ją skompilować i zainstalować: ./Configure darwin64-x86_64-cc shared enable-ec_nistp_64_gcc_128 no-ssl2 no-ssl3 no-comp –-openssldir=/usr/local/ssl/macos-x86_64 make depend sudo make install 2. Pobierz bibliotekę cURL (spod adresu https://curl.haxx.se/download.html), rozpakuj ją i utwórz folder o nazwie build, a następnie uruchom w nim CMake, określając zmienne OPENSSL_ROOT_DIR i OPENSSL_INCLUDE_DIR, aby skonfigurować openssl. Jeśli chcesz wyłączyć generowanie projektów testów i dokumentacji, przypisz zmiennym BUILD_TESTING, BUILD_CURL_EXE i USE_MANUAL wartość OFF. Wynikiem kompilacji debugującej będzie plik o nazwie libcurl-d.dylib: cmake -G Xcode .. -DOPENSSL_ROOT_DIR=/usr/local/bin -DOPENSSL_INCLUDE_ DIR=/usr/local/include/ 3. Pobierz bibliotekę curlcpp (spod adresu https://github.com/JosephP91/curlcpp), utwórz folder o nazwie build i uruchom w nim narzędzie CMake, odpowiednio definiując zmienne CURL_LIBRARY i CURL_INCLUDE_DIR. Pierwsza z nich musi wskazywać na plik libcurl-d.dylib, a druga na folder z nagłówkami CURL. Otwórz wygenerowany projekt i go skompiluj. Rezultatem będzie plik o nazwie libcurlcpp.a: cmake -G Xcode .. -DCURL_LIBRARY=<path>/curl-7.59.0/build/lib/Debug/ libcurl-d.dylib -DCURL_INCLUDE_DIR=<path>/curl-7.59.0/include 4. W projekcie Xcode, w którym chcesz używać bibliotek cURL i curlcpp, dodaj nazwę CURL_STATICLIB do makr preprocesora, ścieżki do folderów curl/include i curlcpp/include — do listy Header Search Paths (ścieżki wyszukiwania nagłówków), a foldery wyjściowe obu bibliotek — do listy Library Search Paths (ścieżki wyszukiwania bibliotek). Dodatkowo do listy Link Binary With Libraries (biblioteki linkowane binarnie) musisz dodać dwie biblioteki statyczne: libcurl-d.dylib i libcurlcpp.a. 256 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi Biblioteka libcurl ma dwa modele programowania (zwane interfejsami): easy i multi. Interfejs easy (łatwy) zapewnia synchroniczny, wydajny i prosty w użyciu model programowania służący do przesyłania danych. Interfejs multi (wielowątkowy) jest modelem asynchronicznym, który zapewnia wykonywanie wielu transferów danych za pomocą pojedynczego wątku lub większej liczby wątków. Korzystając z interfejsu easy, musisz najpierw zainicjować sesję, a następnie zdefiniować różne opcje, w tym adres URL i być może funkcje wywołania zwrotnego, które będą wykonywane w momencie, gdy dane staną się dostępne. Po zakończeniu konfiguracji wykonujesz transfer danych będący operacją blokującą, która kończy swoje działanie dopiero z chwilą zakończenia przesyłania informacji. Po zakończeniu transferu możesz odczytać informacje o nim, a następnie powinieneś zamknąć sesję. Inicjowanie i zamykanie sesji są obsługiwane w bibliotece curlcpp zgodnie ze wzorcem projektowym RAII. Poniżej zaprezentowana funkcja get_json_document() pobiera adres URL i wykonuje żądanie HTTP GET. Odpowiedź z serwera jest zapisywana do strumienia std::stringstream, który zostaje zwrócony do wątku wywołującego: #include #include #include #include "curl_easy.h" "curl_form.h" "curl_ios.h" "curl_exception.h" std::stringstream get_json_document(std::string_view url) { std::stringstream str; try { curl::curl_ios<std::stringstream> writer(str); curl::curl_easy easy(writer); easy.add<CURLOPT_URL>(url.data()); easy.add<CURLOPT_FOLLOWLOCATION>(1L); easy.perform(); } catch (curl::curl_easy_exception const & error) { auto errors = error.get_traceback(); error.print_traceback(); } return str; } Jak wspomniano wcześniej, podczas wykonywania operacji HTTP GET dla adresu https:// blockchain.info/ticker otrzymujemy obiekt JSON. Do reprezentowania danych zwracanych przez ten interfejs API są używane następujące typy: struct exchange_info { double delay_15m_price; double latest_price; double buying_price; 257 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów double selling_price; std::string symbol; }; using blockchain_rates = std::map<std::string, exchange_info>; Do obsługi danych JSON możemy użyć biblioteki nlohmann/json. Szczegóły dotyczące tej biblioteki można znaleźć w rozdziale 9. „Serializacja danych”. Poniższa funkcja from_json() deserializuje obiekt exchange_info z formatu JSON: #include "json.hpp" using json = nlohmann::json; void from_json(const json& jdata, exchange_info& info) { info.delay_15m_price = jdata.at("15m").get<double>(); info.latest_price = jdata.at("last").get<double>(); info.buying_price = jdata.at("buy").get<double>(); info.selling_price = jdata.at("sell").get<double>(); info.symbol = jdata.at("symbol").get<std::string>(); } Łącząc wszystko razem, możemy napisać program, który z serwera pobiera informacje o kursach wymiany, deserializuje odpowiedź JSON i wyświetla informacje w konsoli: int main() { auto doc = get_json_document("https://blockchain.info/ticker"); json jdata; doc >> jdata; blockchain_rates rates = jdata; for (auto const & kvp : rates) { std::cout << "1BPI = " << kvp.second.latest_price << " " << kvp.first << std::endl; } } 98. Pobieranie wiadomości e-mailowych przy użyciu protokołu IMAP Protokół Internet Message Access Protocol (w skrócie IMAP) to internetowy protokół służący do pobierania wiadomości e-mailowych z serwera pocztowego przy użyciu protokołu TCP/IP. Większość dostawców usługi poczty elektronicznej, włącznie z najważniejszymi: Gmailem, Outlook.com i Yahoo! Mail oferuje wsparcie dla tego protokołu. Istnieje kilka bibliotek C++ pozwalających na obsługę protokołu IMAP, między innymi VMIME, który jest 258 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi wieloplatformowym produktem o otwartym kodzie obsługującym IMAP, POP i SMTP. Jednak w tej książce użyję biblioteki cURL (a dokładniej mówiąc, libcurl) w celu wysyłania żądań HTTP do serwera pocztowego przy użyciu protokołu IMAP. Wymagane działanie można osiągnąć za pomocą kilku poleceń IMAP. Na poniższej liście imap.domena.com jest domeną przykładową: Polecenie GET imaps://imap.domena.com pobiera wszystkie foldery znajdujące się w skrzynce pocztowej. Jeśli chcesz uzyskać podfoldery określonego folderu, na przykład inbox, powinieneś wykonać GET imaps://imap.domena.com/ <nazwa_folderu>. Polecenie SEARCH UNSEEN imaps://imap.domena.com/<nazwa_folderu> pobiera identyfikatory wszystkich nieprzeczytanych wiadomości e-mailowych z danego folderu. Polecenie GET imaps://imap.domain.com/<nazwa_folderu>/;UID=<id> pobiera wiadomość e-mailową o podanym identyfikatorze z określonego folderu. Jeśli korzystasz z serwerów Gmail, Outlook.com lub Yahoo! Mail, to wiedz, że ich ustawienia IMAP są bardzo podobne do siebie. Wszystkie używają portu 933 z szyfrowaniem TLS. Nazwą użytkownika jest Twój adres e-mailowy, a hasłem — hasło do konta. Różnica polega na nazwach serwerów. W przypadku Gmaila jest to imap.gmail.com, w przypadku Outlook.com jest to imap-mail.outlook.com, a w przypadku Yahoo! Mail mamy imap.mail.yahoo.com. Pamiętaj, że jeśli korzystasz z weryfikacji dwuetapowej, musisz wygenerować hasło dla aplikacji innej firmy i użyć go zamiast hasła do konta. Wspomniane funkcjonalności zostały zaimplementowane w poniższym fragmencie kodu jako funkcje składowe klasy imap_connection. Wykorzystuje ona adres URL serwera, numer portu, nazwę użytkownika i jego hasło. Metoda pomocnicza setup_easy() inicjuje obiekt curl::curl_easy przy użyciu ustawień uwierzytelniania, takich jak port, nazwa użytkownika i hasło oraz szyfrowanie TLS. Definiuje także inne typowe ustawienia, takie jak identyfikator aplikacji (opcjonalnie): class imap_connection { public: imap_connection(std::string_view url, unsigned short const port, std::string_view user, std::string_view pass): url(url), port(port), user(user), pass(pass) { } std::string get_folders(); std::vector<unsigned int> fetch_unread_uids(std::string_view folder); std::string fetch_email(std::string_view folder, unsigned int uid); private: void setup_easy(curl::curl_easy& easy) 259 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów { easy.add<CURLOPT_PORT>(port); easy.add<CURLOPT_USERNAME>(user.c_str()); easy.add<CURLOPT_PASSWORD>(pass.c_str()); easy.add<CURLOPT_USE_SSL>(CURLUSESSL_ALL); easy.add<CURLOPT_SSL_VERIFYPEER>(0L); easy.add<CURLOPT_SSL_VERIFYHOST>(0L); easy.add<CURLOPT_USERAGENT>("libcurl-agent/1.0"); } private: std::string unsigned short std::string std::string }; url; port; user; pass; Metoda get_folders() zwraca listę folderów zawartych w skrzynce pocztowej. Jednakże zwracany jest oryginalny łańcuch otrzymany z serwera bez faktycznego analizowania jego zawartości. W kolejnym ćwiczeniu mógłbyś przeanalizować zwracane dane i wyodrębnić z nich foldery. Funkcja tworzy obiekt curl::curl_easy, inicjuje go odpowiednimi parametrami, takimi jak adres URL i informacje uwierzytelniające, wykonuje żądanie, a następnie zwraca wynik ze strumienia std::stringstream, w którym został on umieszczony: std::string imap_connection::get_folders() { std::stringstream str; try { curl::curl_ios<std::stringstream> writer(str); curl::curl_easy easy(writer); easy.add<CURLOPT_URL>(url.data()); setup_easy(easy); easy.perform(); } catch (curl::curl_easy_exception const & error) { auto errors = error.get_traceback(); error.print_traceback(); } return str.str(); } Oto przykładowy wygląd otrzymanych danych: * * * * * * LIST LIST LIST LIST LIST LIST (\HasNoChildren) "/" "INBOX" (\HasNoChildren) "/" "Notes" (\HasNoChildren) "/" "Trash" (\HasChildren \Noselect) "/" "[Gmail]" (\All \HasNoChildren) "/" "[Gmail]/All Mail" (\Drafts \HasNoChildren) "/" "[Gmail]/Drafts" 260 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi * * * * LIST LIST LIST LIST (\HasNoChildren \Important) "/" "[Gmail]/Important" (\HasNoChildren \Sent) "/" "[Gmail]/Sent Mail" (\HasNoChildren \Junk) "/" "[Gmail]/Spam" (\Flagged \HasNoChildren) "/" "[Gmail]/Starred" Metoda fetch_unread_uids() jest dość podobna do wcześniejszej. Zwraca ona wektor liczb całkowitych bez znaku, reprezentujących identyfikatory nieprzeczytanych wiadomości e-mailowych z określonego folderu. Metoda ta wykonuje żądanie w podobny sposób jak poprzednia funkcja, z tym że analizuje wynik, aby utworzyć listę identyfikatorów e-mailowych. Ustawia również opcję CURLOPT_CUSTOMREQUEST na wartość SEARCH UNSEEN. W wyniku tego domyślna metoda GET zostaje zastąpiona inną metodą (w tym przypadku przez SEARCH): std::vector<unsigned int> imap_connection::fetch_unread_uids(std::string_view folder) { std::stringstream str; try { curl::curl_ios<std::stringstream> writer(str); curl::curl_easy easy(writer); easy.add<CURLOPT_URL>((url.data() + std::string("/") + folder.data() + std::string("/")).c_str()); easy.add<CURLOPT_CUSTOMREQUEST>("SEARCH UNSEEN"); setup_easy(easy); easy.perform(); } catch (curl::curl_easy_exception const & error) { auto errors = error.get_traceback(); error.print_traceback(); } std::vector<unsigned int> uids; str.seekg(8, std::ios::beg); unsigned int uid; while (str >> uid) uids.push_back(uid); return uids; } Ostatnią metodą do zaimplementowania jest fetch_email(). Wykorzystuje ona nazwę folderu i identyfikator e-mailowy, zwracając wiadomość w postaci łańcucha. Ta metoda została zaprezentowana poniżej: std::string imap_connection::fetch_email(std::string_view folder, unsigned int uid) { std::stringstream str; try 261 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów { curl::curl_ios<std::stringstream> writer(str); curl::curl_easy easy(writer); easy.add<CURLOPT_URL>((url.data() + std::string("/") + folder.data() + std::string("/;UID=") + std::to_string(uid)).c_str()); setup_easy(easy); easy.perform(); } catch (curl::curl_easy_exception error) { auto errors = error.get_traceback(); error.print_traceback(); } return str.str(); } Przedstawiona klasa może być używana do pobierania żądanej treści, jak pokazano w poniższym fragmencie kodu. W programie odczytujemy foldery skrzynki pocztowej, a następnie pobieramy identyfikatory wszystkich nieprzeczytanych wiadomości e-mailowych znajdujących się w folderze inbox. Jeśli jakieś wiadomości zostały znalezione, odczytujemy i wyświetlamy najnowszą z nich: int main() { setlocale(LC_ALL, "polish"); imap_connection imap( "imaps://imap.gmail.com", 993, "...(Twoja nazwa użytkownika)...", "...(Twoje hasło)..."); auto folders = imap.get_folders(); std::cout << folders << std::endl; auto info = imap.examine_folder("inbox"); std::cout << info << std::endl; auto uids = imap.fetch_unread_uids("inbox"); if (!uids.empty()) { auto email = imap.fetch_email("inbox", uids.back()); std::cout << email << std::endl; } return 0; } 262 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi 99. Tłumaczenie tekstu na dowolny język Funkcje tłumaczenia tekstu są dostępne w wielu usługach przetwarzania w chmurze, wliczając w to Microsoft Cognitive Services, Google Cloud Translation API i Amazon Translate. W tej książce będę korzystał z usług Cognitive Services dostępnych w Microsoft Azure. Są one zbiorem algorytmów uczenia maszynowego i sztucznej inteligencji, które można wykorzystać do łatwego dodawania inteligentnych funkcjonalności do aplikacji. Jedną z usług jest Text Translate API, która umożliwia wykrywanie języka, tłumaczenie z jednego języka na inny i konwersję tekstu na mowę. Do wysyłania żądań HTTP będziemy w dalszym ciągu wykorzystywać bibliotekę libcurl. Chociaż istnieją różne plany cenowe związane z korzystaniem z usługi Text Translate API, dostępny jest również poziom bezpłatny. W przypadku tłumaczenia tekstu obsługuje on tłumaczenie do dwóch milionów znaków miesięcznie, co powinno wystarczyć dla większości zastosowań demonstracyjnych i tworzenia prototypów. Aby skorzystać z usługi Text Translate API1, musisz wykonać następujące działania: 1. Zaloguj się na swoje konto Azure. Powinieneś je utworzyć, jeśli jeszcze go nie masz. 2. Utwórz nowy zasób Translator Text API (interfejs API dla tłumaczenia tekstu). 3. Po utworzeniu zasobu przejdź do niego i skopiuj jeden z dwóch wygenerowanych kluczy aplikacji. Ten klucz jest niezbędny do nawiązywania połączeń z usługą. 4. Punktem końcowym dla wywołania usługi jest adres https://api.microsofttranslator.com/V2/Http.svc — nie został on udostępniony w prezentacji zasobu. Dokumentacja dotycząca interfejsu programistycznego dla tłumaczenia tekstu jest dostępna pod adresem https://docs.microsoft.com/en-us/azure/cognitive-services/translator/reference/ v2-0-reference. Aby przetłumaczyć tekst, musisz wykonać następujące czynności: 1. Wyślij żądanie GET na adres <punkt końcowy>/Translate. 2. Podaj wymagane parametry zapytania (text i to) oraz ewentualnie opcjonalne, takie jak parametr from oznaczający, z jakiego języka chcesz tłumaczyć (domyślnie wybrany jest angielski). Tekst do przetłumaczenia nie może być dłuższy niż 10 000 znaków i musi zostać zakodowany za pomocą adresu URL. 3. Przekaż niezbędne nagłówki. Wymagany jest co najmniej klucz subskrypcji Ocp-Apim-Subscription-Key, by przekazać klucz aplikacji dla zasobu platformy Azure. 1 Autor przedstawia interfejs w wersji 2.0. W marcu 2018 roku firma Microsoft wdrożyła wersję 3.0 tego interfejsu; więcej informacji na ten temat można uzyskać pod adresem https://blogs.msdn. microsoft.com/translation/2018/05/07/translatorv3/ oraz https://docs.microsoft.com/en-us/azure/cognitiveservices/translator/reference/v3-0-reference — przyp. tłum. 263 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Przykładowe żądanie GET dotyczące przetłumaczenia zdania "hello world!" z języka angielskiego na polski mogłoby wyglądać tak: GET /V2/Http.svc/Translate?to=pl&text=hello%20world%21 host: api.microsofttranslator.com ocp-apim-subscription-key: <tutaj podaj swój klucz> W przypadku poprawnego wywołania otrzymujemy łańcuch XML reprezentujący przetłumaczony tekst. Jest on kodowany w formacie UTF-8. Nie można w tym przypadku otrzymać wyniku w formacie JSON. Wynik dla poprzedniego przykładu uzyskany z serwera powinien zawierać następującą treść: <string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Cześć ludzie!</string> Funkcjonalność tłumaczenia tekstu można zdefiniować w klasie, która w celu uproszczenia wywoływania translatora będzie obsługiwać klucze aplikacji i punkty końcowe. Takie działania wykonuje przedstawiona poniżej klasa text_translator. Wykorzystuje ona dwa łańcuchy, z których jeden reprezentuje punkt końcowy usługi Text Translation API, a drugi jest kluczem aplikacji. Jak wspomniano wcześniej, wynik z serwera jest zwracany w formacie XML. Funkcja członkowska deserialize_result() wyodrębnia właściwy tekst z postaci XML. Aby uprościć kod, używamy w tym celu wyrażenia regularnego, a nie biblioteki XML, co powinno w zupełności wystarczyć na potrzeby naszego programu testowego: class text_translator { public: text_translator(std::string_view endpoint, std::string_view key) : endpoint(endpoint), app_key(key) {} std::wstring translate_text(std::wstring_view wtext, std::string_view to, std::string_view from = "en"); private: std::string deserialize_result(std::string_view text) { std::regex rx(R"(<string.*>(.*)<\/string>)"); std::cmatch match; if (std::regex_search(text.data(), match, rx)) { return match[1]; } return ""; } std::string endpoint; std::string app_key; }; 264 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi Funkcja członkowska translate_text() wykonuje rzeczywiste tłumaczenie. Jej danymi wejściowymi są tekst do przetłumaczenia, język, na który należy tłumaczyć, a także język źródłowego tekstu, który domyślnie jest językiem angielskim. Tekst wejściowy dla tej metody jest łańcuchem zakodowanym w standardzie UTF-16, lecz musi on zostać przekonwertowany na UTF-8. Odpowiedź z serwera jest zakodowana w formacie UTF-8 i musi zostać odpowiednio przekonwertowana na UTF-16. Odbywa się to za pomocą funkcji pomocniczych utf16_to_utf8() i utf8_to_utf16(): std::wstring text_translator::translate_text(std::wstring_view wtext, std::string_view to, std::string_view from = "en") { try { using namespace std::string_literals; std::stringstream str; std::string text = utf16_to_utf8(wtext); curl::curl_ios<std::stringstream> writer(str); curl::curl_easy easy(writer); curl::curl_header header; header.add("Ocp-Apim-Subscription-Key:" + app_key); easy.escape(text); auto url = endpoint + "/Translate"; url += "?from="s + from.data(); url += "&to="s + to.data(); url += "&text="s + text; easy.add<CURLOPT_URL>(url.c_str()); easy.add<CURLOPT_HTTPHEADER>(header.get()); easy.perform(); auto result = deserialize_result(str.str()); return utf8_to_utf16(result); } catch (curl::curl_easy_exception const & error) { auto errors = error.get_traceback(); error.print_traceback(); } catch (std::exception const & ex) { std::err << ex.what() << std::endl; } return {}; } 265 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Dwie funkcje pomocnicze służące do przeprowadzania konwersji między standardami UTF-8 i UTF-16 mają taką postać: std::wstring utf8_to_utf16(std::string_view text) { std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter; std::wstring wtext = converter.from_bytes(text.data()); return wtext; } std::string utf16_to_utf8(std::wstring_view wtext) { std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter; std::string text = converter.to_bytes(wtext.data()); return text; } Klasa text_translator może być używana do tłumaczenia tekstów między różnymi językami, jak pokazano w następującym przykładzie: int main() { #ifdef _WIN32 SetConsoleOutputCP(CP_UTF8); #endif set_utf8_conversion(std::wcout); text_translator tt( "https://api.microsofttranslator.com/V2/Http.svc", "...(Twój klucz API)..."); std::vector<std::tuple<std::wstring, std::string, std::string>> texts { { L"hello world!", "en", "ro"}, { L"what time is it?", "en", "es" }, { L"ceci est un exemple", "fr", "en" } }; for (auto const [text, from, to] : texts) { auto result = tt.translate_text(text, to, from); std::cout << from << ": "; std::wcout << text << std::endl; std::cout << to << ": "; std::wcout << result << std::endl; } } Wyświetlanie znaków UTF-8 w konsoli nie jest jednak proste. W systemie Windows musisz wywołać funkcję SetConsoleOutputCP (CP_UTF8), aby włączyć odpowiednią stronę kodową. Należy także zdefiniować poprawne ustawienia regionalne UTF-8 dla strumienia wyjściowego, co jest wykonywane przy użyciu funkcji set_utf8_conversion(): 266 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi void set_utf8_conversion(std::wostream& stream) { auto codecvt = std::make_unique<std::codecvt_utf8<wchar_t>>(); std::locale utf8locale(std::locale(), codecvt.get()); codecvt.release(); stream.imbue(utf8locale); } Oto wynik działającego przykładowego programu: 100. Wykrywanie twarzy na obrazie Jest to kolejne zadanie, które można rozwiązać za pomocą usług Microsoft Cognitive Services. Jedna z usług, zwana Face API, udostępnia algorytmy wykrywania twarzy, płci, wieku, emocji i różnych fizjonomicznych charakterystycznych punktów oraz atrybutów, a także pozwala na wyszukiwanie podobieństw twarzy, identyfikowanie osób, grupowanie obrazów na podstawie wizualnych podobieństw fizjonomicznych oraz na wiele innych działań. Podobnie jak w przypadku usługi Text Translate API, istnieje bezpłatny plan cenowy, który pozwala na wykonanie do 30 000 transakcji w ciągu miesiąca, przy czym jedynie 20 na minutę. Transakcja jest w zasadzie wywołaniem interfejsu API. Istnieje kilka płatnych planów cenowych, które pozwalają na wykonanie większej liczby transakcji w ciągu miesiąca i minuty, lecz w przypadku naszego zadania możesz skorzystać z poziomu darmowego. Udostępniona jest także 30-dniowa wersja próbna, z której możesz skorzystać. Aby rozpocząć używanie interfejsu Face API, musisz wykonać następujące działania: 1. Zaloguj się na swoje konto Azure. Powinieneś je utworzyć, jeśli jeszcze go nie masz. 2. Utwórz nowy zasób Face API (interfejs API obsługi twarzy). 3. Po utworzeniu zasobu przejdź do niego i skopiuj jeden z dwóch wygenerowanych kluczy aplikacji, a także adres punktu końcowego. Obie dane są niezbędne do nawiązywania połączeń z usługą. 267 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Dokumentacja dotycząca interfejsu Face API jest dostępna pod adresem https://azure.micro soft.com/pl-pl/services/cognitive-services/face/. Powinieneś dokładnie przeczytać opis metody Detect. Oto co trzeba zrobić: 1. Wyślij żądanie POST na adres <punkt końcowy>/Detect. 2. Podaj opcjonalne parametry zapytania, takie jak flagi dla zwracanego identyfikatora twarzy, charakterystyczne punkty, a także łańcuch wskazujący, jakie atrybuty twarzy należy przeanalizować i zwrócić. 3. Podaj opcjonalne i obowiązkowe nagłówki żądań. W celu przekazania klucza aplikacji dla zasobu platformy Azure jest wymagany co najmniej klucz subskrypcji Ocp-Apim-Subscription-Key. 4. Prześlij obraz do analizy. W obiekcie JSON można przekazać adres URL do obrazu (z typem zawartości application/json) lub faktyczny obraz (z typem zawartości application/octet-stream). Ponieważ nasze zadanie wymaga wczytywania obrazu z dysku, musimy użyć drugiej opcji. W przypadku poprawnego wywołania odpowiedź jest obiektem JSON zawierającym wszystkie żądane informacje. W przypadku niepowodzenia odpowiedź jest innym obiektem JSON z informacją o błędzie. Oto przykładowe żądanie przeanalizowania i zwrócenia cech charakterystycznych twarzy, wieku, płci i emocji, a także identyfikatora twarzy. Informacje o zidentyfikowanej twarzy są przechowywane na serwerze przez 24 godziny i mogą być używane z innymi algorytmami Face API: POST /face/v1.0/detect?returnFaceId=true&returnFaceLandmarks=true&returnFaceAttributes= age,gender,emotion host: westeurope.api.cognitive.microsoft.com ocp-apim-subscription-key: <tutaj podaj klucz API> content-type: application/octet-stream content-length: <rozmiar danych> accept: */* Poniżej przedstawiono wynik w formacie JSON zwrócony przez serwer. Umieściliśmy tu tylko jego fragment, ponieważ cała odpowiedź jest dość długa. Rzeczywisty wynik obejmuje 27 różnych charakterystycznych punktów twarzy, natomiast tutaj pokazaliśmy tylko dwa pierwsze z nich: [{ "faceId": "0ddb348a-6038-4cbb-b3a1-86fffe6c1f26", "faceRectangle": { "top": 86, "left": 165, "width": 72, "height": 72 }, "faceLandmarks": { "pupilLeft": { "x": 187.5, 268 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi "y": 102.9 }, "pupilRight": { "x": 214.6, "y": 104.7 } }, "faceAttributes": { "gender": "male", "age": 54.9, "emotion": { "anger": 0, "contempt": 0, "disgust": 0, "fear": 0, "happiness": 1, "neutral": 0, "sadness": 0, "surprise": 0 } } }] Do deserializacji obiektu JSON użyjemy biblioteki nlohmann/json, a w celu utworzenia żądań HTTP zastosujemy bibliotekę libcurl. Następujące klasy służą do obsługi wyniku otrzymanego z serwera w przypadku powodzenia: struct { int int int int }; face_rectangle width = 0; height = 0; left = 0; top = 0; struct face_point { double x = 0; double y = 0; }; struct face_landmarks { face_point pupilLeft; face_point pupilRight; face_point noseTip; face_point mouthLeft; face_point mouthRight; face_point eyebrowLeftOuter; face_point eyebrowLeftInner; face_point eyeLeftOuter; face_point eyeLeftTop; face_point eyeLeftBottom; face_point eyeLeftInner; 269 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów face_point face_point face_point face_point face_point face_point face_point face_point face_point face_point face_point face_point face_point face_point face_point face_point eyebrowRightInner; eyebrowRightOuter; eyeRightInner; eyeRightTop; eyeRightBottom; eyeRightOuter; noseRootLeft; noseRootRight; noseLeftAlarTop; noseRightAlarTop; noseLeftAlarOutTip; noseRightAlarOutTip; upperLipTop; upperLipBottom; underLipTop; underLipBottom; }; struct face_emotion { double anger = 0; double contempt = 0; double disgust = 0; double fear = 0; double happiness = 0; double neutral = 0; double sadness = 0; double surprise = 0; }; struct face_attributes { std::string gender; double age; face_emotion emotion; }; struct face_info { std::string face_rectangle face_landmarks face_attributes }; faceId; rectangle; landmarks; attributes; Ponieważ obraz może zawierać wiele twarzy, serwer w rzeczywistości zwraca tablicę obiektów. Faktycznym typem odpowiedzi jest więc przedstawiony poniżej face_detect_response: using face_detect_response = std::vector<face_info>; Deserializacja jest wykonywana tak, jak w innych przypadkach omówionych w tej książce, czyli przy użyciu przeciążonych funkcji from_json(). Jeśli już rozwiązałeś inne zadania związane z deserializacją obiektów JSON, powinieneś dobrze znać to zagadnienie: 270 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi using json = nlohmann::json; void from_json(const json& jdata, face_rectangle& rect) { rect.width = jdata.at("width").get<int>(); rect.height = jdata.at("height").get<int>(); rect.top = jdata.at("top").get<int>(); rect.left = jdata.at("left").get<int>(); } void from_json(const json& jdata, face_point& point) { point.x = jdata.at("x").get<double>(); point.y = jdata.at("y").get<double>(); } void from_json(const json& jdata, face_landmarks& mark) { mark.pupilLeft = jdata.at("pupilLeft"); mark.pupilRight = jdata.at("pupilRight"); mark.noseTip = jdata.at("noseTip"); mark.mouthLeft = jdata.at("mouthLeft"); mark.mouthRight = jdata.at("mouthRight"); mark.eyebrowLeftOuter = jdata.at("eyebrowLeftOuter"); mark.eyebrowLeftInner = jdata.at("eyebrowLeftInner"); mark.eyeLeftOuter = jdata.at("eyeLeftOuter"); mark.eyeLeftTop = jdata.at("eyeLeftTop"); mark.eyeLeftBottom = jdata.at("eyeLeftBottom"); mark.eyeLeftInner = jdata.at("eyeLeftInner"); mark.eyebrowRightInner = jdata.at("eyebrowRightInner"); mark.eyebrowRightOuter = jdata.at("eyebrowRightOuter"); mark.eyeRightInner = jdata.at("eyeRightInner"); mark.eyeRightTop = jdata.at("eyeRightTop"); mark.eyeRightBottom = jdata.at("eyeRightBottom"); mark.eyeRightOuter = jdata.at("eyeRightOuter"); mark.noseRootLeft = jdata.at("noseRootLeft"); mark.noseRootRight = jdata.at("noseRootRight"); mark.noseLeftAlarTop = jdata.at("noseLeftAlarTop"); mark.noseRightAlarTop = jdata.at("noseRightAlarTop"); mark.noseLeftAlarOutTip = jdata.at("noseLeftAlarOutTip"); mark.noseRightAlarOutTip = jdata.at("noseRightAlarOutTip"); mark.upperLipTop = jdata.at("upperLipTop"); mark.upperLipBottom = jdata.at("upperLipBottom"); mark.underLipTop = jdata.at("underLipTop"); mark.underLipBottom = jdata.at("underLipBottom"); } void from_json(const json& jdata, face_emotion& emo) { emo.anger = jdata.at("anger").get<double>(); emo.contempt = jdata.at("contempt").get<double>(); emo.disgust = jdata.at("disgust").get<double>(); emo.fear = jdata.at("fear").get<double>(); emo.happiness = jdata.at("happiness").get<double>(); 271 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów emo.neutral = jdata.at("neutral").get<double>(); emo.sadness = jdata.at("sadness").get<double>(); emo.surprise = jdata.at("surprise").get<double>(); } void from_json(const json& jdata, face_attributes& attr) { attr.age = jdata.at("age").get<double>(); attr.emotion = jdata.at("emotion"); attr.gender = jdata.at("gender").get<std::string>(); } void from_json(const json& jdata, face_info& info) { info.faceId = jdata.at("faceId").get<std::string>(); info.attributes = jdata.at("faceAttributes"); info.landmarks = jdata.at("faceLandmarks"); info.rectangle = jdata.at("faceRectangle"); } Jeśli jednak z jakiegoś powodu wywołanie funkcji się nie powiedzie, zwracany jest inny obiekt w celu opisania błędu. W tym przypadku używana jest klasa face_error_response: struct face_error { std::string code; std::string message; }; struct face_error_response { face_error error; }; Potrzebujemy również przeciążenia funkcji from_json(), które umożliwi deserializację odpowiedzi opisującej błąd. Implementacja odpowiednich funkcji jest następująca: void from_json(const json& jdata, face_error& error) { error.code = jdata.at("code").get<std::string>(); error.message = jdata.at("message").get<std::string>(); } void from_json(const json& jdata, face_error_response& response) { response.error = jdata.at("error"); } Mając wszystkie wcześniej zdefiniowane funkcje, możemy obecnie zrealizować rzeczywiste połączenie z interfejsem Face API. Podobnie jak miało to miejsce w przypadku tłumaczenia tekstu, możemy utworzyć klasę opakowującą żądaną funkcjonalność (którą można rozbudować). Pomoże ona nam w łatwym zarządzaniu kluczem aplikacji i punktem końcowym (zamiast przekazywania ich za każdym razem podczas wywołania funkcji). W tym celu została utworzona klasa face_manager: 272 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi class face_manager { public: face_manager(std::string_view endpoint, std::string_view key) : endpoint(endpoint), app_key(key) {} face_detect_response detect_from_file(std::string_view path); private: face_detect_response parse_detect_response(long const status, std::stringstream & str); std::string endpoint; std::string app_key; }; Metoda detect_from_file() używa łańcucha reprezentującego ścieżkę do obrazu znajdującego się na dysku. Wczytuje obraz, wysyła go do usługi Face API, deserializuje odpowiedź i zwraca obiekt face_detect_response, który jest zbiorem obiektów typu face_info. Ponieważ podczas wywołania przekazujemy rzeczywisty obraz, typem zawartości jest application/ octet-stream. Zawartość pliku musimy przekazać do pola CURLOPT_POSTFIELDS zawartego w interfejsie curl_easy, a jego rozmiar — do pola CURLOPT_POSTFIELDSIZE: face_detect_response face_manager::detect_from_file(std::string_view path) { try { auto data = load_image(path); if (!data.empty()) { std::stringstream str; curl::curl_ios<std::stringstream> writer(str); curl::curl_easy easy(writer); curl::curl_header header; header.add("Ocp-Apim-Subscription-Key:" + app_key); header.add("Content-Type:application/octet-stream"); auto url = endpoint + "/detect" "?returnFaceId=true" "&returnFaceLandmarks=true" "&returnFaceAttributes=age,gender,emotion"; easy.add<CURLOPT_URL>(url.c_str()); easy.add<CURLOPT_HTTPHEADER>(header.get()); easy.add<CURLOPT_POSTFIELDSIZE>(data.size()); easy.add<CURLOPT_POSTFIELDS>(reinterpret_cast<char*>( data.data())); 273 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów easy.perform(); auto status = easy.get_info<CURLINFO_RESPONSE_CODE>(); return parse_detect_response(status.get(), str); } } catch (curl::curl_easy_exception const & error) { auto errors = error.get_traceback(); error.print_traceback(); } catch (std::exception const & ex) { std::cerr << ex.what() << std::endl; } return {}; } Metoda parse_detect_response() odpowiada za deserializację odpowiedzi JSON z serwera. Wykorzystuje w tym celu rzeczywisty kod odpowiedzi HTTP. Jeśli wykonanie funkcji się powiedzie, status jest równy wartości 200. W przypadku niepowodzenia otrzymujemy kod 4xx: face_detect_response face_manager::parse_detect_response( long const status, std::stringstream & str) { json jdata; str >> jdata; try { if (status == 200) { face_detect_response response = jdata; return response; } else if (status >= 400) { face_error_response response = jdata; std::cout << response.error.code << std::endl << response.error.message << std::endl; } } catch (std::exception const & ex) { std::cerr << ex.what() << std::endl; } return {}; } 274 ecb84badecb8c394873734f1e9bfb90f e Rozdział 12. • Praca w sieci i usługi W celu wczytania pliku obrazu z dysku funkcja detect_from_file() używa funkcji o nazwie load_image(). Jako argument wykorzystuje ona łańcuch reprezentujący ścieżkę do pliku i zwraca jego zawartość w formacie std::vector<uint8_t>. Implementacja tej funkcji jest następująca: std::vector<uint8_t> load_image(std::string_view filepath) { std::vector<uint8_t> data; std::ifstream ifile(filepath.data(), std::ios::binary | std::ios::ate); if (ifile.is_open()) { auto size = ifile.tellg(); ifile.seekg(0, std::ios::beg); } } data.resize(static_cast<size_t>(size)); ifile.read(reinterpret_cast<char*>(data.data()), size); return data; Obecnie mamy wszystko, co jest konieczne, by używać algorytmu Detect zawartego w interfejsie Face API, deserializować otrzymaną odpowiedź i wyświetlać jej zawartość w konsoli. Poniższy program wyświetla informacje związane z twarzami zidentyfikowanymi w pliku o nazwie albert_and_elsa.jpg znajdującym się w folderze res naszego projektu. Pamiętaj, aby z zasobem Face API używać swojego punktu końcowego i dedykowanego klucza aplikacji: int main() { setlocale(LC_ALL, "polish"); face_manager manager( "https://westeurope.api.cognitive.microsoft.com/face/v1.0", "...(Twój klucz API)..."); #ifdef _WIN32 std::string path = R"(res\albert_and_elsa.jpg)"; #else std::string path = R"(./res/albert_and_elsa.jpg)"; #endif auto results = manager.detect_from_file(path); for (auto const { std::cout << << << << } } & face : results) "ID twarzy: "wiek: "płeć: "ramka: " " " " << << << << << face.faceId << std::endl << face.attributes.age << std::endl << face.attributes.gender << std::endl << "{" << face.rectangle.left "," << face.rectangle.top "," << face.rectangle.width "," << face.rectangle.height "}" << std::endl << std::endl; 275 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Oto zawartość obrazu albert_and_elsa.jpg: Poniżej przedstawiono wynik działania programu. Należy pamiętać, że rzeczywiste tymczasowe identyfikatory twarzy będą się oczywiście zmieniać przy każdym połączeniu. Jak widać na podstawie otrzymanego wyniku, zidentyfikowano dwie twarze. Pierwsza z nich to Albert Einstein, a wykryty wiek jest równy 54,9 roku. To zdjęcie zostało zrobione w 1921 roku, gdy Einstein miał w rzeczywistości 42 lata. Drugie oblicze to Elsa Einstein, żona Alberta Einsteina, która w tym czasie miała 45 lat. W jej przypadku wykryty wiek jest równy 41,6 roku. Na podstawie wyników możemy wnioskować, że wykryty wiek jest jedynie przybliżonym wskaźnikiem, a nie bardzo dokładną informacją, na której można polegać: ID twarzy: wiek: płeć: ramka: 77e0536f-073d-41c5-920d-c53264d17b98 54.9 male {165,86,72,72} ID twarzy: wiek: płeć: ramka: afb22044-14fa-46bf-9b65-16d4fe1d9817 41.6 female {321,151,59,59} W przypadku niepowodzenia wywołania interfejsu API zostanie zwrócony komunikat o błędzie (ze statusem HTTP 400). Metoda parse_detect_response() deserializuje odpowiedź błędu i wyświetla komunikat w konsoli. Na przykład w przypadku użycia niewłaściwego klucza API z serwera zostanie zwrócony następujący komunikat i zostanie wyświetlony w konsoli: Unspecified Access denied due to invalid subscription key. Make sure you are subscribed to an API you are trying to call and provide the right key. (Odmowa dostępu z powodu nieprawidłowego klucza subskrypcji. Upewnij się, że subskrybujesz interfejs API, z którym chcesz się połączyć, a następnie podaj odpowiedni klucz). 276 ecb84badecb8c394873734f1e9bfb90f e Bibliografia Artykuły 1337C0D3R, Longest Palindromic Substring Part I, 2011, https://articles.leetcode.com/longest-palindromic-substring-part-i/. Aditya Goel, Permutations of a given string using STL, 2016, https://www.geeksforgeeks.org/permutations-of-a-given-string-using-stl/. Andrei Jakab, Using libcurl with SSH support in Visual Studio 2010, 2010, https://curl.haxx.se/libcurl/c/Using-libcurl-with-SSH-support-in-Visual-Studio2010.pdf. Ashwani Gautam, What is the analysis of quick sort?, 2017, https://www.quora.com/What-is-the-analysis-of-quick-sort. Ashwin Nanjappa, How to build Boost using Visual Studio, 2014, https://codeyarns.com/2014/06/06/how-to-build-boost-using-visual-studio/. busycrack, Telnet IMAP Commands Note, 2012, https://busylog.net/telnet-imap-commands-note/. Dan Madden, Encrypting Log Files, 2000, https://www.codeproject.com/Articles/644/Encrypting-Log-Files. Georgy Gimel’farb, Algorithm Quicksort: Analysis of Complexity, 2016, https://www.cs.auckland.ac.nz/courses/compsci220s1c/lectures/2016S1C/ CS220-Lecture10.pdf. Jay Doshi, Chanchal Khemani, Juhi Duseja, Dijkstra’s Algorithm, http://codersmaze.com/data-structure-explanations/graphs-data-structure/ dijkstras-algorithm-for-shortest-path/. Jeffrey Walton, Applied Crypto++: Block Ciphers, 2008, https://www.codeproject.com/Articles/21877/Applied-Crypto-Block-Ciphers. ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Jeffrey Walton, Compiling and Integrating Crypto++ into the Microsoft Visual C++ Environment, 2006, https://www.codeguru.com/cpp/v-s/ devstudio_macros/openfaq/article.php/c12853/Compiling-and-Integrating-Cryptointo-the-Microsoft-Visual-C-Environment.htm. Jeffrey Walton, Product Keys Based on the Advanced Encryption Standard(AES), 2007, https://www.codeproject.com/Articles/16465/Product-Keys-Based-on-theAdvanced-Encryption-Stan. Jonathan Boccara, How to split a string in C++, 2017, https://www.fluentcpp.com/2017/04/21/how-to-split-a-string-in-c/. Kenny Kerr, Resource Management in the Windows API, 2013, https://visualstudiomagazine.com/articles/2013/09/01/get-a-handle-on-thewindows-api.aspx. Kenny Kerr, Windows with C++ - C++ and the Windows API, 2011, https://msdn.microsoft.com/magazine/hh288076. Marius Bancila, Integrate Windows Azure Face APIs in a C++ application, 2015, https://www.codeproject.com/Articles/989752/Integrate-Windows-Azure-FaceAPIs-in-a-Cplusplus-a. Marius Bancila, Using Cognitive Services to find your Game of Thrones look-alike, 2018, https://www.codeproject.com/Articles/1234217/Using-Cognitive-Services-tofind-your-Game-of-Thro. Mary K. Vernon, Priority Queues, http://pages.cs.wisc.edu/~vernon/cs367/notes/11.PRIORITY-Q.html. Mathias Bynens, In search of the perfect URL validation regex, https://mathiasbynens.be/demo/url-regex. O. S. Tezer, SQLite vs MySQL vs PostgreSQL: A Comparison Of Relational Database Management Systems, 2014, https://www.digitalocean.com/community/tutorials/sqlite-vs-mysql-vs-postgresqla-comparison-of-relational-database-management-systems. Robert Nystrom, Game Programming patterns: Double Buffer, 2014, http://gameprogrammingpatterns.com/double-buffer.html. Robert Sedgewick, Philippe Flajolet, Introduction to the Analysis of Algorithms, 2013, http://www.informit.com/articles/article.asp?Xp=2017754&seqNum=5. Rosso Salmanzadeh, Using libcurl in Visual Studio, 2002, https://curl.haxx.se/libcurl/c/visual_studio.pdf. Sergii Bratus, Implementation of the Licensing System for a Software Product, 2010, https://www.codeproject.com/Articles/99499/Implementation-of-the-LicensingSystem-for-a-Softw. Shubham Agrawal, Dijkstra’s Shortest Path Algorithm using priority_queue of STL, 2016, https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-usingpriority_queue-stl/. 278 ecb84badecb8c394873734f1e9bfb90f e Bibliografia Travis Tidwell, An Online RSA Public and Private Key Generator, 2013, http://travistidwell.com/blog/2013/09/06/an-online-rsa-public-and-private-keygenerator/. Victor Volkman, Crypto++ Holds the Key to Encrypting Your C++ Application Data, 2006, https://www.codeguru.com/cpp/misc/misc/cryptoapi/article.php/c11953/ Cryptosupregsup-Holds-the-Key-to-Encrypting-Your-C-Application-Data.htm. Yang Song, Split a string using C++, 2014, https://web.archive.org/web/ 20180429050756/http://ysonggit.github.io/coding/2014/12/16/split-a-string-using-c.html. Decorator Design Pattern, https://sourcemaking.com/design_patterns/decorator. Composite Design Pattern, https://sourcemaking.com/design_patterns/composite. Template Method Design Pattern, https://sourcemaking.com/design_patterns/template_method. Strategy Design Pattern, https://sourcemaking.com/design_patterns/strategy. Chain of Responsibility, https://sourcemaking.com/design_patterns/chain_of_responsibility. Understanding the PDF File Format: Overview, https://blog.idrsolutions.com/2013/01/understanding-the-pdf-file-format-overview/. RSA Signing is Not RSA Decryption, https://www.cs.cornell.edu/courses/cs5430/2015sp/notes/rsa_sign_vs_dec.php. RSA Cryptography, https://www.cryptopp.com/wiki/RSA_Cryptography. Using rand() (C/C++), http://eternallyconfuzzled.com/arts/jsw_art_rand.aspx. Crypto++ Keys and Formats, https://www.cryptopp.com/wiki/Keys_and_Formats. INTERNET MESSAGE ACCESS PROTOCOL — VERSION 4 rev1, https://tools.ietf.org/html/rfc3501.html. Internal Versus External BLOBs in SQLite, https://www.sqlite.org/intern-v-extern-blob.html. OpenSSL Compilation and Installation, https://wiki.openssl.org/index.php/Compilation_and_Installation. 279 ecb84badecb8c394873734f1e9bfb90f e Nowoczesny C++. Zbiór praktycznych zadań dla przyszłych ekspertów Dokumentacja bibliotek C/C++ JSON parser/generator benchmark, https://github.com/miloyip/nativejson-benchmark. Crypto++, https://www.cryptopp.com/wiki/Main_Page. Hummus PDF, http://pdfhummus.com/How-To. JSON for Modern C++, https://github.com/nlohmann/json. PDF-Writer, https://github.com/galkahana/PDF-Writer. PNGWriter, https://github.com/pngwriter/pngwriter. pugixml 1.9 quick start guide, https://pugixml.org/docs/quickstart.html. SQLite, https://www.sqlite.org/docs.html. sqlite_modern_cpp, https://github.com/SqliteModernCpp/sqlite_modern_cpp. Ziplib wiki, https://bitbucket.org/wbenny/ziplib/wiki/Home. 280 ecb84badecb8c394873734f1e9bfb90f e Skorowidz deserializacja danych, 178 deszyfrowanie plików, 226 dodawanie zakresu wartości, 36, 43 dzień tygodnia, 79, 83 A adres IP, 247, 249 adres IPv4, 35, 37 algorytm, 89 przekształcania współbieżnego, 127, 129 scalania, 92, 111 sortowania, 92, 113 sortowania współbieżnego, 128, 134 tworzenia par, 91, 110 wyboru, 92, 112 wyszukiwania współbieżnego, 127–132 archiwum ZIP, 187, 191, 196 ASCII, 231 F filtrowanie listy, 91, 105 format JSON, 170, 177, 178 funkcje języka, 35 G generator kodów kreskowych, 189, 202 generowanie losowych haseł, 141, 147 numerów, 141, 151 permutacji ciągu znaków, 91, 107 gra Fizz-Buzz, 247, 250 w życie, 93, 122 B baza danych, 187 SQLite, 189, 207, 212, 216 biblioteka Crypto++, 236 filesystem, 73 bufor cykliczny, 90, 97 H C histogram tekstu, 90, 103 ciąg Collatza, 31 czas wykonania funkcji, 79, 81 czynniki pierwsze liczby, 27 I iteratory, 35 D J data i czas, 79 dekodowanie base64, 225, 231 język XPath, 170, 175 ecb84badecb8c394873734f1e9bfb90f e Skorowidz obraz PNG, 188, 199 obrazy, 187, 198 obsługa klienta, 137 multimediów, 190, 216 odczytywanie informacji, 189, 207 K kalendarz miesięczny, 80, 86 kod Graya, 28 kodowanie base64, 225, 231 kolejka priorytetowa, 89, 95 komunikaty, 136 kontener, 36, 43 typu wektorowego, 142, 158 kryptografia, 225 kursy wymiany bitcoinów, 248, 255 P pakowanie plików, 188, 192, 196 palindrom, 54, 61 pliki deszyfrowanie, 226, 240 logów, 68, 76 PDF, 171, 180, 183 PNG, 188, 198 podpisywanie, 226, 242 szyfrowanie, 226, 240 wyznaczanie skrótów, 226, 239 XML, 169 pobieranie danych, 170, 175 serializacja danych, 169, 172 podpisywanie plików, 226, 242 podwójne buforowanie, 90, 100 pomiar czasu, 79, 81 program Weasel, 93, 120 protokół IMAP, 248, 258 przekształcanie listy, 91, 106 współbieżne, 129 L liczba dni, 82 pi, 32 liczby Armstronga, 26 obfite, 24 pierwsze szóstkowe, 24 rzymskie, 29 zaprzyjaźnione, 25 lista procesów, 67, 70 Ł łańcuchy, 53 dzielenie, 60 dzielenie na tokeny, 54 elementy adresu URL, 55, 64 łączenie, 54, 59 podciąg palindromiczny, 54, 61 przekształcanie dat, 55, 65 sprawdzanie tablic rejestracyjnych, 54, 63 S serializacja danych, 169, 170, 172, 177 skróty dla plików, 226 sortowanie szybkie, 113 współbieżne, 128, 134 sprawdzanie numerów ISBN, 33 poprawności haseł, 141, 144 strefy czasowe, 80, 84 struktury danych, 89 strumienie, 67 system obsługi klienta, 128, 137 zatwierdzania, 142, 155 systemy plików, 67 szyfr Cezara, 225, 227 Vigenère’a, 225, 228 N najdłuższy ciąg Collatza, 31 najkrótsza ścieżka, 92, 116 najmniejsza wspólna wielokrotność, 22 największa liczba pierwsza, 23 największy wspólny dzielnik, 21 numer dnia, 79, 83 numery ISBN, 33 O obliczanie ceny, 143, 163 rozmiaru katalogu, 68, 73 282 ecb84badecb8c394873734f1e9bfb90f e Skorowidz szyfrowanie PKWare, 196 plików, 226 W wektor, 142, 158 wiadomości e-mail, 248, 258 wielkie litery, 54, 58 współbieżność, 127 wstawianie informacji, 189, 212 wykrywanie twarzy, 248, 267 wyliczanie zakresu, 38 wyrażenia regularne, 53, 68, 75 wyszukiwanie plików, 68, 75, 187, 191 współbieżne, 130, 132 wyświetlanie komunikatów, 128, 136 skal temperatur, 36, 49 wyznaczanie minimum, 36, 42 wzorce projektowe, 141 Ś średnia ocena filmów, 91, 109 T tablice, 35 dwuwymiarowe, 40 tłumaczenie tekstu, 248, 263 tokeny, 54 trójkąt Pascala, 67, 69 tworzenie pliku PDF, 171, 180, 183 pliku PNG, 188, 198 tymczasowe pliki logów, 68, 76 typ danych IPv4, 35, 37 long long, 21 Z zadania matematyczne, 19 zakres adresów IPv4, 38 zamiana typu binarnego, 53, 56 łańcuchowego, 53, 57 zbiór danych, 90, 102 ZIP, 187, 191, 196 znajdowanie adresu IP, 249 U uchwyt systemu operacyjnego, 36, 45 usuwanie plików, 68, 73 pustych wierszy, 68, 72 uwierzytelnianie użytkowników, 226, 236 283 ecb84badecb8c394873734f1e9bfb90f e ecb84badecb8c394873734f1e9bfb90f e ecb84badecb8c394873734f1e9bfb90f e