Uploaded by aniolm

Bancila Marius - Nowoczesny C++ - Zbiór praktycznych zadań dla przyszłych ekspertów

advertisement
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 nossl3 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/cognitiveservices/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
Download