1/90 PB161 Programování v jazyce C++ Petr Ročkai Část A: Pravidla a organizace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Část 1: Hodnoty a funkce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Část 2: Složené hodnoty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Část 3: Metody a operátory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Část 4: Životní cyklus hodnot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 Část S.1: Funkce a hodnoty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Část 5: Ukazatele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Část 6: Dědičnost a pozdní vazba . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 Část 7: Výjimky a princip RAII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 Část S.2: Ukazatele, výjimky, OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Část 8: Součtové typy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Část 9: Knihovna algoritmů . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 Část 10: Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 Část 11: Vstup a výstup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Část S.3: Součtové typy, řetězce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 Část K: Vzorová řešení . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Část T: Technické informace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 Část U: Doporučení k zápisu kódu . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Část A: Pravidla a organizace Tento dokument je sbírkou cvičení a komentovaných příkladů zdrojového kódu. Každá kapitola odpovídá jednomu týdnu semestru a tedy jednomu cvičení. Cvičení v prvním týdnu semestru („nulté“) je určeno k seznámení se s výukovým prostředím, studijními materiály a základními nástroji ekosystému. Každá část sbírky (zejména tedy všechny ukázky a příklady) jsou také k dispozici jako samostatné soubory, které můžete upravovat a spouštět. Této rozdělené verzi sbírky říkáme zdrojový balík. Aktuální verzi1 (ve všech variantách) můžete získat dvěma způsoby: 1. Ve studijních materiálech2 předmětu v ISu – soubory PDF ve složce text, zdrojový balík ve složkách 00 (organizační informace), 01 až 12 (jednotlivé kapitoly = týdny semestru), dále s1 až s3 (sady úloh) a konečně ve složce sol vzorová řešení. Doporučujeme soubory stahovat dávkově pomocí volby „stáhnout jako ZIP“. 2. Po přihlášení na studentský server aisa (buď za pomoci ssh nebo putty) zadáním příkazu pb161 update. Všechny výše uvedené složky pak naleznete ve složce ~/pb161. Tato kapitola (složka) dále obsahuje závazná pravidla a organizační pokyny. Než budete pokračovat, pozorně si je prosím přečtěte. Pro komunikaci s organizátory kurzu slouží diskusní fórum v ISu (více informací naleznete v části T.1). Nepište prosím organizátorům ani cvičícím maily ohledně předmětu, nejste-li k tomu specificky vyzváni. S žádostmi o výjimky ze studijních povinností, omluvenkami, atp., se obracejte vždy na studijní oddělení. 1 Některé příklady a ukázky jsou prozatím v anglickém jazyce, nebo zcela chybí. Tyto budeme překládat a doplňovat průběžně. 2 https://is.muni.cz/auth/el/fi/jaro2024/PB161/um/ A.1: Přehled Tento předmět sestává z cvičení, sad domácích úloh a závěrečného praktického testu (kolokvia). Protože se jedná o „programovací“ předmět, většina práce v předmětu – a tedy i jeho hodnocení – se bude zaměřovat na praktické programování. Je důležité, abyste programovali co možná nejvíce, ideálně každý den, ale minimálně několikrát každý týden. K tomu Vám budou sloužit příklady v této sbírce (typicky se bude jednat o velmi malé programy v rozsahu jednotek až desítek řádků, kterých byste měli být v průměru schopni vyřešit několik za hodinu) a domácí úlohy, kterých budou za semestr 3 sady, a budou znatelně většího rozsahu (maximálně malé stovky řádků). V obou případech bude v průběhu semestru stoupat náročnost – je tedy důležité, abyste drželi krok a práci neodkládali na poslední chvíli. Protože programování je těžké, bude i tento kurz těžký – je zcela nezbytné vložit do něj odpovídající úsilí. Doufáme, že kurz úspěšně absolvujete, a co je důležitější, že se v něm toho naučíte co nejvíce. Je ale nutno podotknout, že i přes svou náročnost je tento kurz jen malým krokem na dlouhé cestě. A.1.1 Probíraná témata Předmět je rozdělen do 4 bloků (čtvrtý blok patří do zkouškového období). Do každého bloku v semestru patří 4 kapitoly (témata) a jim odpovídající 4 cvičení. bl. téma 1 1. funkce, jednoduché hodnoty, reference 2. složené hodnoty 3. vlastní metody a operátory 4. životní cyklus hodnot, vlastnictví 2 5. ukazatele 6. dědičnost, pozdní vazba 7. výjimky, princip RAII 8. lexikální uzávěry, další operátory 3 9. součtové typy 10. knihovna algoritmů 11. řetězce 12. vstup a výstup – 13. bonusy, opakování A.1.2 Organizace sbírky V následujících sekcích naleznete detailnější informace a závazná pravidla kurzu: doporučujeme Vám, abyste se s nimi důkladně seznámili.3 Zbytek sbírky je pak rozdělen na části, které odpovídají jednotlivým týdnům semestru. Důležité: během prvního týdne semestru už budete řešit přípravy z první kapitoly, přestože první cvičení je ve až v týdnu druhém. Nulté cvičení je volitelné a není nijak hodnoceno. Kapitoly jsou číslovány podle témat z předchozí tabulky: ve druhém týdnu semestru se tedy ve cvičení budeme zabývat tématy, ke kterým jste v prvním týdnu vypracovali a odevzdali přípravy. 3 Pravidla jsou velmi podobná těm v kurzu IB111, ale přesto si je pozorně přečtěte. 2/90 PB161 Programování v jazyce C++, 16. února 2024 A.1.3 Plán semestru Tento kurz vyžaduje značnou aktivitu během semestru. V této sekci naleznete přehled důležitých událostí formou kalendáře. Jednotlivé události jsou značeny takto (bližší informace ke každé naleznete v následujících odstavcích tohoto úvodu): • „#X“ – číslo týdne v semestru, • „cv0“ – tento týden běží „nulté“ cvičení (kapitola B), • „cv1“ – tento týden probíhají cvičení ke kapitole 1, • „X/v“ – mezivýsledek verity testů příprav ke kapitole X, • „X/p“ – poslední termín odevzdání příprav ke kapitole X, • „sX/Y“ – Yté kolo verity testů k sadě X, • „sX/z1“ – první kolo známek za kvalitu kódu sady X, • „sX/op“ – termín pro opravná odevzdání sady X, • „sX/z2“ – finální známky za kvalitu kódu sady X, • „test“ – termín programovacího testu. Nejdůležitější události jsou zvýrazněny: termíny odevzdání příprav a poslední termín odevzdání úloh ze sad (obojí vždy o 23:59 uvedeného dne). únor Po Út St Čt Pá So Ne #1 19 20 21 22 23 24 25 cv 0 01/v 01/p #2 26 27 28 29 cv 1 s1/1 s1/2 02/v březen Po Út St Čt Pá So Ne #2 1 2 3 s1/3 02/p #3 4 5 6 7 8 9 10 cv 2 s1/4 s1/5 03/v s1/6 03/p #4 11 12 13 14 15 16 17 cv 3 s1/7 s1/8 04/v s1/9 04/p #5 18 19 20 21 22 23 24 cv 4 s1/10 s1/11 05/v s1/12 05/p #6 25 26 27 28 29 30 31 cv 5 s2/1 s2/2 06/v s2/3 06/p duben Po Út St Čt Pá So Ne #7 1 2 3 4 5 6 7 cv 6 s2/4 s1/z1 s2/5 07/v s2/6 07/p #8 8 9 10 11 12 13 14 cv 7 s2/7 s1/op s2/8 08/v s2/9 08/p #9 15 16 17 18 19 20 21 cv 8 s2/10 s1/z2 s2/11 09/v s2/12 09/p #10 22 23 24 25 26 27 28 cv 9 s3/1 s3/2 10/v s3/3 10/p #11 29 30 cv10 s3/4 s2/z1 květen Po Út St Čt Pá So Ne #11 1 sv 2 3 4 5 s3/5 11/v s3/6 11/p #12 6 7 8 sv 9 10 11 12 cv11 s3/7 s2/op s3/8 12/v s3/9 12/p #13 13 14 15 16 17 18 19 cv12 s3/10 s2/z2 s3/11 s3/12 20 21 22 23 24 25 26 27 28 29 30 31 s3/z1 červen Po Út St Čt Pá So Ne 1 2 3 4 5 6 7 8 9 s3/op 10 11 12 13 14 15 16 s3/z2 test 17 18 19 20 21 22 23 test test 24 25 26 27 28 29 30 test test A.2: Hodnocení Abyste předmět úspěšně ukončili, musíte v každém bloku4 získat 60 bodů. Žádné další požadavky nemáme. Výsledná známka závisí na celkovém součtu bodů (splníte-li potřebných 4×60 bodů, automaticky získáte známku alespoň E). Hodnota ve sloupci „předběžné minimum“ danou známku zaručuje – na konci semestru se hranice ještě mohou posunout směrem dolů tak, aby výsledná stupnice přibližně odpovídala očekávané distribuci dle ECTS.5 známka předběžné minimum po vyhodnocení semestru A 420 90. percentil + 75 B 360 65. percentil + 75 C 310 35. percentil + 75 D 270 10. percentil + 75 E 240 240 Body lze získat mnoha různými způsoby (přesnější podmínky naleznete v následujících sekcích této kapitoly). V blocích 1-3 (probíhají během semestru) jsou to: • za každou úspěšně odevzdanou přípravu 1 bod (max. 6 bodů každý týden, nebo 24/blok), • za každou přípravu, která projde „verity“ testy navíc 0,5 bodu (max. 3 body každý týden, nebo 12/blok), • za účast6 na cvičení získáte 3 body (max. tedy 12/blok), • za aktivitu ve cvičení 3 body (max. tedy 12/blok). Za přípravy a cvičení lze tedy získat teoretické maximum 60 bodů. Dále můžete získat: • 10 bodů za úspěšně vyřešený příklad ze sady domácích úloh (celkem vždy 60/blok). V blocích 2-4 navíc můžete získat body za kvalitu řešení příkladů ze sady 4 Máte-li předmět ukončen zápočtem, čtvrtý blok a tedy ani závěrečný test pro Vás není relevantní. Platí požadavek na 3×60 bodů z bloků v semestru. 5 Percentil budeme počítat z bodů v semestru (první tři bloky) a bude brát do úvahy všechny studenty, bez ohledu na ukončení, kteří splnili tyto tři bloky (tzn. mají potřebné minimum 3×60 bodů). 6 V případě, že jste řádně omluveni v ISu, nebo Vaše cvičení odpadlo (např. padlo na státní svátek), můžete body za účast získat buď náhradou v jiné skupině (pro státní svátky dostanete instrukce mailem, individuální případy si domluvte s cvičícími obou dotčených skupin). Nemůžete-li účast nahradit takto, domluvte se se svým cvičícím (v tomto případě lze i mailem) na vypracování 3 rozšířených příkladů ze sbírky (přesné detaily Vám sdělí cvičící podle konkrétní situace). Neomluvenou neúčast lze nahrazovat pouze v jiné skupině a to max. 1–2× za semestr. 3/90 úloh předchozího bloku: • za kvalitu kódu max. 5 bodů za příklad (celkem 30/blok). Konečně blok 4, který patří do zkouškového období, nemá ani cvičení ani sadu domácích úloh. Krom bodů za kvalitu kódu ze třetí sady lze získat: • 15 bodů za každý zkouškový příklad (celkem 90/blok). Celkově tedy potřebujete: • blok 1: 60/120 bodů, • blok 2: 60/150 bodů, • blok 3: 60/150 bodů, • blok 4: 60/120 bodů (neplatí pro ukončení zápočtem). A.3: Přípravy Jak již bylo zmíněno, chcete-li se naučit programovat, musíte programování věnovat nemalé množství času, a navíc musí být tento čas rozložen do delších období – semestr nelze v žádném případě doběhnout tím, že budete týden programovat 12 hodin denně, i když to možná pokryje potřebný počet hodin. Proto od Vás budeme chtít, abyste každý týden odevzdali několik vyřešených příkladů z této sbírky. Tento požadavek má ještě jeden důvod: chceme, abyste vždy v době cvičení už měli látku každý samostatně nastudovanou, abychom mohli řešit zajímavé problémy, nikoliv opakovat základní pojmy. Také Vás prosíme, abyste příklady, které plánujete odevzdat, řešili vždy samostatně: případnou zakázanou spolupráci budeme trestat (viz také konec této kapitoly). A.3.1 Odevzdání Každý příklad obsahuje základní sadu testů. To, že Vám tyto testy prochází, je jediné kritérium pro zisk základních bodů za odevzdání příprav. Poté, co příklady odevzdáte, budou tytéž testy na Vašem řešení automaticky spuštěny, a jejich výsledek Vám bude zapsán do poznámkového bloku. Smyslem tohoto opatření je zamezit případům, kdy omylem odevzdáte nesprávné, nebo jinak nevyhovující řešení, aniž byste o tom věděli. Velmi silně Vám proto doporučujeme odevzdávat s určitým předstihem, abyste případné nesrovnalosti měli ještě čas vyřešit. Krom základních („sanity“) testů pak ve čtvrtek o 23:59 a znovu v sobotu o 23:59 (těsně po konci odevzdávání) spustíme rozšířenou sadu testů („verity“). Za každý odevzdaný příklad, který splnil základní („sanity“) testy získáváte jeden bod. Za příklad, který navíc splnil rozšířené testy získáte dalšího 0,5 bodu (tzn. celkem 1,5 bodu). Výsledky testů naleznete v poznámkovém bloku v informačním systému. Příklady můžete odevzdávat: 1. do odevzdávárny s názvem NN v ISu (např. 01), 2. příkazem pb161 submit ve složce ~/pb161/NN. Podrobnější instrukce naleznete v kapitole T (technické informace, soubory 00/t*). Termíny pro odevzdání příprav k jednotlivým kapitolám jsou shrnuty v přehledovém kalendáři v části A.1 takto: • „01/v“ – předběžné (čtvrteční) verity testy pro příklady z první kapi- toly, • „01/p“ – poslední (sobotní) termín odevzdání příprav z 1. kapitoly, • analogicky pro další kapitoly. A.4: Cvičení Těžiště tohoto předmětu je jednoznačně v samostatné domácí práci – učit se programovat znamená zejména hodně programovat. Společná cvičení sice nemohou tuto práci nahradit, mohou Vám ale přesto v lecčem pomoct. Smyslem cvičení je: 1. analyzovat problémy, na které jste při samostatné domácí práci narazili, a zejména prodiskutovat, jak je vyřešit, 2. řešit programátorské problémy společně (s cvičícím, ve dvojici, ve skupině) – nahlédnout jak o programech a programování uvažují ostatní a užitečné prvky si osvojit. Cvičení je rozděleno na dva podobně dlouhé segmenty, které odpovídají těmto bodům. První část probíhá přibližně takto: • cvičící vybere ty z Vámi odevzdaných příprav, které se mu zdají něčím zajímavé – ať už v pozitivním, nebo negativním smyslu, ∘ řešení bude anonymně promítat na plátno a u každého otevře diskusi o tom, čím je zajímavé; ∘ Vaším úkolem je aktivně se do této diskuse zapojit (můžete se například ptát proč je daná věc dobře nebo špatně a jak by se udělala lépe, vyjádřit svůj názor, odpovídat na dotazy cvičícího), ∘ k promítnutému řešení se můžete přihlásit a ostatním přiblížit, proč je napsané tak jak je, nebo klidně i rozporovat případnou kritiku (není to ale vůbec nutné), • dále podobným způsobem vybere vzájemné (peer) recenze, které jste v předchozím týdnu psali, a stručně je s Vámi prodiskutuje (celkovou strukturu recenze, proč je který komentář dobrý nebo nikoliv, jestli nějaký komentář chybí, atp.) – opět se můžete (resp. byste se měli) zapojovat, • na Vaši žádost lze ve cvičení analogicky probrat neúšpěšná řešení příkladů (a to jak příprav, tak příkladů z uzavřených sad). Druhá část cvičení je variabilnější, ale bude se vždy točit kolem bodů za aktivitu (každý týden můžete za aktivitu získat maximálně 3 body). Ve čtvrtém, osmém a dvanáctém týdnu proběhnou „vnitrosemestrálky“ kde budete řešit samostatně jeden příklad ze sbírky, bez možnosti hledat na internetu – tak, jak to bude na závěrečném testu; každé úspěšné řešení (tzn. takové, které splní verity testy) získá ony 3 body za aktivitu pro daný týden. V ostatních týdnech budete ve druhém segmentu kombinovat různé aktivity, které budou postavené na příkladech typu r z aktuální kapitoly (které konkrétní příklady budete ve cvičení řešit vybere cvičící, může ale samozřejmě vzít v potaz Vaše preference): 1. Můžete se přihlásit k řešení příkladu na plátně, kdy primárně vymýšlíte řešení Vy, ale zbytek třídy Vám bude podle potřeby radit, nebo se ptát co/jak/proč se v řešení děje. U jednodušších příkladů se od Vás bude také očekávat, že jako součást řešení doplníte testy. 2. Cvičící Vám může zadat práci ve dvojicích – první dvojice, která se dopracuje k funkčnímu řešení získá možnost své řešení předvést zbytku třídy – vysvětlit jak a proč funguje, odpovědět na případné dotazy, opravit chyby, které v řešení publikum najde, atp. – a získat tak body za aktivitu. Získané 3 body budou rozděleny rovným dílem mezi vítězné řešitele. 3. příklad můžete také řešit společně jako skupina – takto vymyšlený kód bude zapisovat cvičící (body za aktivitu se v tomto případě neudělují). A.5: Sady domácích úloh Ke každému bloku patří sada 6 domácích úloh, které tvoří významnou část hodnocení předmětu. Na úspěšné odevzdání každé domácí úlohy budete mít 12 pokusů rozložených do 4 týdnů odpovídajícího bloku cvičení. Odevzdávání bude otevřeno vždy v 0:00 prvního dne bloku (tzn. 24h před prvním spuštěním verity testů). Termíny odevzdání (vyhodnocení verity testů) jsou vždy v pondělí, středu a pátek v 23:59 – vyznačeno jako s1/1–12, s2/1–12 a s3/1–12 v přehledovém kalendáři v části A.1. A.5.1 Odevzdávání Součástí každého zadání je jeden zdrojový soubor (kostra), do kterého své řešení vepíšete. Vypracované příklady lze pak odevzdávat stejně jako přípravy: 1. do odevzdávárny s názvem sN_úkol v ISu (např. s1_a_queens), 2. příkazem pb161 submit sN_úkol ve složce ~/pb161/sN, např. pb161 submit s1_a_queens. Podrobnější instrukce naleznete opět v kapitole T. A.5.2 Vyhodnocení Vyhodnocení Vašich řešení probíhá ve třech fázích, a s každou z nich je spjata sada automatických testů. Tyto sady jsou: • „syntax“ – kontroluje, že odevzdaný program je syntakticky správně, lze jej přeložit a prochází základními statickými kontrolami, 4/90 PB161 Programování v jazyce C++, 16. února 2024 • „sanity“ – kontroluje, že odevzdaný program se chová „rozumně“ na jednoduchých případech vstupu; tyto testy jsou rozsahem a stylem podobné těm, které máte přiložené k příkladům ve cvičení, • „verity“ – důkladně kontrolují správnost řešení, včetně složitých vstupů a okrajových případů a kontroly paměťových chyb. Fáze na sebe navazují v tom smyslu, že nesplníte-li testy v některé fázi, žádná další se už (pro dané odevzdání) nespustí. Pro splnění domácí úlohy je klíčová fáze „verity“, za kterou jsou Vám uděleny body. Časový plán vyhodnocení fází je následovný: • kontrola „syntax“ se provede obratem (do cca 5 minut od odevzdání), • kontrola „sanity“ každých 6 hodin počínaje půlnocí (tzn. 0:00, 6:00, 12:00, 18:00), • kontrola „verity“ se provede v pondělí, středu a pátek ve 23:59 (dle tabulky uvedené výše). Vyhodnoceno je vždy pouze nejnovější odevzdání, a každé odevzdání je vyhodnoceno v každé fázi nejvýše jednou. Výsledky naleznete v poznámkových blocích v ISu (každá úloha v samostatném bloku), případně je získáte příkazem pb161 status. A.5.3 Bodování Za každý domácí úkol, ve kterém Vaše odevzdání v příslušném termínu splní testy „verity“, získáte 10 bodů. Za stejný úkol máte dále možnost získat body za kvalitu kódu, a to vždy v hodnotě max. 5 bodů. Body za kvalitu se počítají v bloku, ve kterém byly uděleny, tzn. body za kvalitu ze sady 1 se započtou do bloku 2. Maximální bodový zisk za jednotlivé sady: • sada 1: 60 za funkčnost v bloku 1 + 30 za kvalitu v bloku 2, • sada 2: 60 za funkčnost v bloku 2 + 30 za kvalitu v bloku 3, • sada 3: 60 za funkčnost v bloku 3 + 30 za kvalitu v bloku 4 (zkouškovém). A.5.4 Hodnocení kvality kódu Automatické testy ověřují správnost vašich programů (do takové míry, jak je to praktické – ani nejpřísnější testy nemůžou zaručit, že máte program zcela správně). Správnost ale není jediné kritérium, podle kterého lze programy hodnotit: podobně důležité je, aby byl program čitelný. Programy totiž mimo jiné slouží ke komunikaci myšlenek lidem – dobře napsaný a správně okomentovaný kód by měl čtenáři sdělit, jaký řeší problém, jak toto řešení funguje a u obojího objasnit proč. Je Vám asi jasné, že čitelnost programu člověkem může hodnotit pouze člověk: proto si každý Váš úspěšně vyřešený domácí úkol přečte opravující a své postřehy Vám sdělí. Přitom zároveň Váš kód oznámkuje podle kritérií podrobněji rozepsaných v kapitole Z. Tato kritéria aplikujeme při známkování takto: • hodnocení A dostane takové řešení, které jasně popisuje řešení zadaného problému, je správně dekomponované na podproblémy, je zapsáno bez zbytečného opakování, a používá správné abstrakce, algoritmy a datové struktury, • hodnocení B dostane program, který má výrazné nedostatky v jedné, nebo nezanedbatelné nedostatky ve dvou oblastech výše zmíněných, například: ∘ je relativně dobře dekomponovaný a zbytečně se neopakuje, ale používá nevhodný algoritmus nebo datovou strukturu a není zapsán příliš přehledně, ∘ používá optimální algoritmus a datové struktury a je dobře dekomponovaný, ale lokálně opakuje tentýž kód s drobnými obměnami, a občas používá zavádějící nebo jinak nevhodná jména podprogramů, proměnných atp., ∘ jinak dobrý program, který používá zcela nevhodný algoritmus, nebo velmi špatně pojmenované proměnné, nebo je zapsaný na dvě obrazovky úplně bez dekompozice, • hodnocení X dostanou programy, u kterých jste se dobrovolně vzdali hodnocení (a to jasně formulovaným komentářem na začátku souboru, např. „Vzdávám se hodnocení.“), • hodnocení C dostanou všechny ostatní programy, zejména ty, které kombinují dvě a více výrazné chyby zmiňované výše. Známky Vám budou zapsány druhé úterý následujícího bloku. Dostanete-li známku B nebo C, budete mít možnost svoje řešení ještě zlepšit, odevzdat znovu, a známku si tak opravit: • na opravu budete mít týden, • na opraveném programu nesmí selhat verity testy, • testy budou nadále probíhat se stejnou kadencí jako během řádné doby k vypracování (pondělí, středa, pátek o 23:59). Bude-li opravující s vylepšeným programem spokojen, výslednou známku Vám upraví. Termíny, které se vážou k hodnocení kvality, jsou vždy v úterý a jsou vyznačené v přehledovém kalendáři v části A.1 takto: • „s1/z1“ – obdržíte známky za první sadu, • „s1/op“ – termín pro odevzdání opravených řešení 1. sady, • „s1/z2“ – výsledné známky za první sadu, • analogicky pro s2 a s3. Jednotlivé výsledné známky se promítnou do bodového hodnocení úkolu ná- sledovně: • známka A Vám vynese 5 bodů, • známka B pak 2 body, • známka X žádné body neskýtá, • známka C je hodnocena -1 bodem. Samotné body za funkcionalitu se při opravě kvality již nijak nemění. A.5.5 Neúspěšná řešení Příklady, které se Vám nepodaří vyřešit kompletně (tzn. tak, aby na nich uspěla kontrola „verity“) nebudeme hodnotit. Nicméně může nastat situace, kdy byste potřebovali na „téměř hotové“ řešení zpětnou vazbu, např. proto, že se Vám nepodařilo zjistit, proč nefunguje. Taková řešení můžou být předmětem společné analýzy ve cvičení, v podobném duchu jako probíhá rozprava kolem odevzdaných příprav (samozřejmě až poté, co pro danou sadu skončí odevzdávání). Máte-li zájem takto rozebrat své řešení, domluvte se, ideálně s předstihem, se svým cvičícím. To, že jste autorem, zůstává mezi cvičícím a Vámi – Vaši spolužáci to nemusí vědět (ke kódu se samozřejmě můžete v rámci debaty přihlásit, uznáte-li to za vhodné). Stejná pravidla platí také pro nedořešené přípravy (musíte je ale odevzdat). Tento mechanismus je omezen prostorem ve cvičení – nemůžeme zaručit, že v případě velkého zájmu dojde na všechny (v takovém případě cvičící vybere ta řešení, která bude považovat za přínosnější pro skupinu – je tedy možné, že i když se na Vaše konkrétní řešení nedostane, budete ve cvičení analyzovat podobný problém v řešení někoho jiného). A.6: Vzájemné recenze Jednou z možností, jak získat body za aktivitu, jsou vzájemné (peer) recenze. Smyslem této aktivity je získat praxi ve čtení a hodnocení cizího kódu. Možnost psát tyto recenze se váže na vlastní úspěšné vypracování téhož příkladu. Příklad: odevzdáte-li ve druhém týdnu 4 přípravy, z toho u třech splníte testy „verity“ (řekněme p1, p2, p5), ve třetím týdnu dostanete po jednom řešení těchto příkladů (tzn. budete mít možnost recenzovat po jedné instanci 02/p1, 02/p2 a 02/p5). Termín pro odevzdání recenzí na přípravy z druhé kapitoly je shodný s termínem pro odevzdání příprav třetí kapitoly (tzn. sobotní půlnoc). Vypracování těchto recenzí je dobrovolné. Za každou vypracovanou recenzi získáte jeden bod za aktivitu, počítaný v týdnu, kdy jste recenze psali (v uvedeném příkladu by to tedy bylo ve třetím týdnu semestru, tedy do stejné „kolonky“ jako body za příklady 02/r). Udělení bodů je podmíněno smysluplným obsahem – nestačí napsat „nemám co dodat“ nebo „není zde co komentovat“. Je-li řešení dobré, napište proč je dobré (viz též níže). Vámi odevzdané recenze si přečte Váš cvičící a některé z nich může vybrat k diskusi ve cvičení (v dalším týdnu), v podobném duchu jako přípravy samotné. Pozor, v jednom týdnu lze získat maximálně 3 body za aktivitu, bez ohledu na jejich zdroj (recenze, vypracování příkladu u tabule, atp.). Toto omezení není dotčeno ani v případě, kdy dostanete k vypracování více než 3 příklady (můžete si ale vybrat, které z nich chcete recenzovat). 5/90 A.6.1 Jak recenze psát Jak recenze vyzvednout a odevzdat je blíže popsáno v kapitole T. Své komentáře vkládejte přímo do vyzvednutých zdrojových souborů. Komentáře můžete psát česky (slovensky) nebo anglicky, volba je na Vás. Komentáře by měly být stručné, ale užitečné – Vaším hlavním cílem by mělo být pomoct adresátovi naučit se lépe programovat. Snažte se aplikovat kritéria a doporučení z předchozí sekce (nejlépe na ně přímo odkázat, např. „tuto proměnnou by šlo jistě pojmenovat lépe (viz doporučení 2.b)“). Nebojte se ani vyzvednout pozitiva (můžete zde také odkázat doporučení, máte-li například za to, že je obzvlášť pěkně uplatněné) nebo poznamenat, když jste se při čtení kódu sami něco naučili. Komentáře vkládejte vždy před komentovaný celek, a držte se podle možnosti tohoto vzoru (použití ** pomáhá odlišit původní komentáře autora od poznámek recenzenta): /** A short, one-line remark. **/ U víceřádkových komentářů: /** A longer comment, which should be wrapped to 80 columns or ** less, and where each line should start with the ** marker. ** It is okay to end the comment on the last line of text like ** this. **/ Při vkládání komentářů neměňte existující řádky (zejména se ujistěte, že máte vypnuté automatické formátování, editujete-li zdrojový kód v nějakém IDE). Jediné povolená operace jsou: • vložení nových řádků (prázdných nebo s komentářem), nebo • doplnění komentáře na stávající prázdný řádek. A.7: Závěrečný programovací test Zkouškové období tvoří pomyslný 4. blok a platí zde stejné kritérium jako pro všechny ostatní bloky: musíte získat alespoň 60 bodů. Závěrečný test: • proběhne v počítačové učebně bez přístupu k internetu nebo vlastním materiálům, • k dispozici bude tato sbírka (bez vzorových řešení příkladů typu e a r), skripta PB152 a manuálové stránky (příkaz man), • budete moct používat textový editor nebo vývojové prostředí VS Code, překladač jazyka C (gcc a/nebo clang) a nástroje valgrind a gdb. • proběhne v počítačové učebně bez přístupu k internetu nebo vlastním materiálům, • k dispozici budou oficiální studijní materiály: ∘ tato sbírka (bez vzorových řešení příkladů typu e a r) a ∘ offline kopie příručky cppreference (bez fulltextového vyhledávání), • budete moct používat textový editor nebo vývojové prostředí VS Code, překladače g++ a clang, nástroj clang-tidy a nástroje valgrind a gdb. Na vypracování praktické části budete mít 4 hodiny čistého času, a bude sestávat ze šesti příkladů, které budou hodnoceny automatickými testy, s maximálním ziskem 90 bodů. Příklady jsou hodnoceny binárně (tzn. příklad je uznán za plný počet bodů, nebo uznán není). Kvalita kódu hodnocena nebude, ani nebudeme řešení kontrolovat nástrojem clang-tidy. Příklady budou na stejné úrovni obtížnosti jako příklady typu p/r/v ze sbírky. Během zkoušky můžete kdykoliv odevzdat (na počet odevzdání není žádný konkrétní limit) a vždy dostanete zpět výsledek testů syntaxe a sanity. Součástí zadání bude navíc soubor tokens.txt, kde naleznete 4 kódy. Každý z nich lze použít nejvýše jednou (vložením do komentáře do jednoho z příkladů), a každé použití kódu odhalí výsledek verity testu pro ten soubor, do kterého byl vložen. Toto se projeví pouze při prvním odevzdání s vloženým kódem, v dalších odevzdáních bude tento kód ignorován (bez ohledu na soubor, do kterého bude vložen). Zkouška proběhne až po vyhodnocení recenzí za třetí blok (tzn. ve druhé polovině zkouškového období). Plánované termíny7 jsou tyto (žádné další vypsané nebudou): • čtvrtek 13.6. 9:00–13:00, 14:00–18:00, • čtvrtek 20.6. 9:00–13:00, 14:00–18:00, • čtvrtek 27.6. 9:00–13:00, 14:00–18:00. A.7.1 Vnitrosemestrálky V posledním týdnu každého bloku, tedy • cvičení 4 (18.-22. března), • cvičení 8 (15.-19. dubna), • cvičení 12 (13.-17. května), proběhne v rámci cvičení programovací test na 40 minut. Tyto testy budou probíhat za stejných podmínek, jako výše popsaný závěrečný test (slouží tedy mimo jiné jako příprava na něj). Řešit budete vždy ale pouze jeden příklad, za který můžete získat 3 body, které se počítají jako body za aktivitu v tomto cvičení. A.8: Opisování Na všech zadaných problémech pracujte prosím zcela samostatně – toto se týká jak příkladů ze sbírky, které budete odevzdávat, tak domácích úloh ze sad. To samozřejmě neznamená, že Vám zakazujeme společně studovat a vzájemně si pomáhat látku pochopit: k tomuto účelu můžete využít všechny zbývající příklady ve sbírce (tedy ty, které nebude ani jeden z Vás odevzdávat), a samozřejmě nepřeberné množství příkladů a cvičení, které jsou 7 Může se stát, že termíny budeme z technických nebo organizačních důvodů posunout na jiný den nebo hodinu. V takovém případě Vám samozřejmě změnu s dostatečným předstihem oznámíme. k dispozici online. Příklady, které odevzdáváte, slouží ke kontrole, že látce skutečně rozumíte, a že dokážete nastudované principy prakticky aplikovat. Tato kontrola je pro Váš pokrok naprosto klíčová – je velice snadné získat pasivním studiem (čtením, posloucháním přednášek, studiem již vypracovaných příkladů) pocit, že něčemu rozumíte. Dokud ale sami nenapíšete na dané téma několik programů, jedná se pravděpodobně skutečně pouze o pocit. Abyste nebyli ve zbytečném pokušení kontroly obcházet, nedovolenou spolupráci budeme relativně přísně trestat. Za každý prohřešek Vám bude strženo v každé instanci (jeden týden příprav se počítá jako jedna instance, příklady ze sad se počítají každý samostatně): • 1/2 bodů získaných (ze všech příprav v dotčeném týdnu, nebo za jednotlivý příklad ze sady), • 10 bodů z hodnocení bloku, do kterého opsaný příklad patří, • 10 bodů (navíc k předchozím 10) z celkového hodnocení. Opíšete-li tedy například 2 přípravy ve druhém týdnu a: • Váš celkový zisk za přípravy v tomto týdnu je 4,5 bodu, • Váš celkový zisk za první blok je 65 bodů, jste automaticky hodnoceni známkou X (65 - 2,25 - 10 je méně než potřebných 60 bodů). Podobně s příkladem z první sady (65 - 5 - 10), atd. Máte-li v bloku bodů dostatek (např. 80 - 5 - 10 > 60), ve studiu předmětu pokračujete, ale započte se Vám ještě navíc penalizace 10 bodů do celkové známky. Přestává pro Vás proto platit pravidlo, že 4 splněné bloky jsou automaticky E nebo lepší. V situaci, kdy: • za bloky máte před penalizací 77, 62, 61, 64, • v prvním bloku jste opsali domácí úkol, budete penalizováni: • v prvním bloku 10 + 5, tzn. bodové zisky za bloky budou efektivně 62, 62, 61, 64, • v celkovém hodnocení 10, tzn. celkový zisk 62 + 62 + 61 + 64 - 10 = 239, a budete tedy hodnoceni známkou F. To, jestli jste příklad řešili společně, nebo jej někdo vyřešil samostatně, a poté poskytl své řešení někomu dalšímu, není pro účely kontroly opisování důležité. Všechny „verze“ řešení odvozené ze společného základu budou penalizovány stejně. Taktéž zveřejnění řešení budeme chápat jako pokus o podvod, a budeme jej trestat, bez ohledu na to, jestli někdo stejné řešení odevzdá, nebo nikoliv. Podotýkáme ještě, že kontrola opisování nespadá do desetidenní lhůty pro hodnocení průběžných kontrol. Budeme se sice snažit opisování kontrolovat 6/90 PB161 Programování v jazyce C++, 16. února 2024 co nejdříve, ale odevzdáte-li opsaný příklad, můžete být bodově penalizováni kdykoliv (tedy i dodatečně, a to až do konce zkouškového období). 7/90 Část 1: Hodnoty a funkce Vítejte v PB161. Než budete pokračovat, přečtěte si prosím kapitolu A (složku 00 ve zdrojovém balíku). Podrobnější informace jak se soubory v této složce pracovat naleznete v souboru 00/t3_sources.txt, resp. v sekci T.3. Cvičení bude tematicky sledovat přednášku z předchozího týdne: první kapitola tak odpovídá první přednášce. Tématy pro tento týden jsou funkce, řízení toku, skalární hodnoty a reference. Tyto koncepty jsou krom přednášky demonstrovány v příkladech typu d (ukázkách; naleznete je také v souborech d?_*.cpp, např. d1_fibonacci.cpp). 1. fibonacci – iterativní výpočet Fibonacciho čísel, 2. comb – výpočet kombinačního čísla, 3. hamming – hammingova vzdálenost dvojkového zápisu, 4. root † – výpočet n-té celočíselné odmocniny. Ve druhé části každé kapitoly pak naleznete tzv. elementární cvičení, která byste měli být schopni relativně rychle vyřešit a ověřit si tak, že jste porozuměli konceptům z přednášky a ukázek. Tyto příklady naleznete v souborech pojmenovaných e?_*.cpp. Řešení můžete vepsat přímo do nachystaného souboru se zadáním. Základní testy jsou součástí zadání. Vzorová řešení těchto příkladů naleznete v kapitole K (klíč) na konci sbírky, resp. ve složce sol zdrojového balíku. Mějte na paměti, že přiložená vzorová řešení nemusí být vždy nejjednodušší možná. Také není nutné, aby Vaše řešení přesně odpovídalo tomu vzorovému, nebo bylo založeno na stejném principu. Důležité je, aby pracovalo správně a dodržovalo požadovanou (resp. adekvátní) složitost. Elementární příklady první kapitoly jsou: 1. factorial – spočtěte faktoriál zadaného čísla, 2. concat – zřetězení binárního zápisu dvou čísel, 3. zeros – počet nul v zápisu čísla. V další části naleznete o něco složitější příklady, tzv. přípravy. Jejich hlavním účelem je samostatně procvičit látku dané kapitoly, a to ještě předtím, než se o ní budeme bavit ve cvičení. Doporučujeme každý týden vyřešit alespoň 3 přípravy. Abyste byli motivovaní je řešit, odevzdaná řešení jsou bodována (detaily bodování a termíny odevzdání naleznete v kapitole A). Ve zdrojovém balíku se jedná o soubory s názvem p?_*.cpp. Pozor: Diskutovat a sdílet řešení příprav je přísně zakázáno. Řešení musíte vypracovat zcela samostatně (bližší informace naleznete opět v kapitole A). Přípravy: 1. nhamming – Hammingova vzdálenost s libovolným základem, 2. digitsum – opakovaný ciferný součet, 3. parity – počet jedniček v binárním zápisu, 4. periodic – hledání nejkratšího periodického vzoru, 5. balanced – ciferné součty ve vyvážených soustavách, 6. subsetsum – známý příklad na backtracking. Další část je tvořena rozšířenými úlohami, které jsou typicky o něco málo složitější, než přípravy. Na těchto úlohách budeme probranou látku dále procvičovat ve cvičení. Tyto úlohy můžete také řešit společně, diskutovat jejich řešení se spolužáky, atp. Svá řešení můžete také srovnat s těmi vzorovými, která jsou k nalezení opět v kapitole K. Tento typ úloh naleznete v souborech pojmenovaných r?_*.cpp. 1. bitwise – ternární bitové operátory, 2. euler – Eulerova funkce (počet nesoudělných čísel), 3. hamcode – kód pro detekci chyb Hamming(8,4), 4. cbc – cipher block chaining, 5. cellular – celulární automat nad celým číslem, 6. flood – vyplňování „ploch“ v celém čísle. Poslední část jsou tzv. volitelné úkoly, které se podobají těm rozšířeným, se dvěma důležitými rozdíly: volitelné úlohy jsou určeny k samostatné přípravě (nebudeme je tedy používat ve cvičení) a nejsou k nim dostupná vzorová řešení. Je totiž důležité, abyste si dokázali sami zdůvodnit a otestovat správnost řešení, aniž byste jej srovnávali s řešením někoho jiného (a přiložený vzor k tomu jistě svádí). Je nicméně povoleno tyto příklady (a jejich řešení, jak abstraktně, tak konkrétně) diskutovat se spolužáky. Přesto velmi důrazně doporučujeme, abyste si řešení zkusili prvně vypracovat sami. 1. xxx – … 2. xxx – … 3. xxx – … 1 Hlavičkové soubory Samotný jazyk, který ve svých řešeních používáte, omezujeme jen minimálně (varování překladače a kontrola nástrojem clangtidy ovšem některé obzvláště problémové konstrukce zamítnou). Trochu významnější omezení klademe na používání standardní knihovny: do svých odevzdaných programů prosím vkládejte pouze ty standardní hlavičky, kterých použití jsme již v předmětu zavedli. Přehled bude vždy uveden v úvodu příslušné kapitoly. Pro tu první jsou to tyto tři: • cassert – umožňuje použití tvrzení assert, • algorithm – nabízí funkce std::min, std::max, • cstdint – celočíselné typy std::intNN_t a std::uintNN_t. Omezeno je pouze vkládání hlavičkových souborů: je-li povolena hlavička algorithm, můžete v principu používat i jiné algoritmy, které poskytuje. Přesto spíše doporučujeme držet se toho, co jsme Vám zatím ukázali. Na nic jiného, než vkládání standardních hlaviček, v tomto předmětu preprocesor potřebovat nebudete. Jiné direktivy než #include tedy prosím vůbec nepoužívejte. 1.d: Demonstrace (ukázky) 1.d.1 [fibonacci] V této ukázce naprogramujeme klasický ukázkový algoritmus, totiž výpočet n-tého Fibonacciho čísla (a použijeme k tomu iterativní algoritmus). Algoritmus bude implementovat podprogram (funkce) fibonacci. Definice podprogramu se v jazyce C++ začíná tzv. signaturou neboli hlavičkou funkce, která: 1. popisuje návratovou hodnotu (zejména její typ), 2. udává název podprogramu a 3. jeho formální parametry, opět zejména jejich typy, ale obvykle i názvy. Signatura může popisovat i další vlastnosti, se kterými se setkáme později. V tomto případě bude návratovou hodnotou celé číslo (znaménkového typu int), podprogram ponese název fibonacci a má jeden parametr, opět celočíselného typu int. int fibonacci( int n ) Po signatuře následuje tzv. tělo, které je syntakticky shodné se složeným příkazem, a je tedy tvořeno libovolným počtem (včetně nuly) příkazů uzavřených do složených závorek. V těle funkce jsou formální parametry (v tomto případě n) ekvivalentní lokálním proměnným pomyslně inicializovaným hodnotou skutečného parametru. { Tělo je tvořeno posloupností příkazů (typický příkaz je ukončen středníkem, ale toto neplatí např. pro složené příkazy, které jsou ukončeny složenou závorkou). Prvním příkazem podprogramu fibonacci je deklarace lokálních proměnných a, b, opět celočíselného typu int. Deklarace se skládá z: 1. typu, případně klíčového slova auto, 2. neprázdného seznamu deklarovaných jmen (oddělených čárkou), které mohou být doplněny tzv. deklarátory (označují např. reference: uvidíme je v pozdější ukázce), 3. volitelného inicializátoru, který popisuje počáteční hodnotu proměnné. 8/90 PB161 Programování v jazyce C++, 16. února 2024 int a = 1, b = 1, c; Samotný výpočet zapíšeme pomocí tzv. třídílného for cyklu (jinou variantu cyklu for si ukážeme v další kapitole), který má následující strukturu: 1. klíčové slovo for, 2. hlavička cyklu, uzavřená v kulatých závorkách, a. inicializační příkaz (výraz, deklarace proměnné, nebo prázdný příkaz) je vždy ukončen středníkem a provede se jednou před začátkem cyklu; deklaruje-li proměnné, tyto jsou platné právě po dobu vykonávání cyklu, b. podmínka cyklu (výraz nebo prázdný příkaz) je opět vždy ukončena středníkem a určuje, zda se má provést další iterace cyklu (vyhodnotíli se na true), c. výraz iterace (výraz, který není ukončen středníkem), který je vyhodnocen vždy na konci těla (před dalším vyhodnocením podmínky cyklu), 3. tělo cyklu (libovolný příkaz, často složený). for ( int i = 2; i < n; ++i ) { V jazyce C++ je přiřazení výraz, kterého vyhodnocení má vedlejší efekt, a to konkrétně změnu proměnné, která je odkazována levou stranou operátoru = (jedná se o výraz, který se musí vyhodnotit na tzv. l-hodnotu8 – l od left, protože stojí na levé straně přiřazení). Na pravé straně pak stojí libovolný výraz. c = a + b; a = b; b = c; } Příkaz návratu z podprogramu return má dvojí význam (podobně jako ve většině imperativních jazyků): 1. určí návratovou hodnotu podprogramu (tato se získá vyhodnocením výrazu uvedeného po klíčovém slově return), 2. ukončí vykonávání podprogramu a předá řízení volajícímu. return b; } Všechny ukázky v této sbírce obsahují několik jednoduchých testovacích případů, kterých účelem je jednak předvést, jak lze implementovanou funkcionalitu použít, jednak ověřit, že fungování programu odpovídá naší před- 8 Zjednodušeně, l-hodnota je takový výraz, který popisuje identitu resp. lokaci – typicky proměnnou, která je uložena v paměti. L-hodnoty rozlišujeme proto, že smyslem přiřazení je uložit (zapsat) výsledek své pravé strany, a na levé straně tedy musí stát objekt, do kterého lze tuto pravou stranu skutečně zapsat. Nejjednodušší l-hodnotou je název proměnné. stavě. Zkuste si přiložené testy různě upravovat, abyste si ověřili, že dobře rozumíte tomu, jak ukázka funguje. int main() /* demo */ { Použití (volání) podprogramu je výraz a jeho vyhodnocení odpovídá naší intuitivní představě: skutečné parametry (uvedené v kulatých závorkách za jménem) se použijí jako pomyslné inicializátory formálních parametrů a s takto inicializovanými parametry se vykoná tělo podprogramu. Po jeho ukončení se výraz volání podprogramu vyhodnotí na návratovou hodnotu. assert( fibonacci( 1 ) == 1 ); assert( fibonacci( 2 ) == 1 ); assert( fibonacci( 7 ) == 13 ); assert( fibonacci( 20 ) == 6765 ); } 1.d.2 [comb] V této ukázce se zaměříme na vlastnosti celočíselných typů. Podíváme se přitom na kombinační čísla, definovaná jako: (n¦k) = n!/(k! ⋅ (n − k)!) kde k ≤ n. Samozřejmě, mohli bychom počítat kombinační čísla přímo z definice, má to ale jeden důležitý problém: celočíselné proměnné mají v C++ pevný rozsah. Výpočet mezivýsledku n! tak může velmi lehce překročit horní hranici použitého typu, a to i v případech, kdy celkový výsledek není problém reprezentovat. Proto je důležité najít formu výpočtu, která nebude vytvářet zbytečně velké mezivýsledky. Výpočet kombinačního čísla lze navíc provádět na libovolném celočíselném typu (včetně těch bezznaménkových), proto čistou funkci comb definujeme tak, aby fungovala pro všechny takové typy. Parametr uvedený klíčovým slovem auto může být libovolného typu (použití funkce s takovým typem parametru, pro který tělo funkce není typově správné, překladač zamítne). Něco jiného znamená návratová hodnota deklarovaná jako auto: tento typ se odvodí z příkazů return v těle funkce. Chceme-li toto tzv. odvození návratového typu využít, musí mít výraz u všech příkazů return v těle stejný typ. auto comb( auto n, auto k ) { Kombinační čísla jsou definovaná pouze pro n ≥ k a tuto vstupní podmínku si můžeme lehce ověřit tvrzením: assert( n >= k ); Výpočet budeme provádět na stejném typu, jaký má vstupní n. Protože tento typ neznáme, musíme si pomoct konstrukcí decltype, která nám umožní vytvořit proměnnou stejného typu, jako nějaká existující. Pozor! Je-li původní proměnná referencí, bude i nová proměnná referencí. Pozor! Nemůžeme zde použít auto result = 1. Proč? decltype( n ) result = 1; Jak jistě víte, faktoriál je definován takto: n! = n ∏ i=1 i A tedy: n!/k! = n ∏ i=1 i/ k ∏ i=1 i = n ∏ i=k+1 i Tento výpočet bychom jednoduše zapsali do for cyklu v příslušných mezích. Ve skutečnosti ale můžeme výpočet ještě znatelně zlepšit. Klíčové pozorování je, že ani zbývající (n − k)! není potřeba vyčíslovat. Víme jistě, že výsledek bude celé číslo, tzn. všechny faktory (n − k)! se musí pokrátit s nějakými faktory n!/k!. Jedna možnost je seřadit faktory čitatele sestupně a faktory jmenovatele vzestupně a mezivýsledek střídavě násobit a dělit příslušným faktorem (celočíselnost mezivýsledků je zde zaručena tím, že jsou to opět kombinační čísla, jak lze nahlédnout např. rozšířením příslušných zlomků vhodným faktoriálem). Toto řešení je optimální v počtu aritmetických operací, není ale optimální ve velikosti mezivýsledku. Přesněji, je-li (n¦ℎ) největší kombinační číslo s daným n, největší mezivýsledek při výpočtu (n¦k) bude ℎ⋅(n¦ℎ). Využijemeli navíc symetrie (n¦k) = (n¦n − k), můžeme tuto mez zlepšit na k ⋅ (n¦k) a zároveň zabezpečit, že k ≤ ℎ. Je nicméně zřejmé, že výpočet nám může přetéct i v případě, kdy celkový výsledek reprezentovat lze. Proměnné nom_f a denom_f budou reprezentovat aktuální faktory v čitateli a jmenovateli. Opět budou stejného typu jako vstupní n. decltype( n ) nom_f = n, denom_f = 1; Cyklus provedeme pro nom_f v klesajícím rozsahu ⟨n, k) resp. ⟨n, n − k), podle toho která spodní mez je větší. Protože jednotlivé mezihodnoty na spodní hranici iterace nezávisí, je jistě výhodnější provést méně iterací. while ( nom_f > std::max( k, n - k ) ) { Máme-li cyklus zapsaný správně, faktor jmenovatele nemůže překročit menší z hodnot k nebo n − k. O tomto se opět ujistíme tvrzením. assert( denom_f <= std::min( k, n - k ) ); Dále provedeme samotný krok výpočtu. Tvrzením se ujistíme, že provádíme 9/90 skutečně pouze celočíselná dělení beze zbytku (kdyby tomu tak nebylo, výpočet by byl nesprávný!). result *= nom_f; assert( result % denom_f == 0 ); result /= denom_f; Nakonec upravíme iterační proměnné a pokračujeme další iterací. --nom_f; ++denom_f; } return result; } int main() /* demo */ { assert( comb( 1, 1 ) == 1 ); assert( comb( 2, 1 ) == 2 ); assert( comb( 5, 2 ) == 10 ); Postup implementovaný podprogramem comb nám umožňuje spočítat, za pomoci 64-bitových proměnných, všechna kombinační čísla pro n ≤ 60, a to i přesto, že nejen 60! ≈ 1, 1 ⋅ 2272 , ale 60!/30! ≈ 1, 34 ⋅ 2164 a tedy ani toto mnohem menší číslo se do 64-bitové proměnné v žádném případě nevejde. Poznámka: typ std::int64_t je právě 64-bitový celočíselný typ se znaménkem. Abychom ho zde mohli použít, museli jsme výše vložit hlavičku cstdint. for ( std::int64_t i = 1; i < 60; ++i ) for ( std::int64_t k = 1; k < i; ++k ) assert( comb( i + 1, k + 1 ) == comb( i, k ) + comb( i, k + 1 ) ); return 0; } 1.d.3 [hamming] Tato ukázka přináší výstupní parametry a s nimi reference. Naším úkolem bude naprogramovat podprogram hamming, který spočítá tzv. Hammingovu vzdálenost dvou nezáporných čísel a, b. Hammingova vzdálenost se tradičně definuje jako počet znaků, ve kterých se vstupní hodnoty liší. Abychom tedy mohli mluvit o vzdálenosti čísel, musíme je nějak zapsat – v této ukázce k tomu zvolíme dvojkovou soustavu. Protože Hammingova vzdálenost je navíc definovaná pouze pro stejně dlouhá slova, je-li některý dvojkový zápis kratší, doplníme ho pro účely výpočtu levostrannými nulami. Krom samotné vzdálenosti nás bude zajímat také řád nejvyšší číslice, ve které se vstupní čísla liší (rozmyslete si, že taková existuje právě tehdy, je-li výsledná vzdálenost nenulová). Pro tento dodatečný výsledek (který navíc nemusí být vždy definovaný) použijeme již zmiňovaný výstupní parametr. V případech, kdy definovaný není, nebudeme hodnotu výstupního parametru měnit. Výstupní parametr realizujeme referencí, kterou zapíšeme za pomoci deklarátoru & – reference na celočíselnou hodnotu typu int bude tedy int &.9 Takto deklarovaná reference zavádí nové jméno pro již existující objekt. Objeví-li se tedy reference ve formálním parametru, takto zavedené jméno se přímo váže k hodnotě skutečného parametru. Pro tento skutečný parametr platí stejná omezení, jako pro levou stranu přiřazení (musí tedy být l-hodnotou). Je to proto, že takto zavedený parametr je pouze novým jménem pro skutečný parametr. Zejména tedy platí, že kdykoliv se formální parametr objeví na levé straně přiřazení, toto přiřazení má efekt na skutečný parametr. Díky tomu můžeme uvnitř těla změnit hodnotu skutečného parametru. Je-li tedy skutečný parametr např. jméno lokální proměnné ve volající funkci, hodnota této proměnné se může provedením volané funkce změnit. Rozmyslete si, že u běžných parametrů (které nejsou referencemi) tomu tak není. int hamming( auto a, auto b, int &order ) { Jako obvykle nejprve ověříme vstupní podmínku. assert( a >= 0 && b >= 0 ); Protože pracujeme s dvojkovou reprezentací, můžeme si výpočet zjednodušit použitím vhodných bitových operací. Operátor ^ (xor, exclusive or) nastaví na jedničku právě ty bity výsledku, ve kterých se jeho operandy liší. Hledaná Hammingova vzdálenost je tedy právě počet jedniček v binární reprezentaci čísla x. Všimněte si, že pro lokální proměnnou x neuvádíme typ – podobně jako v deklaraci parametrů a, b jsme zde použili zástupné slovo auto. Typ takto deklarované proměnné se odvodí z jejího inicializátoru (v tomto případě a ^ b). Na rozdíl od konstrukce decltype je takto deklarovaná proměnná vždy hodnotou, i v případě, že pravá strana je referenčního typu.10 auto x = a ^ b; Pro výsledek si zavedeme pomocnou proměnnou result, do které sečteme počet nenulových bitů. Pozor! proměnné jednoduchých typů je nutné ini- 9 To, že se jedná o referenci, je součástí typu takto zavedené proměnné (resp. parametru) – projeví se to např. při použití konstrukce decltype. Zároveň ale platí, že ve většině případů jsou reference a hodnoty záměnné: je to mimo jiné proto, že na výsledek libovolného výrazu lze nahlížet jako na určitý druh reference. Blíže se budeme referencemi zabývat ve čtvrté kapitole. 10 To opět souvisí s tím, že každý výraz lze interpretovat jako referenci. Chování tohoto typu deklarace je uzpůsobeno tomu, že obvykle chceme deklarovat lokální proměnné – lokální reference jsou mnohem vzácnější. cializovat i v případě, že má být jejich počáteční hodnota nulová. Bez inicializátoru vznikne neinicializovaná proměnná kterou je zakázáno číst (níže použitý operátor ++ „zvětši hodnotu o jedna“ samozřejmě svůj operand přečíst musí). int result = 0; Číslo, které obsahuje alespoň jeden nenulový bit je jistě nenulové – cyklus se tedy bude provádět tak dlouho, dokud jsou v čísle x nenulové bity. for ( int i = 0; x != 0; ++i, x >>= 1 ) V těle cyklu budeme zkoumat nejnižší bit hodnoty x, přitom proměnná i obsahuje jeho původní řád. Všimněte si, že for cyklus je poměrně flexibilní, a že je důležité si jeho hlavičku dobře přečíst: v tomto případě se např. proměnná i vůbec neobjevuje v podmínce. Naopak výraz iterace má dvě části (oddělené operátorem čárka, který vyhodnotí svůj první operand pouze pro jeho vedlejší efekt – jeho hodnotu zapomene). Efekt na i je celkem zřejmý, zajímavější je efekt na x: výraz x >>= 1 provede bitový posun proměnné x o jeden bit doprava. Původní druhý nejnižší bit se tak stane nejnižším, atd., až nejvyšší bit se doplní nulou. Příklad: posunem osmibitové hodnoty 10011001 o jednu pozici doprava vznikne hodnota 01001100. Celý cyklus bychom samozřejmě mohli zapsat jako while cyklus a vyhnuli bychom se tím relativně komplikované hlavičce. Výhodou cyklu for v tomto případě ale je, že veškeré informace o změnách iteračních proměnných jsou uvedeny na jeho začátku. Čtenář tak už od začátku ví, jaký mají tyto proměnné význam (i se každou iterací zvýší o jedna, x se bitově posune o jednu pozici doprava) a nemusí tuto informaci zdlouhavě hledat v těle. { V každé iteraci tak budeme zkoumat nejnižší bit hodnoty x (který odpovídá i-tému bitu vstupních parametrů a, b). S výhodou k tomu použijeme operaci bitové konjunkce (and; na jedničku jsou nastaveny právě ty bity výsledku, které mají hodnotu 1 v obou operandech). Tento operátor zapisujeme znakem & (pozor, nezaměňujte s deklarátorem reference!). if ( x & 1 ) { Je-li nejnižší bit nastavený, zvýšíme hodnotu proměnné result a do výstupního parametru order zapíšeme jeho původní řád (který si udržujeme v proměnné i). Protože bity procházíme v pořadí od nejnižšího k nejvyššímu, poslední zápis do parametru order přesně odpovídá nejvyššímu rozdílnému bitu. Podmínka cyklu nám navíc zaručuje, že do proměnné order zapíšeme pouze v případě, že takový bit existuje. 10/90 PB161 Programování v jazyce C++, 16. února 2024 ++result; order = i; } } Po ukončení cyklu platí, že jsme zpracovali všechny nenulové bity x, a tedy všechny bity, ve kterých se hodnoty a, b lišily. Nezbývá, než nastavit návratovou hodnotu a podprogram ukončit. return result; Všimneme si ještě, že hodnotu parametru order jsme nečetli. Definující vlastností výstupních parametrů je, že chování podprogramu nezávisí na jejich počáteční hodnotě. V případě, že jediná operace, kterou s výstupním parametrem provedeme, je přiřazení do něj, je tento požadavek triviálně splněn. } int main() /* demo */ { int order = 3; Protože skutečný parametr order předáváme referencí (odpovídající formální parametr je referenčního typu), změny, které v něm podprogram hamming provede, jsou viditelné i navenek. Nejprve ovšem ověříme, že při nulové vzdálenosti se hodnota order nemění. assert( hamming( 0, 0, order ) == 0 && order == 3 ); assert( hamming( 1, 1, order ) == 0 && order == 3 ); Hodnoty v dalším příkladě se liší ve dvou bitech (osmém a devátém) a proto očekáváme, že po provedení funkce hamming bude mít proměnné order hodnotu 9. assert( hamming( 512, 256, order ) == 2 && order == 9 ); assert( hamming( 0, 1, order ) == 1 && order == 0 ); assert( hamming( 0xffffff, 0, order ) == 24 ); assert( hamming( 0xffffffffff, 0, order ) == 40 ); assert( hamming( 0xf000000000, 0xf, order ) == 8 ); assert( hamming( 0xf000000000, 0xe000000000, order ) == 1 ); return 0; } 1.d.4 [root] † V této poslední ukázce bude naším cílem spočítat celočíselnou n-tou odmocninu zadaného nezáporného čísla k. Nejprve ale budeme potřebovat dvě pomocné funkce: celočíselný dvojkový logaritmus a n-tou mocninu. Vyzbrojeni těmito funkcemi pak budeme schopni odmocninu vypočítat tzv. Newtonovou-Raphsonovou metodou. Celočíselný dvojkový logaritmus čísla n definujeme jako největší celé číslo k takové, že 2k ≤ n. Za povšimnutí stojí, že pro n < 1 takové k neexistuje – proto tuto funkci definujeme pouze pro kladná n. auto int_log2( auto n ) { Jako obvykle nejprve ověříme vstupní podmínku. assert( n > 0 ); Výpočet budeme provádět v pomocné proměnné, která bude stejného typu jako n. decltype( n ) result = 0; Princip výpočtu je jednoduchý, uvážíme-li dvojkový rozvoj čísla n = ∑ ai2i , který obsahuje člen 2i pro každý nenulový bit ai. Dvojkovým logaritmem bude právě nejvyšší mocnina dvojky, která se v tomto rozvoji objeví: jistě pak platí, že 2k ≤ n, stačí se ujistit, že takto získané k je největší možné. Uvažme tedy, že existuje nějaké l > k a zároveň 2l ≤ n. Pak se ale musí 2l nutně objevit ve dvojkovém rozvoji čísla n, což je spor s tím, že k byla nejvyšší mocnina v témže rozvoji. Stačí nám tedy nalézt řád nejvyššího jedničkového bitu. To provedeme tak, že budeme provádět bitové posuny doprava tak dlouho, až n vynulujeme. Počet takto provedených posunů je pak hledaný řád. while ( n > 1 ) { ++result; n >>= 1; } return result; } Jazyk C++ na rozdíl od některých jiných neposkytuje zabudovanou operaci mocnění pro celočíselné typy. Výpočet provedeme známým algoritmem binárního umocňování (anglicky známého popisněji jako „exponentiation by squaring“).11 Klíčovou vlastností tohoto algoritmu je, že jeho složitost je lineární k počtu bitů exponentu – naivní algoritmus opakovaným násobením má naproti tomu složitost exponenciální (složitost je v tomto případě přímo úměrná hodnotě exponentu, nikoliv délce jeho zápisu). auto int_pow( auto n, auto exp ) { Operaci budeme definovat pouze pro kladné exponenty. Vyhneme se tak mimo 11 Popis algoritmu na české wikipedii je v době psaní tohoto textu zcela nepoužitelný. Podívejte se raději do té anglické. jiné nutnosti definovat hodnotu pro 00 . assert( exp >= 1 ); Pomocná proměnná, která bude udržovat „liché“ mocniny. Její význam je přesněji vysvětlen níže. decltype( n ) odd = 1; Výpočet je založený na pozorování, že pro sudý exponent k platí nk = n2l = (n2 )l kde l = k/2. Za cenu výpočtu jedné druhé mocniny – n2 – tak můžeme exponent snížit na polovinu (cyklus se provede právě tolikrát, kolik bitů je v zápisu hodnoty exp). while ( exp > 1 ) { Musíme ovšem ještě vyřešit situaci, kdy je exponent lichý. Zde je potřebný vztah trochu složitější: nk = n⋅(n2l ) pro l = ⌊k/2⌋. V rekurzivním zápisu bychom mohli tento vztah přímo použít, v tom iterativním ale nastane drobný problém: faktor n před závorkou nevstupuje do výpočtu druhé mocniny v další iteraci. Asi nejjednodušším řešením je použití pomocného střadače, který bude udržovat tyto „přebývající“ faktory. Je-li exp liché, přinásobíme tedy faktor n do proměnné odd. Na konci ovšem nesmíme zapomenout, že ve výsledném n tyto faktory chybí. if ( exp % 2 == 1 ) odd *= n; Dále je výpočet pro sudé i liché exponenty stejný: hodnotu proměnné n umocníme na druhou a exponent vydělíme dvěma. n *= n; exp /= 2; } Na závěr si vzpomeneme, že některé faktory celkového výsledku jsme si „odložili“ do proměnné odd. return n * odd; Pro ilustraci uvažme výpočet 310 : iterace n exp odd 0. 3 10 1 1. 3⋅3 5 1 2. (3⋅3)⋅(3⋅3) 2 3⋅3 3. (3⋅3)⋅(3⋅3)⋅(3⋅3)⋅(3⋅3) 1 3⋅3 V proměnné n jsme sesbírali 8 faktorů, zatímco proměnná odd získala 2, celkem jich je tedy potřebných 10. Rekurzivní výpočet by naproti tomu 11/90 dopadl takto: (3 ⋅ (3 ⋅ 3) ⋅ (3 ⋅ 3)) ⋅ (3 ⋅ (3 ⋅ 3) ⋅ (3 ⋅ 3)) Uvažme ještě výpočet 311 . Je zejména důležité si uvědomit, že faktor, který na daném řádku přidáváme do odd (je-li exp na předchozím řádku liché) je právě hodnota n z tohoto předchozího řádku. iterace n exp odd 0. 3 11 1 1. 3⋅3 5 3 2. (3⋅3)⋅(3⋅3) 2 3⋅(3⋅3) 3. (3⋅3)⋅(3⋅3)⋅(3⋅3)⋅(3⋅3) 1 3⋅(3⋅3) Stejný výpočet rekurzivně: 3 ⋅ (3 ⋅ (3 ⋅ 3) ⋅ (3 ⋅ 3)) ⋅ (3 ⋅ (3 ⋅ 3) ⋅ (3 ⋅ 3)) } Tím se dostáváme k poslední části: samotnému výpočtu celočíselné odmocniny. Budeme ji opět definovat jako největší s takové, že sn ≤ k. auto int_nth_root( auto n, auto k ) { Pro jednoduchost budeme uvažovat pouze odmocniny nezáporných čísel. assert( k >= 0 ); assert( n >= 1 ); Jednoduché případy vyřešíme zvlášť, protože by nám v obecném výpočtu níže působily určité potíže. if ( n == 1 || k == 0 ) return k; Na podrobný popis Newtonovy-Raphsonovy metody (známé též jako metoda tečen) zde nemáme prostor: možná ji znáte z kurzu matematické analýzy, případně si ji můžete vyhledat např. na wikipedii. Pro nás jsou klíčové její základní vlastnosti: 1. metoda nám umožní rychle nalézt x takové, že pro zadané f platí f(x) = 0, 2. potřebujeme k tomu samozřejmě definici f, 3. dále její první derivaci f′ 4. a počáteční odhad hledané hodnoty x0. Výpočet opakovaně zlepšuje aktuální odhad x, a to pomocí vzorce: xi+1 = xi − f(xi)/f′(xi) Vyvstává otázka, jak nám hledání nuly pomůže ve výpočtu odmocniny. K tomu musíme problém přeformulovat. Uvažme f(s) = sn − k Je-li f(s) = 0, pak jistě sn = k, což je ale přesně definice n-té odmocniny (prozatím té reálné). Potřebujeme ještě derivaci, která je naštěstí velmi jednoduchá: f′(s) = n ⋅ sn−1 protože n je celočíselná konstanta (pro n = 1 bychom ovšem narazili na problém). Celkem tedy: si+1 = si − (sn i − k)/(n ⋅ sn−1 i ) Nebo výhodněji (přechodem na společný jmenovatel a krácením mocnin si): si+1 = ti / n ti = (n − 1) ⋅ si + k/sn−1 Zbývá počáteční odhad, který potřebujeme spočítat rychle (a samozřejmě potřebujeme, aby byl co nejblíže výsledné hodnotě s). Využijeme k tomu dříve definovaný dvojkový logaritmus. Protože log(ab ) = b ⋅ log(a), můžeme hledanou odmocninu odhadnout jako 2l+1 pro l = ⌊log2 (k)/n⌋. Také si všimneme, že tento odhad leží na stejné straně jediného stacionárního bodu funkce f (totiž s = 0) jako skutečné řešení. Nemůže se nám tedy stát, že by výpočet divergoval. decltype( k ) base = 2; auto s = base << ( int_log2( k ) / n ); Samotná iterace je po zdlouhavé přípravě už velmi jednoduchá. Zbývají nám k vyřešení dva (související) problémy: kdy iteraci ukončit a jak výpočet provést nad celými čísly. Protože k > 0, je funkce f v kritické oblasti konvexní (tečny leží pod grafem). Po první iteraci bude náš odhad tedy celkem jistě příliš velký – průsečík tečny s osou x najdeme vpravo od skutečné nuly – a tato situace se už nezmění. V tuto chvíli ale do hry vstupuje skutečnost, že pracujeme s celými a nikoliv reálnými čísly. Výpočet jsme uspořádali tak, aby byl výpočet průsečíku přesný – konkrétně je výsledkem výpočtu dolní celá část jeho reálné hodnoty. Tato dolní celá část sice může být menší, než skutečná hodnota reálné odmocniny, nemůže ale být menší než námi definovaná celočíselná odmocnina. Tato pozorování nám konečně umožní formulovat podmínku ukončení: najdemeli skutečnou celočíselnou odmocninu, další odhad může být buď stejný nebo větší než ten předchozí. Tato situace zároveň nemůže nastat dříve: 1. z konvexnosti plyne, že odhad, který je příliš velký, se musí ke skutečnému výsledku provedením iterace přiblížit, 2. protože předchozí vypočtený odhad je vždy celé číslo, musí sebemenší posun na reálné ose směrem doleva vést ke snížení jeho dolní celé části alespoň o jedničku. Celkově tedy cyklus skončí přesně ve chvíli, kdy začne platit sn ≤ k. while ( true ) { const auto t = ( n - 1 ) * s + k / int_pow( s, n - 1 ); const auto s_next = t / n; if ( s_next >= s ) return s; else s = s_next; } } int main() /* demo */ { assert( int_log2( 1 ) == 0 ); assert( int_log2( 2 ) == 1 ); assert( int_pow( 2, 2 ) == 4 ); assert( int_nth_root( 1, 2 ) == 2 ); assert( int_nth_root( 2, 4 ) == 2 ); assert( int_nth_root( 3, 8 ) == 2 ); for ( int k = 0; k < 1000; ++k ) for ( int n = 1; n < 20; ++n ) { auto root = int_nth_root( n, k ); assert( int_pow( root, n ) <= k ); assert( int_pow( root + 1, n ) > k ); } return 0; } 1.e: Elementární příklady 1.e.1 [factorial] Vypočtěte faktoriál zadaného nezáporného čísla. int factorial( int n ); 1.e.2 [concat] Najděte a vraťte číslo, které vznikne zapsáním (nezáporných) celých čísel a, b v binárním zápisu za sebe (zápis čísla a bude vlevo, zápis čísla b vpravo a bude doplněn levostrannými nulami na délku b_bits 12/90 PB161 Programování v jazyce C++, 16. února 2024 bitů). Je-li zápis čísla b delší než b_bits, výsledek není definován. Příklad: concat( 1, 1, 2 ) vrátí hodnotu 0b101 = 5. std::uint64_t concat( std::uint64_t a, std::uint64_t b, int b_bits ); 1.e.3 [zeros] Zapišme nezáporné číslo n v soustavě o základu base. Určete kolik (nelevostranných) nul se v tomto zápisu objeví. Do výstupního parametru order uložte řád nejvyšší z nich. Není-li v zápisu nula žádná, hodnotu order nijak neměňte. int zeros( int n, int base, int &order ); 1.e.4 [normalize] Write a function to normalize a fraction, that is, find the greatest common divisor of the numerator and denominator and divide it out. Both numbers are given as in/out parameters. // void normalize( … ) 1.p: Přípravy 1.p.1 [nhamming] Nezáporná čísla a, b zapíšeme v poziční soustavě o základu base. Spočtěte hammingovu vzdálenost těchto dvou zápisů (přitom zápis kratšího čísla podle potřeby doplníme levostrannými nulami). int hamming( int a, int b, int base ); 1.p.2 [digitsum] Funkce digit_sum sečte cifry nezáporného čísla num v zápisu o základu base. Je-li výsledek ve stejném zápisu víceciferný, sečte cifry tohoto výsledku, atd., až je výsledkem jediná cifra, kterou vrátí jako svůj výsledek. int digit_sum( int num, int base ); 1.p.3 [parity] Funkce parity zjistí, je-li počet jedniček na vstupu sudý (výsledkem je false) nebo lichý (výsledkem je true). Jedničky počítáme v binárním zápisu vstupního bezznaménkového čísla word. Je-li navíc chksum na začátku true, počítá se jako další jednička. Celkový výsledek jednak uložte do parametru chksum, jednak ho vraťte jako návratovou hodnotu. bool parity( auto word, bool &chksum ); 1.p.4 [periodic] Najděte nejmenší nezáporné číslo n takové, že 64-bitový zápis čísla word lze získat zřetězením nějakého počtu binárně zapsaných kopií n. Protože potřebný zápis n může obsahovat levostranné nuly, do výstupního parametru length uložte jeho délku v bitech. Je-li možné word sestavit z různě dlouhých zápisů nějakého n, vyberte ten nejkratší možný. Příklad: pro word = 0x100000001 bude hledané n = 1, protože word lze zapsat jako dvě kopie čísla 1 zapsaného do 32 bitů. std::uint64_t periodic( std::uint64_t word, int &length ); 1.p.5 [balanced] V této úloze budeme opět počítat ciferný součet, ale v takzvaných vyvážených ciferných soustavách, které mají jak záporné tak kladné číslice. Budeme uvažovat pouze liché základy symetricky rozložené kolem nuly (tzn. trojkovou s číslicemi −1, 0, 1, pětkovou −2, −1, 0, 1, 2, atd.). Vaším úkolem je napsat predikát is_balanced, který rozhodne, má-li zadané číslo n ve vyvážené soustavě zadané svým základem base nulový ciferný součet. Výpočet cifer čísla n ve vyvážené soustavě o základu b probíhá podobně, jako v té klasické se stejným základem. Nejprve si připomeneme klasický algoritmus. Nastavíme n0 = n a opakujeme: 1. cifru ci získáme jako zbytek po dělení ni základem b, 2. spočítáme ni+1 tak, že vydělíme ni základem b, 3. je-li výsledek nenulový, pokračujeme bodem 1, jinak skončíme. Abychom získali vyvážený zápis místo toho klasického, musíme vyřešit situaci, kdy ci není povolenou číslicí. Všimneme si, že musí po každém kroku platit (přímo z definice použitých operací): ni = ci + bni+1 Tuto rovnost musíme zachovat, ale zároveň potřebujeme, aby ci bylo platnou číslicí. To zajistíme jednoduše tak, že od ci odečteme b a přičteme místo toho jedničku k ni+1 (tím se součet jistě nezmění, protože jsme jedno b ubrali a jedno přidali). bool is_balanced( int n, int base ); 1.p.6 [subsetsum] Vstupem pro problém subset sum je množina povolených čísel A a hledaný součet n. Řešením je pak podmnožina B ⊆ A taková, že součet jejích prvků je právě n. V tomto příkladu budeme pracovat pouze s množinami A, které obsahují kladná čísla menší nebo rovna 64, a které lze tedy reprezentovat jediným bezznaménkovým číslem z rozsahu 0 (prázdná množina) až 264 − 1 (obsahuje všechna čísla 1, 2, …, 64). Číslo 1 pak reprezentuje množinu {1}, číslo 2 množinu {2}, číslo 3 množinu {1, 2} atd. Vašim úkolem je napsat funkci subset_sum, které výsledkem bude true, má-li zadaná instance řešení. Toto řešení zároveň zapíše do výstupního parametru. V případě, že řešení neexistuje, hodnotu solution nijak neměňte. bool subset_sum( int n, std::uint64_t allowed, std::uint64_t &solution ); 1.r: Řešené úlohy 1.r.1 [bitwise] Předmětem této úlohy jsou ternární bitové operace: vstupem jsou 3 čísla a kód operace. Každý bit výsledku je určen příslušnými 3 bity operandů. Tyto 3 vstupní bity lze chápat jako pravdivostní hodnoty – potom je celkem jasné, že operaci můžeme zadat klasickou pravdivostní tabulkou, která pro každou kombinaci 3 bitů určí výsledek. Mohla by vypadat např. takto: a b c výsledek 0 0 0 r0 0 0 1 r1 0 1 0 r2 0 1 1 r3 1 0 0 r4 1 0 1 r5 1 1 0 r6 1 1 1 r7 Snadno se nahlédne, že zafixujeme-li tuto tabulku a zadáme hodnoty r0 až r7, kýžená operace bude jednoznačně určena. Protože potřebujeme 8 pravdivostních hodnot, můžeme je například předat jako jednobajtovou hodnotu typu std::uint8_t. Hodnotu r0 předáme v nejnižším a r7 v nejvyšším bitu. Operace musí fungovat pro libovolný bezznaménkový celočíselný typ. Můžete předpokládat, že hodnoty a, b, c jsou stejných typů (ale také se můžete zamyslet, jak řešit situaci, kdy stejné nejsou). auto bitwise( std::uint8_t opcode, auto a, auto b, auto c ); 1.r.2 [euler] This is a straightforward math exercise. Implement Euler’s [φ], for instance using the product formula φ(n) = nΠ(1 − 1/p) where the product is over all distinct prime divisors of n. You may need to take care to compute the result exactly. long phi( long n ); /* ref: 21 lines */ 1.r.3 [hamcode] V této úloze budeme programovat dekodér pro kód Hamming(8, 4) – jedná se o variaci běžnějšího Hamming(7, 4) s dodatečným paritním bitem. Vstupní blok je zadán osmibitovým číslem bez znaménka: 13/90 d4d3d2p3d1p2p1p0 7 6 5 4 3 2 1 0 Blok je správně utvořený, platí-li tyto vztahy: p1 = P(d1, d2, d4) p2 = P(d1, d3, d4) p3 = P(d2, d3, d4) p0 = P(p1, p2, p3, d1, d2, d3, d4) kde P značí paritu. Graficky (zvýrazněný bit je paritním pro vyznačené bity): Rozmyslete si, jaký mají uvedené vztahy dopad na paritu celých označených oblastí. Poté napište funkci h84_decode, která na vstupu dostane jeden blok, ověří správnost paritních bitů, a je-li vše v pořádku, vrátí true a do výstupního parametru out zapíše dekódovanou půlslabiku (d4 v nejnižším bitu). Jinak vrátí false a hodnotu out nemění. bool h84_decode( std::uint8_t data, std::uint8_t &out ); 1.r.5 [cellular] Napište čistou funkci, která simuluje jeden krok výpočtu jednorozměrného buněčného automatu (cellular automaton). Stav budeme reprezentovat celým číslem bez znaménka – jednotlivé buňky budou bity tohoto čísla. Stav osmibitového automatu by mohl vypadat například takto: 10010110 7 6 5 4 3 2 1 0 Pro zjednodušení použijeme pevnou sadu pravidel (+1, 0, -1 jsou relativní pozice bitů vůči tomu, který právě počítáme): +1 0 -1 nová 0 0 0 1 0 0 1 0 0 1 0 0 0 1 1 0 1 0 0 1 1 0 1 0 1 1 0 1 1 1 1 0 Pravidla určují, jakou hodnotu bude mít buňka v následujícím stavu, v závislosti na okolních buňkách stavu nynějšího. Na krajích stavu interpretujeme chybějící políčko jako nulu. Výpočet s touto sadou pravidel tedy funguje takto: 0100011 0 0 010001 0 1 0 0 01000 1 1 0 1 0 0 1 001 → 0 2 011 → 0 3 110 → 1 atd. 0 0 1 0 0 1 0 1 0 1 1 0 1 0 0 1 0 0 1 0 0 1 0 1 0 1 1 0 1 0 0 7 010 → 0 8 100 → 1 Výpočet kroku by mělo být možné provést na libovolně širokém celočíselném typu. auto cellular_step( auto w ); 14/90 PB161 Programování v jazyce C++, 16. února 2024 Část 2: Složené hodnoty V této kapitole samozřejmě pokračujeme s použitím funkcí, skalárů a referencí, a přidáváme složené hodnoty: standardní kontejnery (std::vector, std::set, std::map, std::array) a součinové (produktové) typy struct a std::tuple. Ukázky: 1. stats – záznamové typy, zjednodušený for cyklus 2. primes – vkládání prvků do hodnoty typu std::vector 3. iterate – vytvoření posloupnosti iterací funkce 4. dfs – dosažitelnost vrcholu v grafu 5. bfs † – nejkratší vzdálenost v neohodnoceném grafu Elementary exercises: 1. fibonacci – stará posloupnost, nová signatura 2. reflexive – reflexivní uzávěr zadané relace 3. unique – odstraněni duplicit ve vektoru Preparatory exercises: 1. minsum – dělení posloupnosti čísel podle součtu 2. connected – rozklad grafu na komponenty souvislosti 3. divisors – kontejner jako vstupně-výstupní parametr 4. midpoint – kontejner s prvky složeného typu 5. dag † – hledání cyklu v orientovaném grafu 6. bipartite – rozhodování o bipartitnosti grafu Regular exercises: 1. mode – nalezněte mód číselné posloupnosti 2. sssp – nejkratší cesty z pevně zvoleného vrcholu 3. solve – solver pro velmi jednoduchou hru 4. buckets – řazení kamenů do kyblíčků podle váhy 5. permute – permutace číslic 6. flood – semínkové vyplňování s vektorem 1 Hlavičkové soubory Tato kapitola přidává řadu nových povolených hlavičkových souborů: • tuple – definice N-tic std::tuple a pomocných funkcí, • vector – definice dynamického pole std::vector, • set – podobně, ale pro std::set a std::multiset, • map – umožňuje použití kontejnerů std::map, std::multimap, • deque – definuje oboustrannou frontu std::deque, • queue – definuje klasickou frontu std::queue, • stack – podobně ale zásobník std::stack, • utility – různé pomocné funkce, std::pair, • ranges – prozatím zejména std::ranges::subrange, • numeric – funkce pro práci (zejména) s číselnými sekvencemi, • cmath – funkce pro práci s čísly s plovoucí desetinnou čárkou. 2.d: Demonstrace (ukázky) 2.d.1 [stats] V této ukázce spočítáme několik jednoduchých statistických veličin – míry polohy (průměr, medián) a variance (směrodatnou odchylku). Využijeme k tomu záznamové typy a sekvenční typ std::vector. Nejprve si definujeme typ pro výsledek naší jednoduché analýzy – použijeme k tomu záznamový typ, který deklarujeme klíčovým slovem struct, názvem, a seznamem deklarací složek uzavřeným do složených závorek (a jako všechny deklarace, ukončíme i tuto středníkem): struct stats { double median = 0.0; double mean = 0.0; double stddev = 0.0; }; Tím máme definovaný nový typ s názvem stats, který můžeme dále používat jako libovolný jiný typ (zabudovaný, nebo ze standardní knihovny). Zejména můžeme vytvářet hodnoty tohoto typu, předávat je jako parametry nebo vracet jako výsledky podprogramů. V této ukázce si zadefinujeme čistou funkci compute_stats, která potřebné veličiny spočítá a vrátí je jako hodnotu typu stats. Vstupní parametr data předáme konstantní referencí: hodnoty nebudeme nijak měnit (programujeme čistou funkci a data považujeme za vstupní parametr). Zároveň nepotřebujeme vytvořit kopii vstupních dat – budeme je pouze číst, taková kopie by tedy byla celkem zbytečná a potenciálně drahá (dat, které chceme zpracovat, by mohlo být mnoho). stats compute_stats( const std::vector< double > &data ) { int n = data.size(); double sum = 0, square_error_sum = 0; stats result; Na tomto místě se nám bude hodit nový prvek řízení toku, kterému budeme říkat stručný for cyklus (angl. „range for“). Jeho účelem je procházet posloupnost hodnot uloženou v iterovatelném typu (použitelnost hodnoty ve stručném for cyklu lze chápat přímo jako definici iterovatelného typu). Do závorky uvádíme deklaraci proměnné cyklu (můžeme zde použít zástupné slovo auto) a dvojtečkou oddělený výraz. Tento výraz musí být iterovatelného typu a výsledná iterovatelná hodnota je ta, kterou budeme cyklem procházet. for ( double x_i : data ) Cyklus se provede pro každý prvek předané iterovatelné hodnoty. Tento prvek je pokaždé uložen do proměnné cyklu (která může být referencí – v takovém případě tato reference odkazuje přímo na prvek, v opačném případě se jedná o kopii). sum += x_i; K položkám hodnoty záznamového typu přistupujeme výrazem expr.field, kde: • expr je výraz záznamového typu (zejména to tedy může být název proměnné), následovaný • tečkou (technicky se jedná o operátor s vysokou prioritou), • field je jméno atributu (tzn. na pravé straně tečky nestojí výraz). Je-li výraz expr l-hodnotou, je l-hodnotou i výraz přístupu k položce jako celek a lze do něj tedy přiřadit hodnotu. result.mean = sum / n; Medián získáme dobře známým postupem. Za povšimnutí stojí indexace vektoru data zápisem indexu do hranatých závorek. Obecněji jsou-li x a i výrazy, je výraz také x[ i ] kde x je indexovatelného typu (omezení na typ i závisí na typu x). Je-li x navíc l-hodnota, je l-hodnotou i výraz x[ i ] jako celek.12 * if ( n % 2 == 1 ) result.median = data[ n / 2 ]; else result.median = ( data[ n / 2 ] + data[ n / 2 - 1 ] ) / 2; Konečně spočítáme směrodatnou odchylku. K tomu budeme potřebovat dříve vypočítaný průměr. for ( double x_i : data ) square_error_sum += std::pow( x_i - result.mean, 2 ); double variance = square_error_sum / ( n - 1 ); result.stddev = std::sqrt( variance ); return result; } 12 V některých případech je x[ i ] l-hodnotou i v případě, že x samotné l-hodnota není (opačná implikace tedy obecně neplatí). Výslednou l-hodnotu ale stejně nelze smysluplně použít. 15/90 int main() /* demo */ { std::vector< double > sample = { 2, 4, 4, 4, 5, 5, 5, 7, 9 }; auto s = compute_stats( sample ); assert( s.mean == 5 ); assert( s.median == 5 ); assert( s.stddev == 2 ); sample.push_back( 1100 ); s = compute_stats( sample ); assert( s.median == 5 ); assert( s.mean > 100 ); assert( s.stddev > 100 ); } 2.d.2 [primes] Krom jednoduchých výstupních parametrů (kterými jsme se zabývali v předchozí kapitole) lze uvažovat i o výstupních parametrech složených typů. V této ukázce naprogramujeme funkci primes, která na konec předaného objektu typu std::vector vloží všechna prvočísla ze zadaného rozsahu. O parametru out budeme mluvit jako o výstupním parametru, i když situace je zde o něco složitější: operace, které můžeme se složenou hodnotou provádět, se totiž neomezují pouze na čtení a přiřazení. Musíme tedy vědět, jak závisí chování operací, které chceme provést, na počáteční hodnotě. Například operace vložení prvku na konec vektoru bude fungovat stejně pro libovolný vektor.13 Protože žádnou jinou operaci s parametrem out provádět nebudeme, je jeho označení za výstupní parametr opodstatněné. void primes( int from, int to, std::vector< int > &out ) { for ( int candidate = from; candidate < to; ++ candidate ) { bool prime = true; int bound = std::sqrt( candidate ) + 1; Rozhodování o prvočíselnosti kandidáta provedeme naivně, zkusmým dělením. for ( int div = 2; div < bound; ++ div ) if ( div != candidate && candidate % div == 0 ) { prime = false; break; 13 Situaci, kdy je vektor „plný“ (obsahuje tolik prvků, že další nelze přidat, i kdyby to kapacita paměti umožnila) můžeme zanedbat: na 64b počítači, který skutečně existuje, nemůže nastat. } Konečně, je-li kandidát skutečně prvočíslem, vložíme ho na konec vektoru odkazovaného parametrem out (protože out je referenčního typu, out je pouze nové jméno pro původní objekt uvedený ve skutečném parametru). Novým prvkem je zde ale zejména volání metody. Syntakticky se podobá přístupu k položce (viz předchozí ukázka), ale je následováno kulatými závorkami a seznamem parametrů, stejně jako volání běžného podprogramu. Výraz před tečkou se použije jako skrytý parametr metody (ta tedy s výslednou hodnotou může pracovat – zde například volání out.push_back( x ) modifikuje objekt out). O metodách si toho povíme více v následující kapitole. if ( prime ) out.push_back( candidate ); } } int main() /* demo */ { std::vector< int > p_out; std::vector< int > p7 = { 2, 3, 5 }, p15 = { 2, 3, 5, 7, 11, 13 }; primes( 2, 7, p_out ); assert( p_out == p7 ); primes( 7, 15, p_out ); assert( p_out == p15 ); } 2.d.3 [closure] In this demo, we will check closure properties of relations: reflexivity, transitivity and symmetry. A relation is a set of pairs, and hence we will represent them as std::set of std::pair instances. We will work with relations on integers. Recall that std::set has an efficient membership test: we will be using that a lot in this program. using relation = std::set< std::pair< int, int > >; The first predicate checks reflexivity: for any x which appears in the relation, the pair (x, x) must be present. Besides membership testing, we will use structured bindings and range for loops. Notice that a braced list of values is implicitly converted to the correct type (std::pair< int, int >). bool is_reflexive( const relation &r ) { Structured bindings are written using auto, followed by square brackets with a list of names to bind to individual components of the right-hand side. In this case, the right-hand side is the loop variable – i.e. each of the elements of r in turn. for ( auto [ x, y ] : r ) { if ( !r.contains( { x, x } ) ) return false; if ( !r.contains( { y, y } ) ) return false; } We have checked all the elements of r and did not find any which would violate the required property. Return true. return true; } Another, even simpler, check is for symmetry. A relation is symmetric if for any pair (x, y) it also contains the opposite, (y, x). bool is_symmetric( const relation &r ) { for ( auto [ x, y ] : r ) if ( !r.contains( { y, x } ) ) return false; return true; } Finally, a slightly more involved example: transitivity. A relation is transitive if ∀x, y, z.(x, y) ∈ r ∧ (y, z) ∈ r → (x, z) ∈ r. bool is_transitive( const relation &r ) { for ( auto [ x, y ] : r ) for ( auto [ y_prime, z ] : r ) if ( y == y_prime && !r.contains( { x, z } ) ) return false; return true; } int main() /* demo */ { relation r_1{ { 1, 1 }, { 1, 2 } }; assert( !is_reflexive( r_1 ) ); assert( !is_symmetric( r_1 ) ); assert( is_transitive( r_1 ) ); relation r_2{ { 1, 1 }, { 1, 2 }, { 2, 2 } }; assert( is_reflexive( r_2 ) ); assert( !is_symmetric( r_2 ) ); assert( is_transitive( r_2 ) ); 16/90 PB161 Programování v jazyce C++, 16. února 2024 relation r_3{ { 2, 1 }, { 1, 2 }, { 2, 2 } }; assert( !is_reflexive( r_3 ) ); assert( is_symmetric( r_3 ) ); assert( !is_transitive( r_3 ) ); } 2.d.4 [dfs] V této ukázce se budeme zabývat prohledáváním orientovaného grafu. Asi nejjednodušším vhodným algoritmem je rekurzivní prohledávání do hloubky. Konkrétně nás bude zajímat odpověď na otázku „je vrchol b dosažitelný z vrcholu a?“. Budeme navíc požadovat, aby byla příslušná cesta neprázdná (tzn. a budeme považovat za dosažitelné z a pouze leží-li na cyklu). Vstupní graf bude zadaný za pomoci seznamů následníků: typ graph udává pro každý vrchol grafu jeho následníky. Asociativní kontejner std::map ukládá dvojice klíč-hodnota a umožňuje mimo jiné efektivně (v logaritmickém čase) nalézt hodnotu podle zadaného klíče. Všimněte si také, že množina vrcholů nemusí nutně sestávat z nepřerušené posloupnosti, nebo jen z malých čisel (proto používáme std::map a nikoliv std::vector). using edges = std::vector< int >; using graph = std::map< int, edges >; Krom samotného grafu budeme potřebovat reprezentaci pro množinu navštívených vrcholů. V grafu s cykly by algoritmus, který si takovou množinu neudržuje, vedl na nekonečnou rekurzi (nebo nekonečný cyklus). Navíc i v acyklickém grafu bude takový algoritmus vyžadovat (v nejhorším případě) exponenciální čas. Protože sémanticky se jedná o množinu, není asi velkým překvapením, že pro její reprezentaci použijeme asociativní kontejner std::set. Vyhledání prvku (resp. test na přítomnost prvku) v std::set má logaritmickou časovou složitost. Podobně tak vložení prvku. using visited = std::set< int >; Hlavní rekurzivní podprogram bude potřebovat 2 pomocné parametry: již zmiňovanou množinu navštívených vrcholů, a navíc pravdivostní hodnotu moved, která řeší případ, kdy potřebujeme zjistit, zda je vrchol dosažitelný sám ze sebe. Naivní řešení by totiž pro dvojici (a, a) vždy vrátilo true (v rozporu s naším zadáním). Proto si v tomto parametru budeme pamatovat, zda jsme se již podél nějaké hrany posunuli. Tento podprogram bude tedy odpovídat na otázku „existuje cesta, která začíná ve vrcholu from, neprochází žádným vrcholem v seen, a zároveň končí ve vrcholu to?“ Všimněte si ale, že množinu seen předáváme odkazem (referencí) – existuje pouze jediná množina navštívených vrcholů, sdílená všemi rekurzivními aktivacemi podprogramu. Jakmile tedy vrchol potkáme na libovolné cestě, bude vyloučen ze zkoumání ve všech ostatních větvích výpočtu (tedy i v těch sourozeneckých, nikoliv jen v potomcích toho současného). bool is_reachable_rec( const graph &g, int from, int to, visited &seen, bool moved ) { První bázový případ je situace, kdy jsme cílový vrchol našli – protože je velmi jednoduchý, vyřešíme jej první. Všimněte si kontroly parametru moved. if ( from == to && moved ) return true; Hlavní cyklus pokrývá zbývající případy: 1. druhý bázový případ, kdy žádný nenavštívený potomek již neexistuje (tzn. nacházíme se ve slepé větvi a výsledkem je false), a 2. případ, kdy existuje dosud nenavštívený soused – pak lze ale problém vyřešit rekurzí, protože současný vrchol jsme z problému vyloučili a zbývající problém je tedy menší. Výsledkem volání metody at je reference na hodnotu přidruženou klíči, který jsme předali v parametru. Proměnná next tedy nabývá hodnot, které odpovídají přímým následníkům vrcholu from. for ( auto next : g.at( from ) ) V případě, že jsme nalezli nenavštívený vrchol, nejprve ho označíme za navštívený a poté provedeme rekurzivní volání. Protože jsme se právě posunuli po hraně from, next, nastavujeme parametr moved na true. if ( !seen.contains( next ) ) { seen.insert( next ); if ( is_reachable_rec( g, next, to, seen, true ) ) return true; } Skončí-li cyklus jinak, než návratem z podprogramu, znamená to, že jsme vyčerpali všechny možnosti, aniž bychom našli přípustnou cestu, která vrcholy from a to spojuje. return false; } Konečně doplníme jednoduchou funkci, která doplní potřebné hodnoty pomocným parametrům. Odpovídá na otázku „lze se do vrcholu to dostat podél jedné nebo více hran, začneme-li ve vrcholu to?“. Za povšimnutí také stojí, že is_reachable je čistou funkcí (a to i přesto, že is_reachable_rec čistou funkcí není). bool is_reachable( const graph &g, int from, int to ) { visited seen; return is_reachable_rec( g, from, to, seen, false ); } int main() /* demo */ { graph g{ { 1, { 2, 3, 4 } }, { 2, { 1, 2 } }, { 3, { 3, 4 } }, { 4, {} }, { 5, { 3 }} }; assert( is_reachable( g, 1, 1 ) ); assert( !is_reachable( g, 4, 4 ) ); assert( is_reachable( g, 1, 2 ) ); assert( is_reachable( g, 1, 3 ) ); assert( is_reachable( g, 1, 4 ) ); assert( !is_reachable( g, 4, 1 ) ); assert( is_reachable( g, 3, 3 ) ); assert( !is_reachable( g, 3, 1 ) ); assert( is_reachable( g, 5, 4 ) ); assert( !is_reachable( g, 5, 1 ) ); assert( !is_reachable( g, 5, 2 ) ); } 2.d.5 [bfs] † The goal of this demonstration will be to find the shortest distance in an unweighted, directed graph: 1. starting from a fixed (given) vertex, 2. to the nearest vertex with an odd value. The canonical ‘shortest path’ algorithm in this setting is breadth-first search. The algorithm makes use of two data structures: a queue and a set, which we will represent using the standard C++ containers named, appropriately, std::queue14 and std::set. In the previous demonstration, we have represented the graph explicitly using adjacency list encoded as instances of std::vector. Here, we will take a slightly different approach: we well use std::multimap – as the name suggests, it is related to std::map with one crucial difference: it can associate multiple values to each key. Which is exactly what we need 14 Strictly speaking, std::queue is not a container, but rather a container adaptor. Internally, unless specified otherwise, an std::queue uses another container, std::deque to store the data and implement the operations. It would also be possible, though less convenient, to use std::deque directly. 17/90 to represent an directed graph – the values associated with each key will be the successors of the vertex given by the key. using graph = std::multimap< int, int >; The algorithm consists of a single function, distance_to_odd, which takes the graph g, as a constant reference, and the vertex initial, as arguments. It then returns the sought distance, or if no matching vertex is found, -1. int distance_to_odd( const graph &g, int initial ) { We start by declaring the visited set, which prevents the algorithm from getting stuck in an infinite loop, should it encounter a cycle in the input graph (and also helps to keep the time complexity under control). std::set< int > visited; The next piece of the algorithm is the exploration queue: we will put two pieces of information into the queue: first, the vertex to be explored, second, its BFS distance from initial. std::queue< std::pair< int, int > > queue; To kickstart the exploration, we place the initial vertex, along with distance 0, into the queue: queue.emplace( initial, 0 ); Follows the standard BFS loop: while ( !queue.empty() ) { auto [ vertex, distance ] = queue.front(); queue.pop(); To iterate all the successors of a vertex in an std::multimap, we will use its equal_range method, which will return a pair of iterators – generalized pointers, which support a kind of ‘pointer arithmetic’. The important part is that an iterator can be incremented using the ++ operator to get the next element in a sequence, and dereferenced using the unary * operator to get the pointed-to element. The result of equal_range is a pair of iterators: • begin, which points at the first matching key-value pair in the multimap, • end, which points one past the last matching element; clearly, if begin == end, the sequence is empty. Incrementing begin will eventually cause it to become equal to end, at which point we have reached the end of the sequence. Let’s try: auto [ begin, end ] = g.equal_range( vertex ); for ( ; begin != end; ++ begin ) { In the body loop, begin points, in turn, at each matching key-value pair in the graph. To get the corresponding value (which is what we care about), we extract the second element: auto [ _, next ] = *begin; if ( visited.contains( next ) ) continue; /* skip already-visited vertices */ First, let us check whether we have found the vertex we were looking for: if ( next % 2 == 1 ) return distance + 1; Otherwise we mark the vertex as visited and put it into the queue, continuing the search. visited.insert( next ); queue.emplace( next, distance + 1 ); } } We have exhausted the queue, and hence all the vertices reachable from initial, without finding an odd-valued one. Indicate failure to the caller. return -1; } int main() /* demo */ { graph g{ { 1, 2 }, { 1, 6 }, { 2, 4 }, { 2, 5 }, { 6, 4 } }, h{ { 8, 2 }, { 8, 6 }, { 2, 4 }, { 2, 5 }, { 5, 8 } }, i{ { 2, 4 }, { 4, 2 } }; assert( distance_to_odd( g, 1 ) == 2 ); assert( distance_to_odd( g, 2 ) == 1 ); assert( distance_to_odd( g, 6 ) == -1 ); assert( distance_to_odd( h, 8 ) == 2 ); assert( distance_to_odd( h, 5 ) == 3 ); assert( distance_to_odd( i, 2 ) == -1 ); } 2.e: Elementární příklady 2.e.1 [fibonacci] Fill in an existing vector with a Fibonacci sequence (i.e. after calling fibonacci( v, n ), the vector v should contain the first n Fibonacci numbers, and nothing else). // void fibonacci( … ) 2.e.2 [reflexive] Build a reflexive closure of a relation given as a set of pairs, returning the result. using relation = std::set< std::pair< int, int > >; relation reflexive( const relation &r ); 2.e.3 [unique] Filter out duplicate entries from a vector, maintaining the relative order of entries. Return the result as a new vector. std::vector< int > unique( const std::vector< int > & ); 2.p: Přípravy 2.p.1 [minsum] Na vstupu dostanete posloupnost celočíselných hodnot (jako instanci kontejneru std::vector). Vaším úkolem je rozdělit je na kratší posloupnosti tak, že každá posloupnost je nejkratší možná, ale zároveň je její součet alespoň sum. Výjimku tvoří poslední posloupnost, pro kterou nemusí nutně existovat potřebné prvky. Pořadí prvků musí být zachováno, tzn. zřetězením všech posloupností na výstupu musí vzniknout původní posloupnost numbers. auto minsum( const std::vector< int > &numbers, int sum ); 2.p.2 [connected] Rozložte zadaný neorientovaný graf na souvislé komponenty (výsledné komponenty budou reprezentované množinou svých vrcholů). Graf je zadaný jako symetrická matice sousednosti. Vrcholy jsou očíslované od 1 do n, kde n je velikost vstupní matice. V grafu je hrana {u, v} přítomna právě tehdy, je-li na řádku u ve sloupci v hodnota true. using graph = std::vector< std::vector< bool > >; using component = std::set< int >; using components = std::set< component >; components decompose( const graph &g ); 2.p.3 [divisors] Nalezněte všechny prvočíselné dělitele čísla num a vložte je do vektoru divs. Počáteční hodnota parametru divs: • obsahuje právě všechny prvočíselné dělitele všech čísel ostře menších než num, • je vzestupně seřazená. Výstupní podmínkou pro vektor divs je: • obsahuje všechna čísla, která obsahoval na vstupu, • zároveň obsahuje všechny prvočíselné dělitele čísla num, • je vzestupně seřazený. 18/90 PB161 Programování v jazyce C++, 16. února 2024 Funkce musí pracovat efektivně. Určit vhodnou časovou složitost je v této úloze součástí zadání. void add_divisors( int num, std::vector< int > &divs ); 2.p.4 [midpoints] Strukturu point doplňte tak, aby měla složky x a y, kde obojí jsou čísla s plovoucí desetinnou čárkou, a to tak, že deklarace point p; vytvoří bod se souřadnicemi 0, 0. struct point; Nyní uvažme uzavřenou lomenou čáru. Nahraďte každou úsečku A takovou, která začíná prostředním bodem úsečky A a končí prostředbním bodem úsečky B, kde B v obrazci následuje bezprostředně po A. Vstup je zadán jako sekvence bodů (kde každý bod náleží dvěma úsečkám). Poslední úsečka jde z posledního bodu do prvního, čím se obrazec uzavře. void midpoints( std::vector< point > &pts ); 2.p.5 [dag] † Budeme opět pracovat s orientovaným grafem – tentokrát budeme hledat cykly. Existuje na výběr několik algoritmů, ty založené na prohledávání do hloubky jsou nejjednodušší. Graf je zadaný jako hodnota typu std::multimap – více se o této reprezentaci dozvíte v ukázce d5_bfs. Čistá funkce is_dag nechť vrátí false právě když g obsahuje cyklus. Pozor, graf nemusí být souvislý. using graph = std::multimap< int, int >; bool is_dag( const graph &g ); 2.p.6 [bipartite] Rozhodněte, zda je zadaný neorientovaný graf bipartitní (tzn. jeho vrcholy lze rozdělit do dvou množin A, B tak, že každá hrana má jeden vrchol v A a jeden v B). Protože graf je neorientovaný, seznamy sousedů na vstupu jsou symetrické. using edges = std::vector< int >; using graph = std::map< int, edges >; bool is_bipartite( const graph &g ); 2.r: Řešené úlohy 2.r.1 [mode] Find the mode (most common value) in a non-empty vector and return it. If there are more than one, return the smallest. int mode( const std::vector< int > & ); 2.r.2 [sssp] Compute single-source shortest path distances for all vertices in an unweighted directed graph. The graph is given using adjacency (successor) lists. The result is a map from a vertex to its shortest distance from initial. Vertices which are not reachable from initial should not appear in the result. using edges = std::vector< int >; using graph = std::map< int, edges >; std::map< int, int > shortest( const graph &g, int initial ); 2.r.3 [solve] Consider a single-player game that takes place on a 1D playing field like this: 0…2……42 The player starts at the leftmost cell and in each round can decide whether to jump left or right. The playing field is given by the input vector jumps. The size of the field is jumps.size() + 1 (the rightmost cell is always 0). The objective is to visit each cell exactly once. bool solve( std::vector< int > jumps ); 2.r.4 [buckets] Sort stones into buckets, where each bucket covers a range of weights; the range of stone weights to put in each bucket is given in an argument – a vector with one element per bucket, each element a pair of min/max values (inclusive). Assume the bucket ranges do not overlap. Stones are given as a vector of weights. Throw away stones which do not fall into any bucket. Return the weights of individual buckets. using bucket = std::pair< int, int >; std::vector< int > sort( const std::vector< int > &stones, const std::vector< bucket > &buckets ); 2.r.5 [colour] Write a function to decide whether a given graph can be 3-colored. A correct colouring is an assignment of colours to vertices such that no edge connects vertices with the same colour. The graph is given as a set of edges. Edges are represented as pairs; assume that if (u, v) is a part of the graph, so is (v, u). using graph = std::set< std::pair< int, int > >; bool has_3colouring( const graph &g ); 2.r.6 [flood] V tomto cvičení implementujeme tzv. semínkové vyplňování, obvykle popsané algoritmem, který: 1. dostane na vstupu bitmapu (odélníkovou síť pixelů), 2. počátečnou pozici v síti, 3. barvu výplně (cílovou barvu), a změní celou souvislou jednobarevnou plochu, která obsahuje počáteční pozici, na cílovou barvu. Budeme uvažovat monochromatický případ – pixely jsou černé nebo bílé, resp. 0 nebo 1. Navíc nebudeme měnit vstupní bitmapu, ale pouze spočítáme, kolik políček změní barvu a tuto hodnotu vrátíme. Příklad (prázdná políčka mají hodnotu 0, vybarvená hodnotu 1, startovní pozice má souřadnice 1, 3): × × 0 1 2 3 4 5 0 1 2 3 4 5 0 1 2 3 4 5 Všimněte si, že „záplava“ se šíří i po diagonálách (např. z pozice (2, 3) na (3, 4) a dále na (4, 3)). Dále: • vstupní bitmapa je zadaná jako jednorozměrný vektor a šířka, • chybí-li nějaké pixely v posledním řádku, uvažujte jejich hodnotu nulo- vou, • je-li poslední řádek kompletní, nic nepřidávejte, • je-li startovní pozice, x0 nebo y0, mimo meze bitmapy (tzn. její šířku a výšku), žádné pixely barvu nezmění. Poslední parametr, fill, udává cílovou barvu. Je-li startovní pozice cílové barvy, podobně se nic nebude měnit. using grid = std::vector< bool >; int flood( const grid &pixels, int width, int x0, int y0, bool fill ); 19/90 Část 3: Metody a operátory Ukázky: 1. freq – analýza frekvence n-gramů 2. lemmings – modelujeme figurky z počítačové hry 3. arithmetic – přetěžování aritmetických operátorů 4. relational – přetěžování relačních operátorů Elementární příklady: 2. cartesian – komplexní čísla Přípravy: 1. area – výpočet plochy jednoduchých útvarů 2. rational – racionální čísla (zlomky) 3. mountains – „rekurzivní“ pohoří 4. polar – komplexní čísla podruhé 5. numset – mírně vylepšená množina čísel 6. continued – řetězové zlomky Regular exercises: 1. poly – polynomy se sčítáním a násobením 2. xxx 3. set – množina čísel s operátory 4. xxx 5. xxx 6. xxx 3.d: Demonstrace (ukázky) 3.d.1 [freq] V této ukáce budeme počítat histogram (číselných) n-gramů, tzn. bloků n po sobě jdoucích čísel v nějaké delší sekvenci. Jednotlivé n-gramy se mohou překrývat (n-gram tedy může začínat na libovolném offsetu). Naším úkolem je navrhnout typ, který bude tuto frekvenci počítat „za běhu“ – počítáme totiž s tím, že vstupních dat bude hodně a budou přicházet po blocích. Zároveň předpokládáme, že různých n-gramů bude řádově méně než vstupních dat. Budeme implementovat dvě metody: 1. count, která pro zadaný n-gram vrátí počet jeho dosavadních výskytů, a metodu 2. process, která zpracuje další blok dat. struct freq { std::size_t ngram_size = 3; Budeme potřebovat dvě datové složky: samotné počítadlo výskytů implementujeme pomocí standardního asociativního kontejneru std::map. Klíčem bude std::vector potřebné délky (reprezentuje n-gram), hodnotou pak počet výskytů tohoto n-gramu. std::map< std::vector< int >, int > _counter; Druhou složkou bude posuvné okno, ve kterém budeme uchovávat poslední zpracovaný n-gram. Je to proto, že některé n-gramy budou rozděleny mezi dva bloky (nebo i více, pokud se objeví velmi krátký blok). Pro jednoduchost budeme toto okno používat pro všechny n-gramy, a realizovat ho budeme jako instanci std::deque15 . std::deque< int > _window; Nejprve implementujeme pomocnou metodu, která zpracuje jedno číslo. Jeli okno již plné, odstraníme z něj nejstarší hodnotu. Je-li po vložení čísla okno dostatečně plné, výsledný n-gram započítáme. Vzpomeňte si, že indexovací operátor kontejneru std::map podle potřeby vloží novou dvojici klíč-hodnota (s hodnotou inicializovanou „na nulu“). void add( int value ) { if ( _window.size() == ngram_size ) _window.pop_front(); _window.push_back( value ); if ( _window.size() == ngram_size ) { std::vector< int > ngram; for ( int v : _window ) ngram.push_back( v ); ++ _counter[ ngram ]; } } Protože metoda add kompletně řeší jak správu okna, tak počítadlo, zpracování bloku už je jednoduché. void process( const std::vector< int > &block ) { 15 Tato volba reprezentace není úplně nejefektivnější, ale pro naše účely dostatečná. Asymptoticky jí není co vytknout. for ( int value : block ) add( value ); } Metodu count, která pouze vrací informace a aktuální objekt nijak nemění, bychom rádi označili jako const. Jako drobný problém se jeví, že indexace položky _counter ale není konstantní operace: jak jsme zmiňovali, operátor indexace může do kontejneru vložit novou dvojici, a tím ho změnit. Nemůžeme také přímo použít metodu at, protože musíme být schopni správně odpovídat i v případě, že dotazovaný n-gram se na vstupu dosud neobjevil, a tedy takový klíč v kontejneru _counter není přítomen. Zbývá tedy metoda find, která nám dá jak informaci o tom, jestli je klíč přítomen (hledání vyžaduje logaritmický čas), a pokud ano, tak nám k němu přímo umožní přístup (již v konstantním čase). Použití s inicializační sekcí podmíněného příkazu if sze považovat za idiomatické. int count( const std::vector< int > &ngram ) const { if ( auto it = _counter.find( ngram ); it != _counter.end() ) return it->second; else return 0; } }; int main() /* demo */ { freq f{ .ngram_size = 3 }; Vytvoříme si na f také konstantní referenci, abychom se ujistili, že metodu count skutečně lze volat na konstantní hodnotu. const freq &cf = f; assert( cf.count( { 1, 1, 1 } ) == 0 ); f.process( { 1, 1, 2, 1, 1 } ); assert( cf.count( { 1, 1, 1 } ) == 0 ); assert( cf.count( { 1, 1, 2 } ) == 1 ); assert( cf.count( { 1, 2, 1 } ) == 1 ); f.process( { 1 } ); assert( cf.count( { 1, 1, 1 } ) == 1 ); assert( cf.count( { 1, 2, 1 } ) == 1 ); f.process( { 1 } ); f.process( { 2, 2 } ); 20/90 PB161 Programování v jazyce C++, 16. února 2024 assert( cf.count( { 1, 1, 1 } ) == 2 ); assert( cf.count( { 1, 2, 2 } ) == 1 ); assert( cf.count( { 2, 2, 1 } ) == 0 ); } 3.d.2 [lemmings] While we are talking about computer games, you might have heard about a game called Lemmings (but it’s not super important if you didn’t). In each level of the game, lemmings start spawning at a designated location, and immediately start to wander about, fall off cliffs, drown and generally get hurt. The player is in charge of saving them (or rather as many as possible), by giving them tasks like digging tunnels, or stopping and redirecting other lemmings. Let’s try to design a type which will capture the state of a single lemming: struct lemming { Each lemming is located somewhere on the map: coordinates would be a good way to describe this. For simplicity, let’s say the designated spawning spot is at coordinates (0, 0). double _x = 0, _y = 0; Unless they hit an obstacle, lemmings simply walk in a given direction – this is another candidate for an attribute; and being rather heedless, it’s probably good idea to keep track of whether they are still alive. bool _facing_right = true; bool _alive = true; Finally, they might be assigned a task, which they will immediately start performing. The exact meaning of the number is not very important. int _task = 0; Let us define a few (mostly self-explanatory) methods: void start_digging() { _task = 1; } bool busy() const { return _task != 0; } bool alive() const { return _alive; } void step() { _x += _facing_right ? 1 : -1; _y += 0; // TODO gravity, terrain, … } }; Earlier, we have mentioned that user-defined types are essentially the same as built-in types – their values can be stored in variables, passed to and from functions and so on. There are more ways in which this is true: for instance, we can construct collections of such values. Earlier, we have seen a sequence of integers, the type of which was std::vector< int >. We can create a vector of lemmings just as easily: as an std::vector< lemming >. Let us try: int count_busy( const std::vector< lemming > &lemmings ) { Note that the vector is marked const (because it is passed into the function as a constant reference). That extends to the items of the vector: the individual lemmings are also const. We are not allowed to call non-const methods, or assign into their attributes here. For instance, calling lemmings[ 0 ].start_digging() would be a compile error. int count = 0; Of course we can iterate a vector of lemmings like any other vector, and call methods on the individual lemmings (inside the vector, since we are using a reference). for ( const lemming &l : lemmings ) if ( l.busy() ) count ++; return count; } int main() /* demo */ { We first create an (empty) vector, then fill it in with 7 lemmings. std::vector< lemming > lemmings; lemmings.resize( 7 ); We can call methods on the lemmings as usual, by indexing the vector: lemmings[ 0 ].start_digging(); assert( count_busy( lemmings ) == 1 ); We can also modify the lemmings in a range for loop – notice the absence of const; this time, we use a mutable reference – the lemmings are modified in place inside the container. for ( lemming &l : lemmings ) { assert( l.alive() ); l.start_digging(); } assert( count_busy( lemmings ) == 7 ); } 3.d.3 [arithmetic] Operator overloading allows instances of classes to behave more like built-in types: it makes it possible for values of custom types to appear in expressions, as operands. Before we look at examples of how this looks, we need to define a class with some overloaded operators. For binary operators, it is customary to define them using a ‘friends trick’, which allows us to define a top-level function inside a class. As a very simple example, we will implement a class which represents integral values modulo 7 (this happens to be a finite field, with addition and multiplication). struct gf7 { int value; We can name a constant by wrapping it in a method. There are other ways to achieve the same effect, but we don’t currently have the necessary pieces of syntax. int modulus() const { return 7; } A helper method to normalize an integer. We would really prefer to enforce the normalization (such that all values of type gf7 would have their value field in the range ⟨0, 7), but we currently do not have the mechanisms to do that either. This will improve in the next chapter. gf7 normalize() const { return { value % modulus() }; } This is the ‘friend trick’ syntax for writing operators, and for binary operators, it is often the preferred one (because of its symmetry). The function is not really a part of the compound type in this case – the trick is that we can write it here anyway. The implementation relies on the simple fact that [a]7 + [b]7 = [a + b]7. friend gf7 operator+( gf7 a, gf7 b ) { return gf7{ a.value + b.value }.normalize(); } For multiplication, we will use the more ‘orthodox‘ syntax, where the operator is a const method: the left operand is passed into the operator as this, the right operand is the argument. In general, operators-as-methods have one explicit argument less (unary operators take 0 arguments, binary take 1 argument). Note that normally, you would use the same form for all symmetric operators for any given type – we mix them here to highlight the difference. We again use the fact that [a]7 ⋅ [b]7 = [a ⋅ b]7. gf7 operator*( gf7 b ) const { return gf7{ value * b.value }.normalize(); 21/90 } Values of type gf7 cannot be directly compared (we did not define the required operators) – instead, we provide this method to convert instances of gf7 back into int’s. int to_int() const { return value % modulus(); } }; Operators can be also overloaded using ‘normal’ top-level functions, like this unary minus (which finds the additive inverse of the given element). gf7 operator-( gf7 x ) { return gf7{ 7 - x.to_int() }; } Now that we have defined the class and the operators, we can look at how is the result used. int main() /* demo */ { gf7 a{ 3 }, b{ 4 }, c{ 0 }, d{ 5 }; Values a, b and so forth can be now directly used in arithmetic expressions, just as we wanted. gf7 x = a + b; gf7 y = a * b; Let us check that the operations work as expected: assert( x.to_int() == c.to_int() ); /* [3]₇ + [4]₇ = [0]₇ */ assert( y.to_int() == d.to_int() ); /* [3]₇ * [4]₇ = [5]₇ */ assert( (-a + a).to_int() == c.to_int() ); /* unary minus */ } 3.d.4 [relational] In this example, we will show relational operators, which are very similar to the arithmetic operators from previous example, except for their return types, which are bool values. The example which we will use in this case are sets of small natural numbers (1-64) with inclusion as the order. We will use three-way comparison to obtain most of the comparison operators ‘for free’. NB. Standard ordered containers like std::set and std::map require the operator less-than to define a strict weak ordering (which is corresponds to a total order). The comparison operators in this example do not define a total order (some sets are incomparable). struct set { Each bit of the below number indicates the presence of the corresponding integer (the index of that bit) in the set. uint64_t bits = 0; We also define a few methods to add and remove numbers from the set, to test for presence of a number and an emptiness check. void add( int i ) { bits |= 1ul << i; } void del( int i ) { bits &= ~( 1ul << i ); } bool has( int i ) const { return bits & ( 1ul << i ); } bool empty() const { return !bits; } We will use the method syntax here, because it is slightly shorter. We start with (in)equality, which is very simple (the sets are equal when they have the same members). Note that defining a separate operator != is not required in C++20. bool operator==( set b ) const { return bits == b.bits; } It will be quite useful to have set difference to implement the comparisons below, so let us also define that: set operator-( set b ) const { return { bits & ~b.bits }; } Since the non-strict comparison (ordering) operators are easier to implement, we will do that first. Set b is a superset of set a if all elements of a are also present in b, which is the same as the difference a - b being empty. We will write a single comparison operator, then use it to implement three-way comparison, which the compiler will then use to derive all the remaining comparison operators. bool operator<=( set b ) const { return ( *this - b ).empty(); } In addition to getting all other comparisons for free, the three-way comparison also allows us to declare the properties of the ordering. friend std::partial_ordering operator<=>( set a, set b ) { if ( a == b ) return std::partial_ordering::equivalent; if ( a <= b ) return std::partial_ordering::less; if ( b <= a ) return std::partial_ordering::greater; return std::partial_ordering::unordered; } }; int main() /* demo */ { set a; a.add( 1 ); a.add( 7 ); a.add( 13 ); set b; b.add( 1 ); b.add( 6 ); b.add( 13 ); In each pair of assertions below, the two expressions are not quite equivalent. Do you understand why? assert( a != b ); assert( !( a == b ) ); assert( a == a ); assert( !( a != a ) ); The two sets are incomparable, i.e. neither is less than the other, but as shown above they are not equal either. assert( !( a < b ) ); assert( !( b < a ) ); a.add( 6 ); // let's make ‹a› a superset of ‹b› And check that the ordering operators work on ordered sets. assert( a > b ); assert( a >= b ); assert( a != b ); assert( b < a ); assert( b <= a ); assert( b != a ); b.add( 7 ); /* let's make the sets equal */ assert( a == b ); assert( a <= b ); assert( a >= b ); } 3.e: Elementární příklady 3.e.2 [cartesian] V tomto příkladu implementujeme typ cartesian, který reprezentuje komplexní číslo pomocí reálné a imaginární části. Takto realizovaná čísla umožníme sčítat, odečítat, získat číslo opačné (unárním mínus) a určit jejich rovnost (zamyslete se, má-li smysl definovat na tomto typu uspořádání; proč ano, proč ne?). struct cartesian; Implementujte také čistou funkci make_cartesian, která vytvoří hodnotu typu cartesian se zadané reálné a imaginární složky. cartesian make_cartesian( double, double ); 3.p: Přípravy 3.p.1 [area] Doplňte definice typů point, polygon a circle tak, abyste pak mohli s jejich pomocí možné implementovat tyto čisté funkce: • make_polygon, která přijme jako parametr celé číslo (počet stran) a dále: ∘ 2 body (střed a některý vrchol), nebo ∘ 1 bod (střed) a 1 reálné číslo (poloměr opsané kružnice), • make_circle které přijme jako parametry: ∘ 2 body (střed a bod na kružnici), nebo ∘ 1 bod a 1 reálné číslo (střed a poloměr), • area, které přijme polygon nebo circle a vrátí plochu odpovídajícího 22/90 PB161 Programování v jazyce C++, 16. února 2024 útvaru. Typ point nechť má složky x a y (reálná čísla). struct point; struct polygon; struct circle; 3.p.2 [rational] V tomto příkladu budeme programovat jednoduchá racionální čísla (taková, že je lze reprezentovat dvojicí celých čísel typu int). Hodnoty typu rat lze: • vytvořit čistou funkcí make_rat( p, q ) kde p, q jsou hodnoty typu int (čitatel a jmenovatel) a q > 0, • použít jako operandy základních aritmetických operací: ∘ sčítání +, ∘ odečítání (-), ∘ násobení (*) a ∘ dělení (/), • libovolně srovnávat (==, !=, <=, <, >, >=). Vzpomeňte si, jak se jednotlivé operace nad racionálními čísly zavádí. Jsouli a = p1/q1 a b = p2/q2 zlomky v základním tvaru, můžete se u libovolné operace a ⋄ b spolehnout, že žádný ze součinů p1 ⋅ q2, p2 ⋅ q1, p1 ⋅ p2 a q1 ⋅ q2 nepřeteče rozsah int-u. struct rat; rat make_rat( int, int ); 3.p.3 [mountains] Vaším úkolem je vytvořit typ mountain_range, který bude reprezentovat rekurzivní pohoří. Rekurzivní pohoří má tento tvar: 1. levý svah (může být prázdný), který může na každém kroku libovolně stoupat, 2. libovolný počet (i nula) vnitřních pohoří stejného typu (první z nich začíná ve výšce, na které skončil levý svah), 3. pravý svah, který je zrcadlovým obrazem toho levého. Například (hlavní pohoří má prázdný svah; závorky naznačují začátky a konce jednotlivých vnitřních pohoří): 1 2 4 4 2 3 3 1 1 2 3 3 2 3 4 4 3 1 ( ( ) ) ( ( ) ( ) ) Je-li outer hodnota typu mountain_range, nechť: 1. outer.get( i ) vrátí výšku i-tého pole pohoří outer, a 2. outer.set_slope( slope ) pro zadaný vektor čísel slope nastaví oba svahy tak, aby ten levý odpovídal výškám v slope, 3. outer.insert( inner ) vloží nové vnitřní pohoří zadané hodnotou inner typu mountain_range, a to těsně před pravý svah. Dobře si rozmyslete vhodnou reprezentaci. Požadujeme: • metoda get musí mít konstantní složitost, • metoda set_slope může být vůči argumentu lineární, ale nesmí záviset na délce vnitřních pohoří, • metoda insert může být vůči vkládanému pohoří (inner) lineární, vůči tomu vnějšímu (outer) ale musí být amortizovaně konstantní. Nově vytvořená hodnota typu mountain_range reprezentuje prázdné pohoří (prázdný svah a žádná vnitřní pohoří). struct mountain_range; 3.p.4 [polar] Vaším úkolem je implementovat typ polar, který realizuje polární reprezentaci komplexního čísla. Protože tato podoba zjednodušuje násobení a dělení, implementujeme v této úloze právě tyto operace (sčítání jsme definovali v příkladu e2_cartesian). Krom násobení a dělení nechť je možné pro hodnoty typu polar určit jejich rovnost operátory == a !=. Rovnost implementujte s ohledem na nepřesnost aritmetiky s plovoucí desetinnou čárkou. V tomto příkladě můžete pro reálná čísla (typu double) místo x == y použít std::fabs( x - y ) < 1e-10. Pozor! Argument komplexního čísla je periodický: buďto jej normalizujte tak, aby ležel v intervalu [0, 2π), nebo zajistěte, aby platilo polar( 1, x ) == polar( 1, x + 2π ). struct polar; polar make_polar( double, double ); 3.p.5 [numset] Navrhněte typ numset, kterého hodnoty budou reprezentovat množiny čísel. Jsou-li ns1, ns2 hodnoty typu numset a dále i, j jsou hodnota typu int, požadujeme následující operace: • ns1.add( i ) – vloží do ns1 číslo i, • ns1.del( i ) – odstraní z ns1 číslo i, • ns1.del_range( i, j ) – odstraní z ns1 všechna čísla, která spadají do uzavřeného intervalu ⟨i, j⟩, • ns1.merge( ns2 ) – přidá do ns1 všechna čísla přítomná v ns2, • ns1.has( i ) – rozhodne, zda je i přítomné v ns1. Složitost: • del_range a merge musí mít nejvýše lineární složitost, • ostatní operace nejvýše logaritmickou. struct numset; 3.p.6 [continued] Předmětem tohoto příkladu jsou tzv. řetězové zlomky. Typ fraction bude reprezentovat racionální číslo, které lze: • zadat posloupností koeficientů řetězového zlomku (přesněji popsáno níže) pomocí metody set_coefficients( cv ) kde cv je vektor hodnot typu int, • sčítat (operátorem +), • násobit (operátorem *), • srovnávat (všemi relačními operátory, tzn. ==, !=, <, <=, >, >=). Řetězový zlomek reprezentujej racionální číslo q0 jako součet celého čísla a0 a převrácené hodnoty nějakého dalšího racionálního čísla, q1, které je samo zapsáno pomocí řetězového zlomku. Tedy q0 = a0 + 1/q1 q1 = a1 + 1/q2 q2 = a2 + 1/q3 a tak dále, až než je nějaké qi celé číslo, kterým sekvence končí (pro racionální číslo se to jistě stane po konečném počtu kroků). Hodnotám a0, a1, a2, … říkáme koeficienty řetězového zlomku – jeho hodnota je jimi jednoznačně určena. Rozmyslete si vhodnou reprezentaci vzhledem k zadanému rozhraní. Je důležité jak to, které operace jsou požadované, tak to, které nejsou. struct fraction; 3.r: Řešené úlohy 3.r.1 [poly] Cílem cvičení je naprogramovat typ, který bude reprezentovat polynomy s celočíselnými koeficienty, s operacemi sčítání (jednoduché) a násobení (méně jednoduché). Polynom je výraz tvaru 7x4 +0x3 +0x2 +3x+x0 – tzn. součet nezáporných celočíseslných mocnin proměnné x, kde u každé mocniny stojí pevný (konstantní) koeficient. Součet polynomů má u každé mocniny součet koeficientů příslušné mocniny sčítanců. Součin je složitější, protože: • každý monom (sčítanec) prvního polynomu musí být vynásoben každým monomem druhého polynomu, • některé z těchto součinů vedou na stejnou mocninu x a tedy jejich koeficienty musí být sečteny. Pro každý polynom existuje nějaké n takové, že všechny mocniny větší 23/90 než n mají nulový koeficient. Tento fakt nám umožní polynomy snadno reprezentovat. Implicitně sestrojená hodnota typu poly nechť má všechny koeficienty nulové. Krom sčítání a násobení (formou operátorů) implementujte také rovnost a metodu set, která má dva parametry: mocninu x a koeficient, obojí celá čísla. struct poly; 3.r.2 [qsort] Implementujte typ array, který reprezentuje pole čísel a bude mít metody: • get( i ) – vrátí hodnotu na indexu i, • append( x ) – vloží hodnotu x na konec pole, • partition( p, l, r ) – přeuspořádá část pole v rozsahu indexů ⟨l, r) tak, aby hodnoty menší než p předcházely hodnotě rovné p a tato předcházela zbývající hodnoty větší nebo rovné p (nejsou-li l a r uvedeny, přeuspořádá celé pole; vstupní podmínkou je, že p je v uvedeném rozsahu přítomno), • sort() – seřadí pole metodou quicksort (bez dodatečné paměti mimo místa na zásobníku potřebného pro rekurzi). Algoritmus quicksort pracuje takto: 1. má-li pole žádné nebo 1 prvek, je již seřazené: konec; 2. jinak jeden z prvků vybereme jako pivot, 3. přeuspořádáme pole na dvě menší partice (viz popis metody partition výše), 4. rekurzivně aplikuje algoritmus quicksort na levou a pravou partici (vynechá přitom hodnoty rovné pivotu). Užitečný invariant: po každé partici jsou prvky rovné vybranému pivotu na pozicích, které budou mít ve výsledném uspořádaném poli. Viz též: https://xkcd.com/1185/ struct array; 3.r.3 [ttt] Naprogramujte typ tictactoe, který bude reprezentovat stav této jednoduché hry (piškvorky na ploše 3×3). Stav hry má tyto složky: • který hráč je na tahu, • který hráč zabral která políčka. V nově vytvořené hře je plocha prázdná a na tahu je hrač s křížky. Metody: • play( x, y ) umístí symbol aktivního hráče na souřadnice x, y (platnost souřadnic i tahu je vstupní podmínkou, roh má souřadnice 0, 0), • read( x, y ) vrátí hodnotu zadaného pole: ∘ -1 je křížek, ∘ 0 je prázdné pole, konečně ∘ 1 je kolečko, • winner() vrátí podobně -1/0/1 podle toho, který hráč vyhrál (0 značí, že hra buď ještě neskončila, nebo skončila remízou). struct tictactoe; 3.r.4 [flight] Vaší úlohou je naprogramovat jednoduchý simulátor hry, kde hráč ovládá létající objekt („loď“) v bočním pohledu. Cílem hráče je nenarazit do hranic „jeskyně“ ve které se pohybuje, a která je zadaná jako seznam dvojic, kde čísla na indexu i udávají vždy souřadnice y spodní a horní meze příslušného sloupce – pole – v rozsahu souřadnice x ∈ ⟨i, i + 1). Například: Loď lze ovládat nastavením stoupání c (pro každý posun o l jednotek doprava se loď zároveň posune c ⋅ l jednotek nahoru; je-li c záporné, posouvá se dolů). Hra má tyto 4 metody: • append( y1, y2 ) přidá na pravý konec hracího pole novou dvojici překážek (zadanou čísly s plovoucí desetinnou čárkou), • move( l ) posune loď o l jednotek doprava (l je celé číslo; při posunu dojde také k příslušné změně výšky podle aktuálního nastavení) a vrátí true v případě, že při tomto posunu nedošlo ke kolizi, • set_climb( c ) nastaví aktuální stoupání na c (číslo s plovoucí desetinnou čárkou), • finished() vrátí true nachází-li se loď na pravém konci hracího pole. V případě, že dojde k pokusu o posun lodě za konec pole, loď zůstane na posledním definovaném poli. Dojde-li ke kolizi, další volání move již nemají žádný efekt a vrací false. Implicitně sestrojený stav hry má hrací pole délky 1 s překážkami (−10, +10) a počáteční výška i stoupání lodě jsou 0. struct flight; 3.r.5 [qfield] Uvažme těleso ℚ a číslo j takové, že j2 = 2 (tedy zejména j ∉ ℚ). Těleso ℚ můžeme rozšířit na tzv. (algebraické) číselné těleso (v tomto případě konkrétně kvadratické těleso) ℚ(j), kterého prvky jsou tvaru a + bj, kde a, b ∈ ℚ. Vaším úkolem je naprogramovat typ, který prvky tohoto tělesa reprezentuje, a umožňuje je sčítat, násobit a rozhodovat jejich rovnost. Rozmyslete si vhodnou reprezentaci; uvažte zejména jak bude vypadat výsledek násobení (a + bj) ⋅ (x + yj). struct qf; Protože k dispozici máme pouze celá čísla, k zápisu jednoho prvku ℚ(j) budeme potřebovat 4 (vystačili bychom si se třemi? pokud ano, jaké to má výhody a nevýhody?). qf make_qf( int a_nom, int a_den, int b_nom, int b_den ); 3.r.6 [life] Hra života je dvourozměrný buněčný automat: buňky tvoří čtvercovou síť a mají dva možné stavy: živá nebo mrtvá. V každé generaci (kroku simulace) spočítáme novou hodnotu pro každou buňku, a to podle těchto pravidel (výsledek závisí na současném stavu buňky a na tom, kolik z jejích 8 sousedů je živých): stav živí sousedi výsledek živá 0–1 mrtvá živá 2–3 živá živá 4–8 mrtvá mrtvá 0–2 mrtvá mrtvá 3 živá mrtvá 4-8 mrtvá Příklad krátké periodické hry: ××× × × × ×××→ → Napište funkci, která dostane na vstupu množinu živých buněk a vrátí množinu buněk, které jsou živé po n generacích. Živé buňky jsou zadané svými souřadnicemi, tzn. jako dvojice x, y. using cell = std::pair< int, int >; using grid = std::set< cell >; grid life( const grid &, int ); 24/90 PB161 Programování v jazyce C++, 16. února 2024 Část 4: Životní cyklus hodnot Ukázky: 1. xxx 2. numbers – a list of numbers which remember their type 3. refs – overloading with references 4. pool – ownership and indirect references Elementary exercises: 1. diameter – basic function overloading (circle diameter) 2. circle – same story, but with constructors 3. force – a type to represent a force (3D vector) Preparatory exercises: 1. distance – vzdálenost v rovině 2. least – nejmenší prvek bez kopií 3. loan – bankovní půjčka 4. zipper – jednoduchá datová struktura 5. rpn – postfixová aritmetika s přetěžováním 6. eval – infixová aritmetika s vlastnictvím zdrojů Regular exercises: 1. complex – přetěžování konstruktorů 2. xxx 3. search – vyhledávací strom s uzly v poli 4. bitptr – odkaz na jednotlivý bit 5. xxx 6. xxx 4.d: Demonstrace (ukázky) 4.d.2 [numbers] In this demonstration, we will look at overloading: both of regular methods and of constructors. The first class which we will implement is number, which can represent either a real (floating-point) number or an integer. Besides the attributes integer and real which store the respective numbers, the class remembers which type of number it stores, using a boolean attribute called is_real. struct number { bool is_real; int integer = 0; double real = 0; We provide two constructors for number: one for each type of number that we wish to store. The overload is selected based on the type of argument that is provided. number( int i ) : is_real( false ), integer( i ) {} number( double r ) : is_real( true ), real( r ) {} }; The second class will be a container of numbers which directly allows the user to insert both floating-point and integer numbers, without converting them to a common type. To make insertion convenient, we provide overloads of the add method. Access to the numbers is index-based and is provided by the at method, which is overloaded for entirely different reasons. class numbers { The sole attribute of the numbers class is the backing store, which is an std::vector of number instances. std::vector< number > _data; public: The two add overloads both construct an appropriate instance of number and push it to the backing vector. Nothing surprising there. void add( double d ) { _data.emplace_back( d ); } void add( int i ) { _data.emplace_back( i ); } The overloads for at are much more subtle: notice that the argument types are all identical – there are only 2 differences, first is the return type, which however does not participate in overload resolution. If two functions only differ in return type, this is an error, since there is no way to select which overload should be used. The other difference is the const qualifier, which indeed does participate in overload resolution. This is because methods have a hidden argument, this, and the trailing const concerns this argument. The const method is selected when the call is performed on a const object (most often because the call is done on a constant reference). const number &at( int i ) const { return _data.at( i ); } number &at( int i ) { return _data.at( i ); } }; int main() /* demo */ { numbers n; n.add( 7 ); n.add( 3.14 ); assert( !n.at( 0 ).is_real ); assert( n.at( 1 ).is_real ); assert( n.at( 0 ).integer == 7 ); Notice that it is possible to assign through the at method, if the object itself is mutable. In this case, overload resolution selects the second overload, which returns a mutable reference to the number instance stored in the container. n.at( 0 ) = number( 3 ); assert( n.at( 0 ).integer == 3 ); However, it is still possible to use at on a constant object – in this case, the resolution picks the first overload, which returns a constant reference to the relevant number instance. Hence, we cannot change the number this way (as we expect, since the entire object is constant, and hence also each of its components). const numbers &n_const = n; assert( n_const.at( 0 ).integer == 3 ); // n_const.at( 1 ) = number( 1 ); this will not compile } 4.d.3 [refs] In this demonstration, we will look at overloading functions based on different kinds of references. This will allow us to adapt our functions to the kind of value (and its lifetime) that is passed to them, and to deal with arguments efficiently (without making unnecessary copies). But first, let’s define a few type aliases: using int_pair = std::pair< int, int >; using int_vector = std::vector< int >; using int_matrix = std::vector< int_vector >; Our goal will be simple enough: write a function which gives access to the first element of any of the above types. In the case of int_matrix, the element is an entire row, which has some important implications that we will discuss shortly. Our main requirements will be that: 1. first should work correctly when we call it on a constant, 2. when called on a mutable value, first( x ) = y should work and alter the value x (i.e. update the first element of x). These requirements are seemingly contradictory: if we return a value (or a constant reference), we can satisfy point 1, but we fail point 2. If we 25/90 return a mutable reference, point 2 will work, but point 1 will fail. Hence we need the result type to be different depending on the argument. This can be achieved by overloading on the argument type. However, we still have one problem: how do we tell apart, using a type, whether the passed value was constant or not? Think about this: if you write a function which accepts a mutable reference, it cannot be called on an argument which is constant: the compiler will complain about the value losing its const qualifier (if you never encountered this behaviour, try it out; it’s important that you understand this). But that means that first( int_pair &arg ) can only be called on mutable arguments, which is exactly what we need. Fortunately for us, if the compiler decides that this particular first cannot be used (because of missing const), it will keep looking for some other first that might work. You hopefully remember that first( const int_pair &arg ) can be called on any value of type int_pair (without creating a copy). If we provide both, the compiler will use the non-const version if it can, but fall back to the const one otherwise. And since overloaded functions can differ in their return type, we have our solution: int &first( int_pair &p ) { return p.first; } int first( const int_pair &p ) { return p.first; } The case of int_vector is completely analogous: int &first( int_vector &v ) { return v[ 0 ]; } int first( const int_vector &v ) { return v[ 0 ]; } Since in the above cases, the return value was of type int, we did not bother with returning const references. But when we look at int_matrix, the situation has changed: the value which we return is an std::vector, which could be very expensive to copy. So we will want to avoid that. The first case (mutable argument), stays the same – we already returned a reference in this case. int_vector &first( int_matrix &v ) { return v[ 0 ]; } At first glance, the second case would seem straightforward enough – just return a const int_vector & and be done with it. But there is a catch: what if the argument is a temporary value, which will be destroyed at the end of the current statement? It’s not a very good idea to return a reference to a doomed object, since an unwitting caller could get into serious trouble if they store the returned reference – that reference will be invalid on the next line, even though there is no obvious reason for that at the callsite. You perhaps also remember, that the above function, with a mutable reference, cannot be used with a temporary as its argument: like with a constant, the compiler will complain that it cannot bind a temporary to an argument of type int_matrix &. So is there some kind of a reference that can bind a temporary, but not a constant? Yes, that would be an rvalue reference, written int_matrix &&. If the above candidate fails, the next one the compiler will look at is one with an rvalue reference as its argument. In this case, we know the value is doomed, so we better return a value, not a reference into the doomed matrix. Moreover, since the input matrix is doomed anyway, we can steal the value we are after using std::move and hence still manage to avoid a copy. int_vector first( int_matrix &&v ) { return std::move( v[ 0 ] ); } If both of the above fail, the value must be a constant – in this case, we can safely return a reference into the constant. The argument is not immediately doomed, so it is up to the caller to ensure that if they store the reference, it does not outlive its parent object. const int_vector &first( const int_matrix &v ) { return v[ 0 ]; } That concludes our quest for a polymorphic accessor. Let’s have a look at how it works when we try to use it: int main() /* demo */ { int_vector v{ 3, 5, 7, 1, 4 }; assert( first( v ) == 3 ); first( v ) = 5; assert( first( v ) == 5 ); const int_vector &const_v = v; assert( first( const_v ) == 5 ); int_matrix m{ int_vector{ 1, 2, 3 }, v }; const int_matrix &const_m = m; assert( first( first( m ) ) == 1 ); first( first( m ) )= 2; assert( first( first( const_m ) ) == 2 ); assert( first( first( int_matrix{ v, v } ) ) == 5 ); What follows is the case where the rvalue-reference overload of first (the one which handles temporaries) saves us: try to comment the overload out and see what happens on the next 2 lines for yourself. const int_vector &x = first( int_matrix{ v, v } ); assert( first( x ) == 5 ); } 4.d.4 [pool] This demo will be our first serious foray into dealing with object lifetime. In particular, we will want to implement binary trees – clearly, the lifetime of tree nodes must exactly match the lifetime of the tree itself: • if the nodes were released too early, the program would perform invalid memory access when traversing the tree, • if the nodes are not released with the tree, that would be a memory leak – we keep the nodes around, but cannot access them. This is an ubiquitous problem, and if you think about it, we have encountered it a lot, but did not have to think about it yet: the items in an std::vector or std::map or other containers have the same property: their lifetime must match the lifetime of the instance which owns them. This is one of the most important differences between C and C++: if we do C++ right, most of the time, we do not need to manage object lifetimes manually. This is achieved through two main mechanisms: 1. pervasive use of automatic variables, through value semantics – local variables and arguments are automatically destroyed when they go out of scope, 2. cascading – whenever an object is destroyed, its attributes are also destroyed automatically, and a mechanism is provided for classes which own additional, non-attribute objects (e.g. elements in an std::vector) to automatically destroy them too (this is achieved by user-defined destructors). In general, destroying objects at an appropriate time is the job of the owner of the object – whether the owner is a function (this is the case with by-value arguments and local variables) or another object (attributes, elements of a container and so on). Additionally, this happens transparently for the user: the compiler takes care of inserting the right calls at the right places to ensure everything is destroyed at the right time. The end result is modular or composable resource management – well-behaved objects can be composed into well-behaved composites without any additional glue or boilerplate. To make things easy for now, we will take advantage of existing containers to do resource management for us, which will save us from writing destructors (the proverbial glue, which is boring to write and easy to get wrong). In the next chapter, we will see how we can use smart pointers for the same purpose. We will be keeping the nodes of our binary tree in an std::vector – this means that each node has an index which we can use to refer to that node. In other words, in this demo (and in some of this week’s exercises) indices will play the role of pointers. Since 0 is a valid index, we will use -1 to indicate an invalid (‘null’) reference. Besides ‘pointers’ to the left and right child, the node will contain a single integer value. 26/90 PB161 Programování v jazyce C++, 16. února 2024 struct node { int left = -1, right = -1; int value; }; As mentioned earlier, the nodes will be held in a vector: let’s give a name to the particular vector type, for convenience: using node_pool = std::vector< node >; Working with node is, however, rather inconvenient: we cannot ‘dereference’ the left/right ‘pointers’ without going through node_pool. This makes for verbose code which is unpleasant to both read and to write. But we can do better: let’s add a simple wrapper type, which will remember both a reference to the node_pool and an index of the node we are interested in: values of this type can then behave like a proper reference to node: only a value of the node_ref type is needed to access the node and to walk the tree. struct node_ref { node_pool &_pool; int _idx; To make the subsequent code easier to write (and read), we will define a few helper methods: first, a get method which returns an actual reference to the node instance that this node_ref represents. node &get() { return _pool[ _idx ]; } And a method to construct a new node_ref using the same pool as this one, but with a new index. node_ref make( int idx ) { return { _pool, idx }; } Normally, we do not want to expose the _pool or node to users directly, hence we keep them private. But it’s convenient for tree itself to be able to access them. So we make tree a friend. node_ref( node_pool &p, int i ) : _pool( p ), _idx( i ) {} For simplicity, we allow invalid references to be constructed: those will have an index -1, and will naturally arise when we encounter a node with a missing child – that missing node is represented as index -1. The valid method allows the user to check whether the reference is valid. The remaining methods (left, right and value) must not be called on an invalid node_ref. This is the moral equivalent of a null pointer. bool valid() const { return _idx >= 0; } What follows is a simple interface for traversing and inspecting the tree. Notice that left and right again return node_ref instances. This makes tree traversal simple and convenient. node_ref left() { return make( get().left ); } node_ref right() { return make( get().right ); } int &value() { return get().value; } }; Finally the class to represent the tree as a whole. It will own the nodes (by keeping a node_pool of them as an attribute, will remember a root node (which may be invalid, if the tree is empty) and provide an interface for adding nodes to the tree. Notice that removal of nodes is conspicuously missing: that’s because the pool model is not well suited for removals (smart pointers will be better in that regard). struct tree { node_pool _pool; int _root_idx = -1; A helper method to append a new node to the pool and return its index. int make( int value ) { _pool.emplace_back(); _pool.back().value = value; return _pool.size() - 1; } node_ref root() { return { _pool, _root_idx }; } bool empty() const { return _root_idx == -1; } We will use a vector to specify a location in the tree for adding a node, with values -1 (left) and 1 (right). An empty vector represents at the root node. using path_t = std::vector< int >; Find the location for adding a node recursively and create the node when the location is found. Assumes that the path is correct. void add( node_ref parent, path_t path, int value, unsigned path_idx = 0 ) { assert( path_idx < path.size() ); int dir = path[ path_idx ]; if ( path_idx < path.size() - 1 ) { auto next = dir < 0 ? parent.left() : parent.right(); return add( next, path, value, path_idx + 1 ); } if ( dir < 0 ) parent.get().left = make( value ); else parent.get().right = make( value ); } Main entry point for adding nodes. void add( path_t path, int value ) { if ( root().valid() ) add( root(), path, value ); else { assert( path.empty() ); _root_idx = make( value ); } } }; int main() /* demo */ { tree t; t.add( {}, 1 ); assert( t.root().value() == 1 ); assert( t.root().valid() ); assert( !t.root().left().valid() ); t.add( { -1 }, 7 ); assert( t.root().value() == 1 ); assert( t.root().left().valid() ); assert( t.root().left().value() == 7 ); t.add( { -1, 1 }, 3 ); assert( t.root().left().right().value() == 3 ); } 4.e: Elementární příklady 4.e.2 [circle] Standard 2D point. struct point; Implement a structure circle with 2 constructors, one of which accepts a point and a number (center and radius) and another which accepts 2 points (center and a point on the circle itself). Store the circle using its center and radius, in attributes center and radius respectively. struct circle; 27/90 4.e.3 [force] In this example, we will define a class that represents a (physical) force in 3D. Forces are vectors (in the mathematical sense): they can be added and multiplied by scalars (scalars are, in this case, real numbers). Forces can also be compared for equality (we will use fuzzy comparison because floating point computations are inexact). Hint: It may be useful to know that when overloading binary operators, the operands do not need to be of the same type. class force; 4.p: Přípravy 4.p.1 [distance] V této úloze se budeme pohybovat v dvourozměrné ploše a počítat při tom uraženou vzdálenost. Typ walk nechť má tyto metody: • line( p ) – přesuneme se do bodu p po úsečce, • arc( p, radius ) – přesuneme se do bodu p po kružnicovém oblouku s poloměrem radius,16 přitom radius je alespoň polovina vzdálenosti do bodu p po přímce, • backtrack() – vrátíme se po vlastních stopách do předchozího bodu (vzdálenost se přitom bude zvětšovat), • distance() – vrátí celkovou dosud uraženou vzdálenost. Metody nechť je možné libovolně řetězit, tzn. je-li w typu walk, následovný výraz musí být dobře utvořený: w.line( { 1, 1 } ) .line( { 2, 1 } ) .backtrack() .arc( { 4, 1 }, 7 ); Hodnoty typu walk lze sestrojit zadáním počátečního bodu, nebo implicitně – začínají pak z bodu (0, 0). struct walk; 4.p.2 [least] Uvažme typ element hodnot, které (z nějakého důvodu) nelze kopírovat. Našim cílem bude naprogramovat funkci, která vrátí nejmenší prvek ze zadaného vektoru hodnot typu element. Definici tohoto typu nijak neměňte. struct element { element( int v ) : value( v ) {} element( element &&v ) : value( v.value ) {} element &operator=( element &&v ) = default; 16 Potřebný středový úhel naleznete například vyřešením rovnoramenného trojúhelníku s délkou ramene radius a základnou určenou vzdáleností spojovaných bodů. bool less_than( const element &o ) const { return value < o.value; } bool equal( const element &o ) const { return value == o.value; } private: int value; }; using data = std::vector< element >; Naprogramujte funkci (nebo rodinu funkcí) least tak, že volání least( d ) vrátí nejmenší prvek zadaného vektoru d typu data. Dobře si rozmyslete platnost (délku života) dotčených objektů. Nápověda: Protože nemůžete přímo manipulovat hodnotami typu element, zkuste využít k zapamatování si dosud nejlepšího kandidáta iterátor. 4.p.3 [loan] V tomto příkladu se budeme zabývat (velmi zjednodušenými) bankovními půjčkami. Navrhneme 2 třídy: account, která bude mít obvyklé metody deposit, withdraw, balance, a které konstruktoru lze předat počáteční zůstatek (v opačném případě bude implicitně nula). Druhá třída bude loan – její konstruktor přijme referenci na instanci třídy account a velikost půjčky (int). Sestrojením hodnoty typu loan se na přidružený účet připíše vypůjčená částka. Třída loan nechť má metodu repay, která zařídí (případně částečné – určeno volitelným parametrem typu int) navrácení půjčky. Proběhne-li vše v pořádku, metoda vrátí true, jinak false. Zůstatek účtu může být záporný, hodnota půjčky nikoliv. Půjčka musí být vždy plně splacena (tzn. zabraňte situaci, kdy se informace o dluhu „ztratí“ aniž by byl tento dluh splacen). struct account; struct loan; 4.p.4 [zipper] V tomto příkladu implementujeme jednoduchou datovou strukturu, které se říká zipper – reprezentuje sekvenci prvků, přitom právě jeden z nich je aktivní (angl. focused). Abychom se nemuseli zabývat generickými datovými typy, vystačíme si celočíselnými položkami. Typ zipper nechť má toto rozhraní: • konstruktor vytvoří jednoprvkový zipper z celého čísla, • shift_left (shift_right) aktivuje prvek vlevo (vpravo) od toho dosud aktivního, a to v čase O(1); metody vrací true bylo-li posun možné provést (jinak nic nezmění a vrátí false), • insert_left (insert_right) přidá nový prvek těsně vlevo (vpravo) od právě aktivního prvku (opět v čase O(1)) • focus zpřístupní aktivní prvek (pro čtení i zápis) • volitelně metody erase_left (erase_right) které odstraní prvek nalevo (napravo) od aktivního, v čase O(1), a vrátí true bylo-li to možné struct zipper; 4.p.5 [rpn] Naprogramujte jednoduchý zásobníkový evaluátor aritmetických výrazů zapsaných v RPN (postfixové notaci). Operace: • push vloží na vrch pracovního zásobníku konstantu, • apply přijme hodnotu jednoho ze tří níže definovaných typů, které reprezentují operace a příslušnou operaci provede, • metoda top poskytne přístup k aktuálnímu vrcholu pracovního zásobníku, včetně možnosti změnit jeho hodnotu, • pop odstraní jednu hodnotu z vrcholu zásobníku a vrátí ji, • empty vrátí true je-li pracovní zásobník prázdný. Podobně jako v příkladu distance zařiďte, aby bylo možné metody push a apply libovolně řetězit. Všechny tři operace uvažujeme jako binární. struct add {}; /* addition */ struct mul {}; /* multiplication */ struct dist {}; /* absolute value of difference */ struct eval; 4.p.6 [eval] V tomto cvičení naprogramujeme vyhodnocování infixových aritmetických výrazů. Zároveň zabezpečíme, aby bylo lze sdílet společné podvýrazy (tzn. uložit je jenom jednou a při dalším výskytu je pouze odkázat). Proto budeme uzly ukládat ve společném úložišti. const int op_mul = 1; const int op_add = 2; const int op_num = 3; struct node { Operace, kterou tento uzel reprezentuje (viz konstanty definované výše). Pouze uzly typu mul a add mají potomky. int op; Položky left a right jsou indexy, přičemž hodnota -1 značí neplatný odkaz. Položka is_root je nastavena na true právě tehdy, když tento uzel není potomkem žádného jiného uzlu. int left = -1, right = -1; bool is_root = true; Hodnota uzlu, je-li tento uzel typu op_num. int value = 0; }; using node_pool = std::vector< node >; 28/90 PB161 Programování v jazyce C++, 16. února 2024 Dočasná reference na uzel, kterou lze použít při procházení stromu, ale která je platná pouze tak dlouho, jako hodnota typu eval, která ji vytvořila. Přidejte (konstantní) metody left a right, kterých výsledkem je nová hodnota typu node_ref popisující příslušný uzel, a dále metodu compute, která vyhodnotí podstrom začínající v aktuálním uzlu. Konečně přidejte metodu update, která upraví hodnotu v aktuálním uzlu. Metodu update je dovoleno použít pouze na uzly typu op_num. struct node_ref; Typ eval reprezentuje výraz jako celek. Umožňuje vytvářet nové výrazy ze stávajících (pomocí metod add, mul a num) a procházet strom výrazů (počínaje z kořenů, které lze získat metodou roots). struct eval { node_pool _pool; std::vector< node_ref > roots(); node_ref add( node_ref, node_ref ); node_ref mul( node_ref, node_ref ); node_ref num( int ); }; 4.r: Řešené úlohy 4.r.1 [complex] Structure angle simply wraps a single double-precision number, so that we can use constructor overloads to allow use of both polar and cartesian forms to create instances of a single type (complex). struct angle; struct complex; Now implement the following two functions, so that they work both for real and complex numbers. // double magnitude( … ) // … reciprocal( … ) The following two functions only make sense for complex numbers, where arg is the argument, normalized into the range (−π, π⟩: double real( complex ); double imag( complex ); double arg( complex ); 4.r.2 [account] In this exercise, you will create a simple class: it will encapsulate some state (account balance) and provide a simple, safe interface around that state. The class should have the following interface: • the constructor takes 2 integer arguments: the initial balance and the maximum overdraft • a withdraw method which returns a boolean: it performs the action and returns true iff there was sufficient balance to do the withdrawal • a deposit method which adds funds to the account • a balance method which returns the current balance (may be negative) and that can be called on const instances of account class account; 4.r.3 [search] Implement a binary search tree, i.e. a binary tree which maintains the search property. That is, a value of each node is: • ≥ than all values in its left subtree, • ≤ than all values in its right subtree. Store the nodes in a pool (a vector or a list, your choice). The interface is as follows: • node_ref root() const returns the root node, • bool empty() const checks whether the tree is empty, • void insert( int v ) inserts a new value into the tree (without reba- lancing). The node_ref class then ought to provide: • node_ref left() const and node_ref right() const, • bool valid() const, • value() const which returns the value stored in the node. Calling root on an empty tree is undefined. struct node; /* ref: 6 lines */ using node_pool = std::vector< node >; class node_ref; /* ref: 12 lines */ class tree; /* ref: 28 lines */ 4.r.4 [bitptr] Implement 2 classes, bitptr and const_bitptr, which provide access to a single (mutable or constant) bit. Instances of these classes should behave as pointers in principle, though we don’t yet have tools to make them behave this way syntactically (that comes next week). In the meantime, let’s use the following interface: • bool get() – read the pointed-to bit, • void set( bool ) – write the same, • void advance() – move to the next bit, • void advance( int ) – move by a given number of bits, • bool valid() – is the pointer valid? A default-constructed bitptr is not valid. Moving an invalid bitptr results in another invalid bitptr. Otherwise, a bitptr is constructed from a std::byte pointer and an int with value between 0 and 7 (with 0 being the least-significant bit). A bitptr constructed this way is always considered valid, regardless of the value of the std::byte pointer passed to it. class bitptr; class const_bitptr; 4.r.6 [sort] Implement sort which works both on vectors (std::vector) and linked lists (std::list) of integers. The former should use in-place quicksort, while the latter should use merge sort (it’s okay to use the splice and merge methods on lists, but not sort). Feel free to refer back to 01/r5 for the quicksort. 29/90 Část S.1: Funkce a hodnoty Tato sada obsahuje příklady zaměřené na zápis jednoduchých podprogramů (zejména čistých funkcí) a na práci s hodnotami (jak skalárními, tak složenými). 1. a_queens – problém osmi dam, 2. b_city – panorama města, 3. c_magic – doplnění magického čtverce, 4. d_reversi – třírozměrná verze hry Reversi, 5. e_cellular – celulární automat na kružnici, 6. f_natural – přirozená čísla se sčítáním a násobením. Úlohu a byste měli zvládnout vyřešit hned po první přednášce. Příklady b, c vyžadují znalosti nejvýše z druhé přednášky, příklad d si vystačí s třetí přednáškou a konečně při řešení příkladů e, f můžete potřebovat i základní znalosti z přednášky čtvrté. Pozor! Řešení některých příkladů z této sady může být potřebné pro vyřešení příkladů v sadách pozdějších. Doporučujeme prolistovat si i zadání pozdějších sad. S.1.a: queens V této úloze budete programovat řešení tzv. problému osmi královen (osmi dam). Vaše řešení bude predikát, kterého vstupem bude jediné 64-bitové bezznaménkové číslo (použijeme typ uint64_t), které popisuje vstupní stav šachovnice: šachovnice 8×8 má právě 64 polí, a pro reprezentaci každého pole nám stačí jediný bit, který určí, je-li na tomto políčku umístěna královna. Políčka šachovnice jsou uspořádána počínaje levým horním rohem (nejvyšší, tedy 64. bit) a postupují zleva doprava (druhé pole prvního řádku je uloženo v 63. bitu, tj. druhém nejvyšším) po řádcích zleva doprava (první pole druhého řádku je 56. bit), atd., až po nejnižší (první) bit, který reprezentuje pravý dolní roh. Predikát nechť je pravdivý právě tehdy, není-li žádná královna na šachovnici ohrožena jinou. Program musí pracovat správně i pro případy, kdy je na šachovnici jiný počet královen než 8. Očekávaná složitost je v řádu 642 operací – totiž O(n2 ) kde n představuje počet políček. Poznámka: preferované řešení používá pro manipulaci se šachovnicí pouze bitové operace a zejména nepoužívá standardní kontejnery. Řešení, které bude nevhodně používat kontejnery (spadá sem např. jakékoliv použití std::vector) nemůže získat známku A. bool queens( std::uint64_t board ); S.1.b: city V tomto úkolu budeme pracovat s dvourozměrnou „mapou města“, kterou reprezentujeme jako čtvercovou síť. Na každém políčku může stát budova (tvaru kvádru), která má barvu a celočíselnou výšku (budova výšky 1 má tvar krychle). Pro práci s mapou si zavedeme: • typ building, který reprezentuje budovu, • typ coordinates, který určuje pozici budovy a nakonec • typ city, který reprezentuje mapu jako celek. Jihozápadní (levý dolní) roh mapy má souřadnice (0, 0), x-ová souřadnice roste směrem na východ, y-ová směrem na sever. struct building { int height; int colour; }; using coordinates = std::tuple< int, int >; using city = std::map< coordinates, building >; Nejsou-li nějaké souřadnice v mapě přítomny, znamená to, že na tomto místě žádná budova nestojí. Vaším úkolem je podle zadané mapy spočítat pravoúhlý boční pohled na město (panorama), které vznikne při pohledu z jihu, a které bude popsáno typy: • column, který reprezentuje jeden sloupec a pro každou viditelnou jednotkovou krychli obsahuje jedno číslo, které odpovídá barvě této krychle, • skyline, které obsahuje pro každou x-ovou souřadnici mapy jednu hodnotu typu column, kde index příslušného sloupce odpovídá jeho x-ové souřadnici. using column = std::vector< int >; using skyline = std::vector< column >; Vstup a odpovídající výstup si můžete představit např. takto: 5 1 2 3 3 1 4 3 2 1 0 0 1 2 3 Napište čistou funkci compute_skyline která výpočet provede. Počet prvků každého sloupce musí být právě výška nejvyšší budovy s danou x-ovou sou- řadnicí. skyline compute_skyline( const city & ); S.1.c: magic Magický čtverec je čtvercová síť o rozměru n × n, kde 1. každé políčko obsahuje jedno z čísel 1 až n2 (a to tak, že se žádné z nich neopakuje), a 2. má tzv. magickou vlastnost: součet každého sloupce, řádku a obou diagonál je stejný. Tomuto součtu říkáme „magická konstanta“. Částečný čtverec je takový, ve kterém mohou (ale nemusí) být některá pole prázdná. Vyřešením částečného čtverce pak myslíme doplnění případných prázdných míst ve čtvercové síti tak, aby měl výsledný čtverec obě výše uvedené vlastnosti. Může se samozřejmě stát, že síť takto doplnit nelze. using magic = std::vector< std::int16_t >; Vaším úkolem je naprogramovat backtrackující solver, který čtverec doplní (je-li to možné), nebo rozhodne, že takové doplnění možné není. Napište podprogram magic_solve, o kterém platí: • návratová hodnota (typu bool) indikuje, bylo-li možné vstupní čtverec doplnit, • parametr in specifikuje částečný čtverec, ve kterém jsou prázdná pole reprezentována hodnotou 0, a který je uspořádaný po řádcích a na indexu 0 je levý horní roh, • je-li výsledkem hodnota true, zapíše zároveň doplněný čtverec do výstupního parametru out (v opačném případě parametr out nezmění), • vstupní podmínkou je, že velikost vektoru in je druhou mocninou, ale 30/90 PB161 Programování v jazyce C++, 16. února 2024 o stavu předaného vektoru out nic předpokládat nesmíte. Složitost výpočtu může být až exponenciální vůči počtu prázdných polí, ale solver nesmí prohledávat stavy, o kterých lze v čase O(n2 ) rozhodnout, že je doplnit nelze. Prázdná pole vyplňujte počínaje levým horním rohem po řádcích (alternativou je zajistit, že výpočet v jiném pořadí nebude výrazně pomalejší). bool magic_solve( const magic &in, magic &out ); S.1.d: reversi Předmětem tohoto úkolu je hra Reversi (známá také jako Othello), avšak ve třírozměrné verzi. Hra se tedy odehrává v kvádru, který se skládá ze sudého počtu polí (krychlí) v každém ze tří základních směrů (podle os x, y a z). Dvě taková pole můžou sousedit stěnou (6 směrů), hranou (12 směrů) nebo jediným vrcholem (8 směrů). Pole může být prázdné, nebo může obsahovat černý nebo bílý hrací kámen. Hru hrají dva hráči (černý a bílý, podle barvy kamenů, které jim patří) a pravidla hry jsou přímočarým rozšířením těch klasických dvourozměrných: • každý hráč má na začátku 4 kameny, rozmístěné kolem prostředního bodu kvádru (jedná se tedy o 8 polí, které tento bod sdílí), a to tak, že žádná dvě obsazená pole stejné barvy nesdílí stěnu, přičemž pole s nejmenšími souřadnicemi ve všech směrech obsahuje bílý kámen, • hráči střídavě pokládají nový kámen do volného pole; je-li na tahu bílý hráč, pokládá bílý kámen do pole, které musí být nepřerušeně spojeno17 černými kameny s alespoň jedním stávajícím bílým kamenem (černý hráč hraje analogicky), • po položení nového kamene se barva všech kamenů, které leží na libovolné takové spojnici, změní na opačnou (tzn. přebarví se na barvu právě položeného kamene). Začíná bílý hráč. Hra končí, není-li možné položit nový kámen (ani jedné barvy). Vyhrává hráč s více kameny na ploše. struct reversi { Metoda start začne novou hru na ploše zadané velikosti. Případná rozehraná partie je tímto voláním zapomenuta. Po volání start je na tahu bílý hráč. void start( int x_size, int y_size, int z_size ); 17 Uvažujme dvojicí polí (krychlí) A, B a úsečku u, která spojuje jejich středy, a která prochází středem stěny, hrany nebo vrcholem pole A. Nepřerušeným spojením myslíme všechna pole, které úsečka u protíná, vyjma A a B samotných. Dvojici polí, pro které potřebná úsečka u neexistuje, nelze nepřerušeně spojit. Metoda size vrátí aktuální velikost hrací plochy. std::tuple< int, int, int > size() const; Metoda play položí kámen na souřadnice zadané parametrem. Barva kamene je určena tím, který hráč je právě na tahu. Byl-li tah přípustný, metoda vrátí true a další volání položí kámen opačné barvy. V opačném případě se hrací plocha nezmění a stávající hráč musí provést jiný tah. Není určeno, co se má stát v případě, že hra ještě nezačala, nebo již skončila (tzn. nebyla zavolána metoda start, nebo by metoda finished vrátila true). bool play( int x, int y, int z ); Nemůže-li aktivní hráč provést platný tah, zavolá metodu pass. Tato vrátí true, jedná-li se o korektní přeskočení tahu (má-li hráč k dispozici jakýkoliv jiný platný tah, musí nějaký provést – volání pass v takovém případě vrátí false a aktivní hráč se nemění). Platí stejná omezení na stav hry jako u metody play. bool pass(); Metoda-predikát finished vrací true právě tehdy, nemůže-li ani jeden z hráčů provést platný tah a hra tedy skončila. Výsledek volání není určen pro hru, která dosud nezačala (nedošlo k volání metody start). bool finished() const; Metodu result je povoleno zavolat pouze v případě, že hra skončila (tzn. volání finished by vrátilo true). Její návratovou hodnotou je rozdíl v počtu kamenů mezi bílým a černým hráčem – kladné číslo značí výhru bílého hráče, záporné výhru černého hráče a nula značí remízu. int result() const; }; S.1.e: cellular Vaším úkolem bude naprogramovat jednoduchý simulátor jednorozměrného celulárního automatu. Implementace bude sestávat ze dvou struktur, automaton_state a automaton, které jsou popsané níže. Zadané rozhraní je nutné dodržet. Definujte strukturu, automaton_state, která reprezentuje stav automatu definovaného na kružnici, s buňkami číslovanými po směru hodinových ručiček od indexu 0. Stav si můžete představit takto: 0 0 1 1 1 1 0 1 Platným indexem je libovolné celé číslo – určuje o kolik políček od indexu 0 se posuneme (kladná čísla po směru, záporná proti směru hodinových ručiček). Jsou-li s, t hodnoty typu automaton_state, dále i, n jsou hodnoty typu int a v je hodnota typu bool, tyto výrazy musí být dobře utvořené: • automaton_state( s ) vytvoří nový stav, který je stejný jako stav s, • automaton_state( n ) vytvoří nový stav o n buňkách, které jsou všechny nastaveny na false (pro n ≤ 0 není definováno), • s.size() vrátí aktuální počet buněk stavu s, • s.get( i ) vrátí hodnotu buňky na indexu i, • s.set( i, v ) nastaví buňku na indexu i na hodnotu v, • s.extend( n ) vloží n nových buněk nastavených na hodnotu false, a to tak, že nové buňky budou na indexech −1 až −n (je-li n záporné, chování není definováno), • s.reduce( n ) odstraní n buněk proti směru hodinových ručiček, počínaje indexem -1 (je-li n ≥ s.size() nebo je n záporné, chování není definováno), • t = s upraví stav t tak, aby byl stejný jako s, • t == s je true právě když jsou stavy s a t stejné, • t != s je true právě když se stavy s a t liší, • t <= s se vyhodnotí na true právě když pro všechny indexy i platí s.get( i ) || !t.get( i ), • t < s se vyhodnotí na true právě když t <= s && t != s. Je-li to možné, výrazy musí pracovat správně i v případech, kdy jsou s a/nebo t konstantní. Metody size, get a set musí pracovat v konstantním čase, vše ostatní v čase nejvýše lineárním. struct automaton_state; Struktura automaton reprezentuje samotný automat. Třída si udržuje interní stav, na kterém provádí výpočty (tzn. například volání metody step() změní tento interní stav). Následovné výrazy musí být dobře utvořené (kde a, b jsou hodnoty typu automaton, s je hodnota typu automaton_state, a konečně n a rule jsou hodnoty typu int): • automaton( rule, n ) sestrojí automat s n buňkami nastavenými na 31/90 false (chování pro n ≤ 0 není definováno), a s pravidlem rule zadaným tzv. Wolframovým kódem (chování je definováno pouze pro rule v rozsahu 0 až 255 včetně), • automaton( rule, s ) sestrojí nový automat a nastaví jeho vnitřní stav tak, aby byl stejný jako s (význam parametru rule je stejný jako výše), • a.state() umožní přístup k internímu stavu, a to tak, že je možné jej měnit, není-li samotné a konstantní (např. a.state().set( 3, true ) nastaví buňku interního stavu s indexem 3 na hodnotu true), • a = b nastaví automat a tak, aby byl stejný jako automat b (zejména tedy upraví nastavené pravidlo a vnitřní stav), • a.step() provede jeden krok výpočtu na vnitřním stavu (jeden krok nastaví všechny buňky vnitřního stavu na další generaci), • a.reset( s ) přepíše vnitřní stav kopií stavu s. Hodnoty, které vstupují do výpočtu nové generace buňky podle zadaného Wolframova kódu, čteme po směru hodinových ručiček (tzn. ve směru rostoucích indexů). Krok výpočtu musí mít nejvýše lineární (časovou i paměťovou) složitost. struct automaton; S.1.f: natural Vaším úkolem je tentokrát naprogramovat strukturu, která bude reprezentovat libovolně velké přirozené číslo (včetně nuly). Tyto hodnoty musí být možné: • sčítat (operátorem +), • odečítat (x - y je ovšem definováno pouze za předpokladu x ≥ y), • násobit (operátorem *), • libovolně srovnávat (operátory ==, !=, <, atd.), • mocnit na kladný exponent typu int metodou power, • sestrojit z libovolné nezáporné hodnoty typu int. Implicitně sestrojená hodnota typu natural reprezentuje nulu. Všechny operace krom násobení musí být nejvýše lineární vůči počtu dvojkových cifer většího z reprezentovaných čísel. Násobení může mít v nejhorším případě složitost přímo úměrnou součinu m ⋅ n (kde m a n jsou počty cifer operandů). struct natural; 32/90 PB161 Programování v jazyce C++, 16. února 2024 Část 5: Ukazatele Before you dig into the demonstrations and exercises, do not forget to read the extended introduction below. That said, the units for this week are, starting with demonstrations: 1. queue – a queue with stable references 2. finexp – like regexps but finite 3. expr – expressions with operators and shared pointers 4. family – genealogy with weak pointers Elementary exercises: 1. dynarray – a simple array with a dynamic size 2. list – a simple linked list with minimal interface 3. iota – an iterable integer range Preparatory exercises: 1. unrolled – a linked list of arrays 2. bittrie – bitwise tries (radix trees) 3. solid – efficient storage of optional data 4. chartrie – binary tree for holding string keys 5. bdd – binary decision diagrams 6. rope – a string-like structure with cheap concatenation Regular exercises: 1. circular – a singly-linked circular list 2. zipper – implementing zipper as a linked list 3. segment – a binary tree of disjoint intervals 4. diff – automatic differentiation 5. critbit – more efficient version of binary tries 6. refcnt † – implement a simple reference-counted heap 5.A: Exclusive Ownership So far, we have managed to almost entirely avoid thinking about memory management: standard containers manage memory behind the scenes. We sometimes had to think about copies (or rather avoiding them), because containers could carry a lot of memory around and copying all that memory without a good reason is rather wasteful (this is why we often pass arguments as const references and not as values). This week, we will look more closely at how memory management works and what we can do when standard containers are inadequate to deal with a given problem. In particular, we will look at building our own pointer-based data structures and how we can retain automatic memory management in those cases using std::unique_ptr. XXX 5.B: Shared Ownership While unique_ptr is very useful and efficient, it only works in cases where the ownership structure is clear, and a given object has a single owner. When ownership of a single object is shared by multiple entities (objects, running functions or otherwise), we cannot use unique_ptr. To be slightly more explicit: shared ownership only arises when the lifetime of the objects sharing ownership is not tied to each other. If A owns B and A and B both need references to C, we can assign the ownership of C to object A: since it also owns B, it must live at least as long as B and hence there ownership is not actually shared. However, if A needs to be able to transfer ownership of B to some other, unrelated object while still retaining a reference to C, then C will indeed be in shared ownership: either A or B may expire first, and hence neither can safely destroy the shared instance of C to which they both keep references. In many modern languages, this problem is solved by a garbage collector, but alas, C++ does not have one. Of course, it is usually better to design data structures in a way that allows for clear, 1:1 ownership structure. Unfortunately, this is not always easy, and sometimes it is not the most efficient solution either. Specifically, when dealing with large immutable (or persistent, in the functional programming sense) data structures, shared ownership can save considerable amount of memory, without introducing any ill side-effects, by only storing common sub-structures once, instead of cloning them. Of course, there are also cases where shared mutable state is the most efficient solution to a problem. 5.d: Demonstrace (ukázky) 5.d.1 [queue] In this example, we will demonstrate the use of std::unique_ptr, which is an RAII class for holding (owning) values dynamically allocated from the heap. We will implement a simple one-way, non-indexable queue. We will require that it is possible to erase elements from the middle in O(1), without invalidating any other iterators. The standard containers which could fit: • std::deque fails the erase in the middle requirement, • std::forward_list does not directly support queue-like operation, hence using it as a queue is possible but awkward; wrapping std::forward_list would be, however, a viable approach to this task, too, • std::list works well as a queue out of the box, but has twice the memory overhead of std::forward_list. As usual, since we do not yet understand templates, we will only implement a queue of integers, but it is not hard to imagine we could generalize to any type of element. Since we are going for a custom, node-based structure, we will need to first define the class to represent the nodes. For sake of simplicity, we will not encapsulate the attributes. struct queue_node { We do not want to handle all the memory management ourselves. To rule out the possibility of accidentally introducing memory leaks, we will use std::unique_ptr to manage allocated memory for us. Whenever a unique_ptr is destroyed, it will free up any associated memory. An important limitation of unique_ptr is that each piece of memory managed by a unique_ptr must have exactly one instance of unique_ptr pointing to it. When this instance is destroyed, the memory is deallocated. std::unique_ptr< queue_node > next; Besides the structure itself, we of course also need to store the actual data. We will store a single integer per node. int value; }; We will also need to be able to iterate over the queue. For that, we define an iterator, which is really just a slightly generalized pointer (you may remember nibble_ptr from last week). We need 3 things: pre-increment, dereference and inequality. struct queue_iterator { queue_node *node; The queue will need to create instances of a queue_iterator. Let’s make that convenient. queue_iterator( queue_node *n ) : node( n ) {} The pre-increment operator simply shifts the pointer to the next pointer of the currently active node. queue_iterator &operator++() { 33/90 node = node->next.get(); return *this; } Equality is very simple (we need this because the condition of iteration loops is it != c.end(), including range for loops). We could implement != directly, but == is usually more natural, and given ==, the compiler will derive != for us automatically. bool operator==( const queue_iterator &o ) const { return o.node == node; } And finally the dereference operator: this is what will be called when *it is evaluated. Also notice the const/non-const overloads (for completeness, it is often preferable to return a const reference from the const overload; this depends on the element type). int &operator*() { return node->value; } int operator*() const { return node->value; } }; This class represents the queue itself. We will have push and pop to add and remove items, empty to check for emptiness and begin and end to implement iteration. class queue { We will keep the head of the list in another unique_ptr. An empty queue will be represented by a null head. Also worth noting is that when using a list as a queue, the head is where we remove items. The end of the queue (where we add new items) is represented by a plain pointer because it does not own the node (the node is owned by its predecessor). std::unique_ptr< queue_node > first; queue_node *last = nullptr; public: As mentioned above, adding new items is done at the ‘tail’ end of the list. This is quite straightforward: we simply create the node, chain it into the list (using the last pointer as a shortcut) and point the last pointer at the newly appended node. We need to handle empty and non-empty lists separately because we chose to represent an empty list using null head, instead of using a dummy node. void push( int v ) { if ( last ) /* non-empty list */ { last->next = std::make_unique< queue_node >(); last = last->next.get(); } else /* empty list */ { first = std::make_unique< queue_node >(); last = first.get(); } last->value = v; } Reading off the value from the head is easy enough. However, to remove the corresponding node, we need to be able to point first at the next item in the queue. Unfortunately, we cannot use normal assignment (because copying unique_ptr is not allowed). We will have to use an operation that is called move assignment and which is written using a helper function in from the standard library, called std::move. Operations which move their operands invalidate the moved-from instance. In this case, first->next is the moved-from object and the move will turn it into a null pointer. In any case, the next pointer which was invalidated was stored in the old head node and by rewriting first, we lost all pointers to that node. This means two things: 1. the old head’s next pointer, now null, is no longer accessible 2. memory allocated to hold the old head node is freed int pop() { int v = first->value; first = std::move( first->next ); Do not forget to update the last pointer in case we popped the last item. if ( !first ) last = nullptr; return v; } The emptiness check is simple enough. bool empty() const { return !last; } Now the begin and end methods. We start iterating from the head (since we have no choice but to iterate in the direction of the next pointers). The end method should return a so-called past-the-end iterator, i.e. one that comes right after the last real element in the queue. For an empty queue, both begin and end should be the same. Conveniently, the next pointer in the last real node is nullptr, so we can use that as our end-of-queue sentinel quite naturally. You may want to go back to the pre-increment operator of queue_iterator just in case. queue_iterator begin() { return { first.get() }; } queue_iterator end() { return { nullptr }; } And finally, erasing elements. Since this is a singly-linked list, to erase an element, we need an iterator to the element before the one we are about to erase. This is not really a problem, because erasing at the head is done by pop. We use the same move assignment construct that we have seen in pop earlier. void erase_after( queue_iterator i ) { assert( i.node->next ); i.node->next = std::move( i.node->next->next ); } }; int main() /* demo */ { We start by constructing an (empty) queue and doing some basic operations on it. For now, we only try to insert and remove a single element. queue q; assert( q.empty() ); q.push( 7 ); assert( !q.empty() ); assert( q.pop() == 7 ); assert( q.empty() ); Now that we have emptied the queue again, we add a few more items and try erasing one and iterating over the rest. q.push( 1 ); q.push( 2 ); q.push( 7 ); q.push( 3 ); We check that erase works as expected. We get an iterator that points to the value 2 from above and use it to erase the value 7. queue_iterator i = q.begin(); ++ i; assert( *i == 2 ); q.erase_after( i ); We can use instances of queue in range for loops, because they have begin and end, and the types those methods return (i.e. iterators) have dereference, inequality and pre-increment. int x = 1; 34/90 PB161 Programování v jazyce C++, 16. února 2024 for ( int v : q ) assert( v == x++ ); That went rather well, let’s just check that the order of removal is the same as the order of insertion (first in, first out). This is how queues should behave. assert( q.pop() == 1 ); assert( q.pop() == 2 ); assert( q.pop() == 3 ); assert( q.empty() ); } 5.d.3 [expr] In this example program, we will look at using shared pointers and operator overloading to get a nicer version of our expression examples, this time with sub-structure sharing: that is, doing something like a + a will not duplicate the sub-expression a. Like in week 7, we will define an abstract base class to represent the nodes of the expression tree. struct expr_base { virtual int eval() const = 0; virtual ~expr_base() = default; }; Since we will use (shared) pointers to expr_base quite often, we can save ourselves some typing by defining a convenient type alias: expr_ptr sounds like a reasonable name. using expr_ptr = std::shared_ptr< expr_base >; We will have two implementations of expr_base: one for constant values (nothing much to see here), struct expr_const : expr_base { const int value; expr_const( int v ) : value( v ) {} int eval() const override { return value; } }; and another for operator nodes. Those are more interesting, because they need to hold references to the sub-expressions, which are represented as shared pointers. struct expr_op : expr_base { enum op_t { add, mul } op; expr_ptr left, right; expr_op( op_t op, expr_ptr l, expr_ptr r ) : op( op ), left( l ), right( r ) {} int eval() const override { if ( op == add ) return left->eval() + right->eval(); if ( op == mul ) return left->eval() * right->eval(); assert( false ); } }; In principle, we could directly overload operators on expr_ptr, but we would like to maintain the illusion that expressions are values. For that reason, we will implement a thin wrapper that provides a more natural interface (and also takes care of operator overloading). Again, the expr class essentially provides Java-like object semantics – which is quite reasonable for immutable objects like our expression trees here. struct expr { expr_ptr ptr; expr( int v ) : ptr( std::make_shared< expr_const >( v ) ) {} expr( expr_ptr e ) : ptr( e ) {} int eval() const { return ptr->eval(); } }; The overloaded operators simply construct a new node (of type expr_op and wrap it up in an expr instance. expr operator+( expr a, expr b ) { return { std::make_shared< expr_op >( expr_op::add, a.ptr, b.ptr ) }; } expr operator*( expr a, expr b ) { return { std::make_shared< expr_op >( expr_op::mul, a.ptr, b.ptr ) }; } int main() /* demo */ { expr a( 3 ), b( 7 ), c( 2 ); expr ab = a + b; expr bc = b * c; expr abc = a + b * c; assert( a.eval() == 3 ); assert( b.eval() == 7 ); assert( ab.eval() == 10 ); assert( bc.eval() == 14 ); assert( abc.eval() == 17 ); } 5.e: Elementární příklady 5.e.1 [dynarray] Implement a dynamic array of integers with 2 operations: element access (using methods get( i ) and set( i, v )) and resize( n ). The constructor takes the initial size as its only parameter. struct dynarray; 5.e.2 [list] Implement a linked list of integers, with head, tail (returns a reference) and empty. Asking for a head or tail of an empty list has undefined results. A default-constructed list is empty. The other constructor takes an int (the value of head) and a reference to an existing list. It will should make a copy of the latter. class list; 5.e.3 [iota] Write a class iota, which can be iterated using a range for to yield a sequence of numbers in the range start, end - 1 passed to the constructor. class iota; 5.p: Přípravy 5.p.1 [unrolled] Předmětem tohoto cvičení je datová struktura, tzv. „rozbalený“ zřetězený seznam. Typ, který bude strukturu zastřešovat, by měl mít metody begin, end, empty a push_back. Ukládat budeme celá čísla. Rozdíl mezi běžným zřetězeným seznamem a rozbaleným seznamem spočívá v tom, že ten rozbalený udržuje v každém uzlu několik hodnot (pro účely tohoto příkladu 4). Samozřejmě, poslední uzel nemusí být zcela zaplněný. Aby měla taková struktura smysl, požadujeme, aby byly hodnoty uloženy přímo v samotném uzlu, bez potřeby další alokace paměti. Návratová hodnota metod begin a end bude „pseudo-iterátor“: bude poskytovat prefixový operátor zvětšení o jedničku (pre-increment), rovnost a operátor dereference. Více informací o tomto typu objektu naleznete například v ukázce d1_queue. V tomto příkladu není potřeba implementovat mazání prvků. struct unrolled_node; struct unrolled_iterator; struct unrolled; 35/90 5.p.2 [bittrie] Binární trie je binární stom, který kóduje množinu bitových řetězců, s rychlým vkládáním a vyhledáváním. Každá hrana kóduje jeden bit. Klíč chápeme jako sekvenci bitů – každý bit určuje, kterým směrem budeme ve stromě pokračovat (0 = doleva, 1 = doprava). Bitový řetězec budeme chápat jako přítomný v reprezentované množině právě tehdy, kdy přesně popisuje cestu k listu. Pro jednoduchost budeme klíče reprezentovat jako vektor hodnot typu bool. using key = std::vector< bool >; struct trie_node; Pro jednoduchost nebudeme programovat klasickou metodu insert. Místo toho umožníme uživateli přímo vystavět trie pomocí metod root (zpřístupní kořen trie) a make (vloží nový uzel: parametry určí rodiče a směr – 0 nebo 1 – ve kterém bude uzel vložen). V obou případech je výsledkem odkaz na uzel, který lze předat metodě make. Hlavní část úkolu tedy spočívá v implementaci metody has, která pro daný klíč rozhodne, je-li v množině přítomen. struct trie; 5.p.3 [solid] V tomto cvičení se zaměříme na typy (v tomto cvičení typ solid) s volitelnými složkami (typ transform_matrix). Budou nás zejména zajímat situace, kdy je relativně častý případ, že volitelná data nejsou potřebná, a zároveň jsou dostatečně velká aby mělo smysl je oddělit do samostatného objektu (v samostatně alokované oblasti paměti). Zároveň budeme požadovat, aby logicky hodnoty hlavního typu (solid) vystupovaly jako jeden celek a nepřítomnost volitelných dat byla vnějšímu světu podle možnosti skrytá. Typ solid bude reprezentovat nějaké třírozměrné těleso, zatímco typ transform_matrix bude popisovat třírozměrnou lineární transformaci takového tělesa, a bude tedy reprezentován devíti čísly s plovoucí desetinnou čárkou (3 řádky × 3 sloupce). Tyto hodnoty nechť jsou (přímo nebo nepřímo) položkami typu transform_matrix (bez jakékoliv další pomocné paměti). Implicitně sestrojená hodnota nechť reprezentuje identitu (hodnoty na hlavní diagonále rovné 1, mimo diagonálu 0). struct transform_matrix; Typ solid bude reprezentovat společné vlastnosti pevných těles (které nezávisí na konkrétním tvaru nebo typu tělesa). Měl by mít tyto metody: • pos_x, pos_y a pos_z určí polohu těžiště v prostoru, • transform_entry( int r, int c ) udává koeficient transformační matice na řádku r a sloupci c, • transform_set( int r, int c, double v ) nastaví příslušný koeficient na hodnotu v, • konstruktor přijme 3 parametry typu double (vlastní souřadnice x, y a z). Výchozí transformační maticí je opět identita. Paměť pro tuto matici alokujte pouze v případě, že se oproti implicitnímu stavu změní některý koeficient. struct solid; 5.p.4 [inttrie] V tomto cvičení rozšíříme binární trie z p2 – místo posloupnosti bitů budeme za klíče brát posloupnosti celých čísel typu int. Vylepšíme také rozhraní – místo ruční správy uzlů poskytneme přímo operaci vložení zadaného klíče. Množiny budeme nadále kódovat do binárního stromu: • levý potomek uzlu rozšiřuje reprezentovaný klíč o jedno celé číslo (podobně jako tomu bylo u binární trie) – toto číslo je tedy součástí levé hrany, • pravý „potomek“ uzlu je ve skutečnosti jeho sourozenec, a hrana není nijak označená (přechodem doprava se klíč nemění), • řetěz pravých potomků tvoří de-facto zřetězený seznam, který budeme udržovat seřazený podle hodnot na odpovídajících levých hranách. Příklad: na obrázku je znázorněná trie s klíči [3, 1], [3, 13, 7], [3, 15], [5, 2], [5, 5], [37]. Levý potomek je pod svým rodičem, pravý je od něj napravo. 3 5 37 1 13 15 2 5 7 Můžete si představit takto reprezentovanou trie jako 232 -ární, které by bylo zcela jistě nepraktické přímo implementovat. Proto reprezentujeme virtuální uzly pomyslného 232 -árního stromu jako zřetězené seznamy pravých potomků ve fyzicky binárním stromě. using key = std::vector< int >; struct trie_node; Rozhraní typu trie je velmi jednoduché: má metodu add, která přidá klíč a metodu has, která rozhodne, je-li daný klíč přítomen. Obě jako parametr přijmou hodnotu typu key. Prefixy vložených klíčů nepovažujeme za přítomné. struct trie; 5.p.5 [bdd] Binární rozhodovací diagram je úsporná reprezentace booleovských funkcí více parametrů. Lze o nich uvažovat jako o orientovaném acyklickém grafu s dodatečnou sémantikou: každý vrchol je buď: 1. proměnná (parametr) a má dva následníky, kteří určí, jak pokračovat ve vyhodnocení funkce, je-li daná proměnná pravdivá resp. nepravdivá; 2. krom proměnných existují dva další uzly, které již žádné následníky nemají, a reprezentují výsledek vyhodnocení funkce; označujeme je jako 0 a 1. Implementujte tyto metody: • konstruktor má jeden parametr typu char – název proměnné, kterou reprezentuje kořenový uzel, • one vrátí „pravdivý“ uzel (tzn. uzel 1), • zero vrátí „nepravdivý“ uzel (tzn. uzel 0), • root vrátí počáteční (kořenový) uzel, • add_var přijme char a vytvoří uzel pro zadanou proměnnou; k jedné proměnné může existovat více než jeden uzel • add_edge přijme rodiče, hodnotu typu bool a následníka, • eval přijme map z char do bool a vyhodnotí reprezentovanou funkci na parametrech popsaných touto mapou (tzn. bude procházet BDD od kořene a v každém uzlu se rozhodne podle zadané mapy, až než dojde do koncového uzlu). Chování není definováno, obsahuje-li BDD uzel, který nemá nastavené oba následníky. struct bdd_node; struct bdd; 5.p.6 [rope] Lano je datová struktura, která reprezentuje sekvenci, implementovaná jako binární strom, který má v listech klasická pole a ve vnitřních uzlech udržuje celočíselné váhy. Sdílení podstromů je dovolené a očekávané. Váhou uzlu se myslí celková délka sekvence reprezentovaná jeho levým podstromem. Díky tomu lze lana spojovat a indexovat v čase lineárním k hloubce stromu.18 Naprogramujte: 18 Spojení dvou lan lze za cenu dodatečné informace v uzlech, nebo pomalejší indexace, provést i v konstantním čase. 36/90 PB161 Programování v jazyce C++, 16. února 2024 • konstruktor, který vytvoří jednouzlové lano z vektoru, • konstruktor, který spojí dvě stávající lana, • metodu get( i ), která získá i-tý prvek, • a set( i, value ), která i-tý prvek nastaví na value. Pro účely tohoto příkladu není potřeba implementovat žádnou formu vyvažo- vání. struct rope; 5.r: Řešené úlohy 5.r.1 [circular] In this exercise, we will implement a slightly unusual data structure: a circular linked list, but instead of the usual access operators and iteration, it will have a rotate method, which rotates the entire list. We require that rotation does not invalidate any references to elements in the list. If you think of the list as a stack, you can think of the rotate operation as taking an element off the top and putting it at the bottom of the stack. It is undefined on an empty list. To add and remove elements, we will implement push and pop which work in a stack-like manner. Only the top element is accessible, via the top method. This method should allow both read and write access. Finally, we also want to be able to check whether the list is empty. As always, we will store integers in the data structure. class circular; 5.r.2 [zipper] V této úloze se vrátíme k datové struktuře „zipper“ – připomínáme, že tato struktura reprezentuje sekvenci prvků (v našem případě celých čísel), přitom právě jeden z těchto prvků je aktivní (angl. focused). Tentokrát budeme tuto strukturu reprezentovat pomocí dvojice zřetězených seznamů sestavených z ukazatelů typu unique_ptr. Seznamy začínají v aktivním prvku a pokračují každý na jeden konec struktury. Typ zipper bude mít toto rozhraní: • konstruktor vytvoří jednoprvkový zipper z celého čísla, • shift_left (shift_right) aktivuje prvek vlevo (vpravo) od toho dosud aktivního, a to v čase O(1); metody vrací true bylo-li posun možné provést (jinak nic nezmění a vrátí false), • insert_left (insert_right) přidá nový prvek těsně vlevo (vpravo) od právě aktivního prvku (opět v čase O(1)) • focus zpřístupní aktivní prvek (pro čtení i zápis). 5.r.3 [segment] In this exercise, we will go back to building data structures, in this particular case a simple binary tree. The structure should represent a partitioning of an interval with integer bounds into a set of smaller, non-overlapping intervals. Implement class segment_map with the following interface: • the constructor takes two integers, which represent the limits of the interval to be segmented, • a split operation takes a single integer, which becomes the start of a new segment, splitting the existing segment in two, • query, given an integer n, returns the bounds of the segment that contains n, as an std::pair of integers. The tree does not need to be self-balancing: the order of splits will determine the shape of the tree. 5.r.4 [diff] In this exercise, we will implement automatic differentiation of simple expressions. You will need the following rules: • linearity: (a ⋅ f(x) + b ⋅ g(x))′ = a ⋅ f′(x) + b ⋅ g′(x) • the Leibniz rule: (f(x) ⋅ g(x))′ = f′(x) ⋅ g(x) + f(x) ⋅ g′(x) • chain rule: (f(g(x)))′ = f′(g(x)) ⋅ g′(x) • derivative of exponential: exp′(x) = exp(x) Define a type, expr (from expression), such that values of this type can be constructed from integers, added and multiplied, and exponentiated using function expnat (to avoid conflicts with the exp in the standard library). class expr; /* ref: 29 + 7 lines */ expr expnat( expr ); Implement function diff that accepts a single expr and returns the derivative (again in the form of expr). Define a constant x of type expr such that diff( x ) is 1. expr diff( expr ); /* ref: 11 lines */ // const expr x; Finally, implement function eval which takes an expr and a double and it substitutes for x and computes the value of the expression. double eval( expr, double ); /* ref: 11 lines */ 37/90 Část 6: Dědičnost a pozdní vazba This week will be about objects in the OOP (object-oriented programming) sense and about inheritance-based polymorphism. In OOP, classes are rarely designed in isolation: instead, new classes are derived from an existing base class (the derived class inherits from the base class). The derived class retains all the attributes (data) and methods (behaviours) of the base (parent) class, and usually adds something on top, or at least modifies some of the behaviours. So far, we have worked with composition (though we rarely called it that). We say objects (or classes) are composed when attributes of classes are other classes (e.g. standard containers). The relationship between the outer class and its attributes is known as ‘has-a’: a circle has a center, a polynomial has a sequence of coefficients, etc. Inheritance gives rise to a different type of relationship, known as ‘is-a’: a few stereotypical examples: • a circle is a shape, • a ball is a solid, a cube is a solid too, • a force is a vector (and so is velocity). This is where polymorphism comes into play: a function which doesn’t care about the particulars of a shape or a solid or a vector can accept an instance of the base class. However, each instance of a derived class is an instance of the base class too, and hence can be used in its place. This is known as the Liskov substitution principle. An important caveat: this does not work when passing objects by value, because in general, the base class and the derived class do not have the same size. Languages like Python or Java side-step this issue by always passing objects by reference. In C++, we have to do that explicitly if we want to use inheritance-based polymorphism. Of course, this also works with pointers (including smart ones, like std::unique_ptr). With this bit of theory out of the way, let’s look at some practical examples: the rest of theory (late binding in particular) will be explained in demonstrations: 1. account – a simple inheritance example 2. shapes – polymorphism and late dispatch 3. expr – dynamic and static types, more polymorphism 4. destroy – virtual destructors 5. factory – polymorphic return values Elementary exercises: 1. resistance – compute resistance of a simple circuit 2. perimeter – shapes and their perimeter length 3. fight – rock, paper and scissors Preparatory exercises: 1. prisoner – the famous dilemma 2. bexpr – boolean expressions with variables 3. sexpr – a tree made of lists (lisp style) 4. network – a network of counters 5. filter – filter items from a data source 6. geometry – shapes and visitors Regular exercises: 1. bom – polymorphism and collections 2. circuit – calling virtual methods within the class 3. loops – circuits with loops 4. xxx 5. xxx 6. while – interpreting while programs using an AST 6.d: Demonstrace (ukázky) 6.d.1 [account] In this example, we will demonstrate the syntax and most basic use of inheritance. Polymorphism will not enter the picture yet (but we will get to that very soon: in the next example). We will consider bank accounts (a favourite subject, surely). We will start with a simple, vanilla account that has a balance, can withdraw and deposit money. We have seen this before. class account { The first new piece of syntax is the protected keyword. This is related to inheritance: unlike private, it lets subclasses (or rather subclass methods) access the members declared in a protected section. We also notice that the balance is signed, even though in this class, that is not strictly necessary: we will need that in one of the subclasses (yes, the system is already breaking down a little). protected: int _balance; public: We allow an account to be constructed with an initial balance. We also allow it to be default-constructed, initializing the balance to 0. account( int initial = 0 ) : _balance( initial ) {} Standard stuff. bool withdraw( int sum ) { if ( _balance > sum ) { _balance -= sum; return true; } return false; } void deposit( int sum ) { _balance += sum; } int balance() const { return _balance; } }; With the base class in place, we can define a derived class. The syntax for inheritance adds a colon, :, after the class name and a list of classes to inherit from, with access type qualifiers. We will always use public inheritance. Also, did you know that naming things is hard? class account_with_overdraft : public account { The derived class has, ostensibly, a single attribute. However, all the attributes of all base classes are also present automatically. That is, there already is an int _balance attribute in this class, inherited from account. We will use it below. protected: int _overdraft; public: This is another new piece of syntax that we will need: a constructor of a derived class must first call the constructors of all base classes. Since this happens before any attributes of the derived class are constructed, this call comes first in the initialization section. The derived-class constructor is free to choose which (overloaded) constructor of the base class to call. If the call is omitted, the default constructor of the base class will be called. account_with_overdraft( int initial = 0, int overdraft = 0 ) 38/90 PB161 Programování v jazyce C++, 16. února 2024 : account( initial ), _overdraft( overdraft ) {} The methods defined in a base class are automatically available in the derived class as well (same as attributes). However, unlike attributes, we can replace inherited methods with versions more suitable for the derived class. In this case, we need to adjust the behaviour of withdraw. bool withdraw( int sum ) { if ( _balance + _overdraft > sum ) { _balance -= sum; return true; } return false; } }; Here is another example based on the same language features. class account_with_interest : public account { protected: int _rate; /* percent per annum */ public: account_with_interest( int initial = 0, int rate = 0 ) : account( initial ), _rate( rate ) {} In this case, all the inherited methods can be used directly. However, we need to add a new method, to compute and deposit the interest. Since naming things is hard, we will call it next_year. The formula is also pretty lame. void next_year() { _balance += ( _balance * _rate ) / 100; } }; The way objects are used in this exercise is not super useful: the goal was to demonstrate the syntax and basic properties of inheritance. In modern practice, code re-use through inheritance is frowned upon (except perhaps for mixins, which are however out of scope for this subject). The main use-case for inheritance is subtype polymorphism, which we will explore in the next unit, shapes.cpp. int main() /* demo */ { We first make a normal account and check that it behaves as expected. Nothing much to see here. account a( 100 ); assert( a.balance() == 100 ); assert( a.withdraw( 50 ) ); assert( !a.withdraw( 100 ) ); a.deposit( 10 ); assert( a.balance() == 60 ); Let’s try the first derived variant, an account with overdraft. We notice that it’s possible to have a negative balance now. account_with_overdraft awo( 100, 100 ); assert( awo.balance() == 100 ); assert( awo.withdraw( 50 ) ); assert( awo.withdraw( 100 ) ); awo.deposit( 10 ); assert( awo.balance() == -40 ); And finally, let’s try the other account variant, with interest. account_with_interest awi( 100, 20 ); assert( awi.balance() == 100 ); assert( awi.withdraw( 50 ) ); assert( !awi.withdraw( 100 ) ); awi.deposit( 10 ); assert( awi.balance() == 60 ); awi.next_year(); assert( awi.balance() == 72 ); } 6.d.2 [shapes] The inheritance model in C++ is an instance of a more general notion, known as subtyping. The defining characteristic of subtyping is the Liskov substitution principle: a value which belongs to a subtype (a derived class) can be used whenever a variable stores, or a formal argument expects, a value that belongs to a supertype (the base class). As mentioned earlier, in C++ this only extends to values passed by reference or through pointers. We will first define a couple useful type aliases to represent points and bounding boxes. using point = std::pair< double, double >; using bounding_box = std::pair< point, point >; Subtype polymorphism is, in C++, implemented via late binding: the decision which method should be called is postponed to runtime (with normal functions and methods, this happens during compile time). The decision whether to use early binding (static dispatch) or late binding (dynamic dispatch) is made by the programmer on a method-by-method basis. In other words, some methods of a class can use static dispatch, while others use dynamic dispatch. class shape { public: To instruct the compiler to use dynamic dispatch for a given method, put the keyword virtual in front of that method’s return type. Unlike normal methods, a virtual method may be left unimplemented: this is denoted by the = 0 at the end of the declaration. If a class has a method like this, it is marked as abstract and it becomes impossible to create instances of this class: the only way to use it is as a base class, through inheritance. This is commonly done to define interfaces. In our case, we will declare two such methods. virtual double area() const = 0; virtual bounding_box box() const = 0; A class which introduces virtual methods also needs to have a destructor marked as virtual. We will discuss this in more detail in a later unit. For now, simply consider this to be an arbitrary rule. virtual ~shape() = default; }; As soon as the interface is defined, we can start working with arbitrary classes which implement this interface, even those that have not been defined yet. We will start by writing a simple polymorphic function which accepts arbitrary shapes and computes the ratio of their area to the area of their bounding box. double box_coverage( const shape &s ) { Hopefully, you remember structured bindings (if not, revisit e.g. 03/rel.cpp). auto [ ll, ur ] = s.box(); auto [ left, bottom ] = ll; auto [ right, top ] = ur; return s.area() / ( ( right - left ) * ( top - bottom ) ); } Another function: this time, it accepts two instances of shape. The values it actually receives may be, however, of any type derived from shape. In fact, a and b may be each an instances of a different derived class. bool box_collide( const shape &sh_a, const shape &sh_b ) { A helper function (lambda) to decide whether a point is inside (or on the 39/90 boundary) of a bounding box. auto in_box = []( const bounding_box &box, const point &pt ) { auto [ x, y ] = pt; auto [ ll, ur ] = box; auto [ left, bottom ] = ll; auto [ right, top ] = ur; return x >= left && x <= right && y >= bottom && y <= top; }; auto [ a, b ] = sh_a.box(); auto box = sh_b.box(); The two boxes collide if either of the corners of one is in the other box. return in_box( box, a ) || in_box( box, b ); } We now have the interface and two functions that are defined in terms of that interface. To make some use of the functions, however, we need to be able to make instances of shape, and as we have seen earlier, that is only possible by deriving classes which provide implementations of the virtual methods declared in the base class. Let’s start by defining a circle. class circle : public shape { point _center; double _radius; public: The base class has a default constructor, so we do not need to explicitly call it here. circle( point c, double r ) : _center( c ), _radius( r ) {} Now we need to implement the virtual methods defined in the base class. In this case, we can omit the virtual keyword, but we should specify that this method overrides one from a base class. This informs the compiler of our intention to provide an implementation to an inherited method and allows it (the compiler) to emit a warning in case we accidentally hide the method instead, by mistyping the signature. The most common mistake is forgetting the trailing const. Please always specify override where it is applicable. double area() const override { return 4 * std::atan( 1 ) * std::pow( _radius, 2 ); } Now the other virtual method. bounding_box box() const override { auto [ x, y ] = _center; double r = _radius; return { { x - r, y - r }, { x + r, y + r } }; } }; And a second shape type, so we can actually make some use of polymorphism. Everything is the same as above. class rectangle : public shape { point _ll, _ur; /* lower left, upper right */ public: rectangle( point ll, point ur ) : _ll( ll ), _ur( ur ) {} double area() const override { auto [ left, bottom ] = _ll; auto [ right, top ] = _ur; return ( right - left ) * ( top - bottom ); } bounding_box box() const override { return { _ll, _ur }; } }; int main() /* demo */ { We cannot directly construct a shape, since it is abstract, i.e. it has unimplemented pure virtual methods. However, both circle and rectangle provide implementations of those methods which we can use. rectangle square( { 0, 0 }, { 1, 1 } ); assert( square.area() == 1 ); assert( square.box() == bounding_box( { 0, 0 }, { 1, 1 } ) ); assert( box_coverage( square ) == 1 ); circle circ( { 0, 0 }, 1 ); Check that the area of a unit circle is π, and the ratio of its area to its bounding box is π / 4. double pi = 4 * std::atan( 1 ); assert( std::fabs( circ.area() - pi ) < 1e-10 ); assert( std::fabs( box_coverage( circ ) - pi / 4 ) < 1e-10 ); The two shapes quite clearly collide, and if they collide, their bounding boxes must also collide. A shape should always collide with itself, and collisions are symmetric, so let’s check that too. assert( box_collide( square, circ ) ); assert( box_collide( circ, square ) ); assert( box_collide( square, square ) ); assert( box_collide( circ, circ ) ); Let’s make a shape a bit further out and check the collision detection with that. circle c1( { 2, 3 }, 1 ), c2( { -1, -1 }, 1 ); assert( !box_collide( circ, c1 ) ); assert( !box_collide( c1, c2 ) ); assert( !box_collide( c1, square ) ); assert( box_collide( c2, square ) ); } 6.d.3 [expr] To better understand polymorphism, we will need to set up some terminology, particularly: • the notion of a static type, which is, essentially, the type written down in the source code, and of a • dynamic type (also known as a runtime type), which is the actual type of the value that is stored behind a given reference (or pointer). The relationship between the static and dynamic type may be: • the static and dynamic type are the same (this was always the case until this week), or • the dynamic type may be a subtype of the static type (we will see that in a short while). Anything else is a bug. We will use a very simple representation of arithmetic expressions as our example here. An expression is a tree, where each node carries either a value or an operation. We will want to explicitly track the type of each node, and for that, we will use an enumerated type. Those work the same as in C, but if we declare them using enum class, the enumerated names will be scoped: we use them as type::sum, instead of just sum as would be the case in C. enum class type { sum, product, constant }; Now for the class hierarchy. The base class will be node. class node { public: 40/90 PB161 Programování v jazyce C++, 16. února 2024 The first thing we will implement is a static_type method, which tells us the static type of this class. The base class, however, does not have any sensible value to return here, so we will just throw an exception. type static_type() const { throw std::logic_error( "bad static_type() call" ); } The ‘real’ (dynamic) type must be a virtual method, since the actual implementation must be selected based on the dynamic type: this is exactly what late binding does. Since the method is virtual, we do not need to supply an implementation if we can’t give a sensible one. virtual type dynamic_type() const = 0; The interesting thing that is associated with each node is its value. For operation nodes, it can be computed, while for leaf nodes (type constant), it is simply stored in the node. virtual int value() const = 0; We also observe the virtual destructor rule. virtual ~node() = default; }; We first define the (simpler) leaf nodes, i.e. constants. class constant : public node { int _value; public: The leaf node constructor simply takes an integer value and stores it in an attribute. constant( int v ) : _value( v ) {} Now the interface common to all node instances: type static_type() const { return type::constant; } In methods of class constant, the static type of this is always19 either constant * or const constant *. Hence we can simply call the static_type method, since it uses static dispatch (it was not declared virtual in the base class) and hence the call will always resolve to the method just above. type dynamic_type() const override { return static_type(); } Finally, the ‘business’ method: 19 As long as we pretend that the volatile keyword does not exist, which is an entirely reasonable thing to do. int value() const override { return _value; } }; The inner nodes of the tree are operations. We will create an intermediate (but still abstract) class, to serve as a base for the two operation classes which we will define later. class operation : public node { const node &_left, &_right; public: operation( const node &l, const node &r ) : _left( l ), _right( r ) {} We will leave static_type untouched: the version from the base class works okay for us, since there is nothing better that we could do here. The dynamic_type and value stay unimplemented. We are facing a dilemma here, though. We would like to add accessors for the children, but it is not clear whether to make them virtual or not. Considering that we keep the references in attributes of this class, it seems unlikely that the implementation of the accessors would change in a subclass and we can use cheaper static dispatch. const node &left() const { return _left; } const node &right() const { return _right; } }; Now for the two operation classes. class sum : public operation { public: The base class does not have a default constructor, which means we need to call the one that’s available manually. sum( const node &l, const node &r ) : operation( l, r ) {} We want to replace the static_type implementation that was inherited from node (through operation): type static_type() const { return type::sum; } And now the (dynamic-dispatch) interface mandated by the (indirect) base class node. We can use the same approach that we used in constant for dynamic_type: type dynamic_type() const override { return static_type(); } And finally the logic. The static return type of left and right is const node &, but the method we call on each, value, uses dynamic dispatch (it is marked virtual in class node). Therefore, the actual method which will be called depends on the dynamic type of the respective child node. int value() const override { return left().value() + right().value(); } }; Basically a re-run of sum. class product : public operation { public: We will use a trick which will allow us to not type out the (boring and redundant) constructor. If all we want to do is just forward arguments to the parent class, we can use the following syntax. You do not have to remember it, but it can save some typing if you do. using operation::operation; Now the interface methods. type static_type() const { return type::product; } type dynamic_type() const override { return static_type(); } int value() const override { return left().value() * right().value(); } }; int main() /* demo */ { Instances of class constant are quite straightforward. Let’s declare some. constant const_1( 1 ), const_2( 2 ), const_m1( -1 ), const_10( 10 ); The constructor of sum accepts two instances of node, passed by reference. Since constant is a subclass of node, it is okay to use those, too. sum sum_0( const_1, const_m1 ), sum_3( const_1, const_2 ); The product constructor is the same. But now we will also try using instances of sum, since sum is also derived (even if indirectly) from node 41/90 and therefore sum is a subtype of node, too. product prod_4( const_2, const_2 ), prod_6( const_2, sum_3 ), prod_40( prod_4, const_10 ); Let’s also make a sum instance which has children of different types. sum sum_9( sum_3, prod_6 ); For all variables which hold values (i.e. not references), static type = dynamic type. To make the following code easier to follow, the static type of each of the above variables is explicitly mentioned in its name. Clearly, we can call the value method on the variables directly and it will call the right method. assert( const_1.value() == 1 ); assert( const_2.value() == 2 ); assert( sum_0.value() == 0 ); assert( sum_3.value() == 3 ); assert( prod_4.value() == 4 ); assert( prod_6.value() == 6 ); assert( prod_40.value() == 40 ); assert( sum_9.value() == 9 ); However, the above results should already convince us that dynamic dispatch works as expected: the results depend on the ability of sum::value and product::value to call correct versions of the value method on their children, even though the static types of the references stored in operation are const node. We can however explore the behaviour in a bit more detail. const node &sum_0_ref = sum_0, &prod_6_ref = prod_6; Now the static type of sum_0_ref is const node &, but the dynamic type of the value to which it refers is sum, and for prod_6_ref the static type is const node & and dynamic is product. assert( sum_0_ref.value() == 0 ); assert( prod_6_ref.value() == 6 ); Let us also check the behaviour of left and right. assert( sum_0.left().value() == 1 ); assert( sum_0.right().value() == -1 ); The static type through which we call left and right does not matter, because neither product nor sum provide a different implementation of the method. const operation &op = sum_0; assert( op.left().value() == 1 ); assert( op.right().value() == -1 ); The final thing to check is the static_type and dynamic_type methods. By now, we should have a decent understanding of what to expect. Please note that sum_0 and sum_0_ref refer to the same instance and hence they have the same dynamic type, even though their static types differ. assert( sum_0.dynamic_type() == type::sum ); assert( sum_0_ref.dynamic_type() == type::sum ); assert( sum_0.static_type() == type::sum ); try { sum_0_ref.static_type(); assert( false ); } catch ( const std::logic_error & ) {} And the same is true about prod_6 and prod_6_ref. assert( prod_6.dynamic_type() == type::product ); assert( prod_6_ref.dynamic_type() == type::product ); assert( prod_6.static_type() == type::product ); try { prod_6_ref.static_type(); assert( false ); } catch ( const std::logic_error & ) {} } 6.d.5 [factory] TODO: do not use strings here As we have seen, subtype polymorphism allows us to define an interface in terms of virtual methods (that is, based on late dispatch) and then create various implementations of this interface. It is sometimes useful to create instances of multiple different derived classes based on runtime inputs, but once they are created, to treat them uniformly. The uniform treatment is made possible by subtype polymorphism: if the entire interaction with these objects is done through the shared interface, the instances are all, at the type level, interchangeable with each other. The behaviour of those instances will of course differ, depending on their dynamic type. When a system is designed this way, the entire program uses a single static type to work with all instances from the given inheritance hierarchy – the type of the base class. Let’s define such a base class. class part { public: virtual std::string description() const = 0; virtual ~part() = default; }; Let’s add a simple function which operates on generic parts. Working with instances is easy, since they can be passed through a reference to the base type. For instance the following function which formats a single line for a bill of materials (bom). std::string bom_line( const part &p, int count ) { return std::to_string( count ) + "x " + p.description(); } However, creation of these instances poses a somewhat unique challenge in C++: memory management. In languages like Java or C#, we can create the instance and return a reference to the caller, and the garbage collector will ensure that the instance is correctly destroyed when it is no longer used. We do not have this luxury in C++. Of course, we could always do memory management by hand, like it’s 1990. Fortunately, modern C++ provides smart pointers in the standard library, making memory management much easier and safer. Recall that a unique_ptr is an owning pointer: it holds onto an object instance while it is in scope and destroys it afterwards. Unlike objects stored in local variables, though, the ownership of the instance held in a unique_ptr can be transferred out of the function (i.e. an instance of unique_ptr can be legally returned, unlike a reference to a local variable). This will make it possible to define a factory: a function which constructs instances (parts) and returns them to the caller. Of course, to actually define the function, we will need to define the derived classes which it is supposed to create. using part_ptr = std::unique_ptr< part >; part_ptr factory( std::string ); In the program design outlined earlier, the derived classes change some of the behaviours, or perhaps add data members (attributes) to the base class, but apart from construction, they are entirely operated through the interface defined by the base class. class cog : public part { int teeth; public: cog( int teeth ) : teeth( teeth ) {} std::string description() const override { return std::string( "cog with " ) + std::to_string( teeth ) + " teeth"; } }; class axle : public part { public: 42/90 PB161 Programování v jazyce C++, 16. února 2024 std::string description() const override { return "axle"; } }; class screw : public part { int _thread, _length; public: screw( int t, int l ) : _thread( t ), _length( l ) {} std::string description() const override { return std::to_string( _length ) + "mm M" + std::to_string( _thread ) + " screw"; } }; Now that we have defined the derived classes, we can finally define the factory function. part_ptr factory( std::string desc ) { We will use std::istringstream (first described in 06/streams.cpp) to extract a description of the instance that we want to create from a string. The format will be simple: the type of the part, followed by its parameters separated by spaces. std::istringstream s( desc ); std::string type; s >> type; /* extract the first word */ if ( type == "cog" ) { int teeth; s >> teeth; return std::make_unique< cog >( teeth ); } if ( type == "axle" ) return std::make_unique< axle >(); if ( type == "screw" ) { int thread, length; s >> thread >> length; return std::make_unique< screw >( thread, length ); } throw std::runtime_error( "unexpected part description" ); } int main() /* demo */ { Let’s first use the factory to make some instances. They will be held by part_ptr (i.e. unique_ptr with the static type part. part_ptr ax = factory( "axle" ), m7 = factory( "screw 7 50" ), m3 = factory( "screw 3 10" ), c8 = factory( "cog 8" ), c9 = factory( "cog 9" ); From the point of view of the static type system, all the parts created above are now the same. We can call the methods which were defined in the interface, or we can pass them into functions which work with parts. assert( ax->description() == "axle" ); assert( m7->description() == "50mm M7 screw" ); assert( m3->description() == "10mm M3 screw" ); assert( c8->description() == "cog with 8 teeth" ); assert( c9->description() == "cog with 9 teeth" ); Let’s try using the bom_line function which we have defined earlier. assert( bom_line( *ax, 3 ) == "3x axle" ); assert( bom_line( *m7, 20 ) == "20x 50mm M7 screw" ); At the end of the scope, the objects are destroyed and all memory is automatically freed. } 6.e: Elementární příklady 6.e.1 [resistance] We are given a simple electrical circuit made of resistors and wires, and we want to compute the total resistance between two points. The circuit is simple in the sense that in any given section, all its immediate sub-sections are either connected in series or in parallel. Here is an example: R2 R1 R5 R3 R4 A B The resistance that we are interested in is between the points A and B. Given R1 and R2 connected in series, the total resistance is R = R1 +R2. For the same resistors connected in parallel, the resistance is given by 1/R = 1/R1 + 1/R2. You will implement 2 classes: series and parallel, each of which represents a single segment of the circuit. Both classes shall provide a method add, that will accept either a number (double) which will add a single resistor to that segment, or a const reference to the opposite class (i.e. an instance of series should accept a reference to parallel and vice versa). class series; class parallel; Then add a top-level function resistance, which accepts either a series or a parallel instance and computes the total resistance of the circuit described by that instance. The exact prototype is up to you. 6.e.2 [perimeter] Implement a simple inheritance hierarchy – the base class will be shape, with a pure virtual method perimeter, the 2 derived classes will be circle and rectangle. The circle is constructed from a radius, while the rectangle from a width and height, all of them floatingpoint numbers. class shape; class circle; class rectangle; bool check_shape( const shape &s, double p ) { return std::fabs( s.perimeter() - p ) < 1e-8; } 6.e.3 [fight] There should be 4 classes: the base class gesture and 3 derived: rock, paper and scissors. Class gesture has a (pure virtual) method fight which takes another gesture (via a const reference) and returns true if the current gesture wins. To do this, add another method, visit, which has 3 overloads, one each for rock, paper and scissors. Then override fight in each derived class, to simply call visit( *this ) on the opposing gesture. The visit method knows the type of both this and the opponent (via the overload) – simply indicate the winner by returning an appropriate constant. class rock; class paper; class scissors; Keep the forward declarations, you will need them to define the overloads for visit. class gesture; 43/90 Now define the 3 derived classes. 6.p: Přípravy 6.p.1 [prisoner] Another exercise, another class hierarchy. The abstract base class will be called prisoner, and the implementations will be different strategies in the well-known game of (iterated) prisoner’s dilemma. The prisoner class should provide method betray which takes a boolean (the decision of the other player in the last round) and returns the decision of the player for this round. In general, the betray method should not be const, because strategies may want to remember past decisions (though we will not implement a strategy like that in this exercise). class prisoner; Implement an always-betray strategy in class traitor, the tit-for-tat strategy in vengeful and an always-cooperate in benign. class traitor; class vengeful; class benign; Implement a simple strategy evaluator in function play. It takes two prisoners and the number of rounds and returns a negative number if the first one wins, 0 if the game is a tie and a positive number if the second wins (the absolute value of the return value is the difference in scores achieved). The scoring matrix: • neither player betrays 2 / 2 • a betrays, b does not: 3 / 0 • a does not betray, b does: 0 / 3 • both betray 1 / 1 int play( prisoner &a, prisoner &b, int rounds ); 6.p.2 [bexpr] Boolean expressions with variables, represented as binary trees. Internal nodes carry a logical operation on the values obtained from children while leaf nodes carry variable references. To evaluate an expression, we will need to supply values for each of the variables that appears in the expression. We will identify variables using integers, and the assignment of values will be done through the type input defined below. It is undefined behaviour if a variable appears in an expression but is not present in the provided input value. using input = std::map< int, bool >; Like earlier in expr.cpp, the base class will be called node, but this time will only define a single method: eval, which accepts a single input argument (as a const reference). class node; /* ref: 6 lines */ Internal nodes are all of the same type, and their constructor takes an unsigned integer, table, and two node references. Assuming bit zero is the lowest-order bit, the node operates as follows: • false false → bit 0 of table • false true → bit 1 of table • true false → bit 2 of table • true true → bit 3 of table class operation; /* ref: 16 lines */ The leaf nodes carry a single integer (passed in through the constructor) – the identifier of the variable they represent. class variable; /* ref: 7 lines */ 6.p.3 [sexpr] An s-expression is a tree in which each node has an arbitrary number of children. To make things a little more interesting, our s-expression nodes will own their children. The base class will be called node (again) and it will have single (virtual) method: value, with no arguments and an int return value. class node; using node_ptr = std::unique_ptr< node >; There will be two types of internal nodes: sum and product, and in this case, they will compute the sum or the product of all their children, regardless of their number. A sum with no children should evaluate to 0 and a product with no children should evaluate to 1. Both will have an additional method: add_child, which accepts (by value) a single node_ptr and both should have default constructors. It is okay to add an intermediate class to the hierarchy. class sum; class product; Leaf nodes carry an integer constant, given to them via a constructor. class constant; 6.p.4 [network] V tomto cvičení budeme definovat síť počítadel, přičemž každý uzel má jedno počítadlo znaménkového typu, které je iniciálně nastavené na nulu, a události které počítadlo mění se šíří po síti podle pravidel uvedených níže. Každý uzel může mít libovolný počet příchozích i odchozích spojení. Události jsou tří typů: reset, který nastaví počítadlo na 0, increment ho zvýší o jedna a decrement ho o jedna sníží. using event = int; const event event_reset = 0; const event event_increment = 1; const event event_decrement = 2; Abstraktní bázová třída node určí polymorfní rozhraní: • react s jediným argumentem typu event (popisuje událost podle konstant výše), • connect která přijme odkaz (referenci) na jiný uzel (typ node) a vytvoří spojení, které směruje od aktuálního uzlu k tomu, který je zadaný v parametru, • read, konstantní metoda, která vrátí aktuální hodnotu počítadla. Dobře si rozmyslete, které metody muí být virtuální a které nioliv. class node; Následují již konkrétní typy uzlů. Každý uzel nejprve aplikuje příchozí událost na svoje vlastní počítadlo, poté ho přepošle všem svým sousedům. Implementujte tyto typy: • forward přepošle stejnou událost, jakou obrdržel, • invert pošle opačnou událost (reset je opačný sám sobě), • gate přepošle stejnou událost, ale pouze je-li nová hodnota počítadla kladná. class forward; class invert; class gate; 6.p.5 [filter] This exercise will be another take on a set of numbers. This time, we will add a capability to filter the numbers on output. It will be possible to change the filter applied to a given set at runtime. The base class for representing filters will contain a single pure virtual method, accept. The method should be marked const. class filter; The set (which we will implement below) will own the filter instance and hence will use a unique_ptr to hold it. using filter_ptr = std::unique_ptr< filter >; The set should have standard methods: add and has, the latter of which will respect the configured filter (i.e. items rejected by the filter will always test negative on has). The method set_filter should set the filter. 44/90 PB161 Programování v jazyce C++, 16. února 2024 If no filter is set, all numbers should be accepted. Calling set_filter with a nullptr argument should clear the filter. Additionally, set should have begin and end methods (both const) which return very simple iterators that only provide dereference to an int (value), pre-increment and inequality. It is a good idea to keep two instances of std::set< int >::iterator in attributes (in addition to a pointer to the output filter): you will need to know, in the pre-increment operator, that you ran out of items when skipping numbers which the filter rejected. class set_iterator; class set; Finally, implement a filter that only accepts odd numbers. class odd; 6.p.6 [geometry] We will go back to a bit of geometry, this time with circles and lines: in this exercise, we will be interested in planar intersections. We will consider two objects to intersect when they have at least one common point. On the C++ side, we will use a bit of a trick with virtual method overloading (in a slightly more general setting, the trick is known as the visitor pattern). First some definitions: the familiar point. using point = std::pair< double, double >; Check whether two floating-point numbers are ‘essentially the same’ (i.e. fuzzy equality). bool close( double a, double b ) { return std::fabs( a - b ) < 1e-10; } We will need to use forward declarations in this exercise, since methods of the base class will refer to the derived types. struct circle; struct line; These two helper functions are already defined in this file and may come in useful (like the slope class above). double dist( point, point ); double dist( const line &, point ); A helper class which is constructed from two points. Two instances of slope compare equal if the slopes of the two lines passing through the respective point pairs are the same. struct slope : std::pair< double, double > { slope( point p, point q ) : point( ( q.first - p.first ) / dist( p, q ), ( q.second - p.second ) / dist( p, q ) ) {} bool operator==( const slope &o ) const { auto [ px, py ] = *this; auto [ qx, qy ] = o; return ( close( px, qx ) && close( py, qy ) ) || ( close( px, -qx ) && close( py, -qy ) ); } bool operator!=( const slope &o ) const { return !( *this == o ); } }; Now we can define the class object, which will have a virtual method intersects with 3 overloads: one that accepts a const reference to a circle, another that accepts a const reference to a line and finally one that accepts any object. class object; Put definitions of the classes circle and line here. A circle is given by a point and a radius (double), while a line is given by two points. NB. Make the line attributes public and name them p and q to make the dist helper function work. struct circle; /* ref: 18 lines */ struct line; /* ref: 18 lines */ Definitions of the helper functions. double dist( point p, point q ) { auto [ px, py ] = p; auto [ qx, qy ] = q; return std::sqrt( std::pow( px - qx, 2 ) + std::pow( py - qy, 2 ) ); } double dist( const line &l, point p ) { auto [ x2, y2 ] = l.q; auto [ x1, y1 ] = l.p; auto [ x0, y0 ] = p; return std::fabs( ( y2 - y1 ) * x0 - ( x2 - x1 ) * y0 + x2 * y1 - y2 * x1 ) / dist( l.p, l.q ); } 6.r: Řešené úlohy 6.r.1 [bom] TODO: do not use strings here Let’s revisit the idea of a bill of materials that made a brief appearance in factory.cpp, but in a slightly more useful incarnation. Define the following class hierarchy: the base class, part, should have a (pure) virtual method description that returns an std::string. It should also keep an attribute of type std::string and provide a getter for this attribute called part_no() (part number). Then add 2 derived classes: • resistor which takes the part number and an integral resistance as its constructor argument and provides a description of the form "resistor ?Ω" where ? is the provided resistance, • capacitor which also takes a part number and an integral capacitance and provides a description of the form "capacitor ?μF" where ? is again the provided value. class part; class resistor; class capacitor; We will also use owning pointers, so let us define a convenient type alias for that: using part_ptr = std::unique_ptr< part >; That was the mechanical part. Now we will need to think a bit: we want a class bom which will remember a list of parts, along with their quantities and will own the part instances it holds. The interface: • a method add, which accepts a part_ptr by value (it will take ownership of the instance) and the quantity (integer) • a method find which accepts an std::string and returns a const reference to the part instance with the given part number, • a method qty which returns the associated quantity, given a part number. class bom; 6.r.2 [circuit] V tomto cvičení se budeme zabývat voláním virtuálních metod zevnitř třídy samotné – přístup, který bychom mohli nazvat „obrácenou“ dědičností. Většina implementace bude totiž v rodičovské třídě, s použitím 45/90 několika (resp. v tomto příkladu jedné) virtuální metody určené pro vnitřní potřebu. Naprogramujte jednoduchou dědickou hierarchii tříd, která bude reprezentovat logický obvod – součástky spojené vodiči. Každá součástka bude mít nejvýše 2 vstupy a jediný výstup (a všechny budou nabývat jednu ze dvou hodnot – true nebo false). Ve třídě component implementujte tyto nevirtuální metody: • connect přijme celé číslo (0 nebo 1 – index vstupu, který hodláme připojovat) a referenci na další součástku, které výstup připojí na vybraný vstup aktuální součástky, • read (bez parametrů), která vrátí aktuální hodnotu výstupu součástky (tento bude samozřejmě záviset na stavu vstupních součástek). Implicitně jsou oba vstupy nepřipojené. Nepřipojené vstupy mají pevnou hodnotu false. Chování není určeno, je-li v obvodu cyklus. class component; Dále doplňte tyto odvozené třídy: • nand reprezentuje součástku, které výstup je NAND vstupů, • source která ignoruje oba vstupy a které výstup je true, • delay která se chová následovně: při prvním volání read vrátí vždy false; další volání read vrátí hodnotu, kterou měl vstup 0 při předchozím volání read. class nand; class source; class delay; 6.r.3 [loops] Same basic idea as circuit.cpp: we model a circuit made of components. Things get a bit more complicated in this version: • loops are allowed • parts have 2 inputs and 2 outputs each The base class, with the following interface: • read takes an integer (decides which output) and returns a boolean, • connect takes two integers and a reference to a component (the first integer defines the input of this and the second integer defines the output of the third argument to connect). There is more than one way to resolve loops, some of which require read to be virtual (that’s okay). Please note that each loop must have at least one delay in it (otherwise, behaviour is still undefined). NB. Each component should first read input 0 and then input 1: the ordering will affect the result. class component; /* ref: 30 lines */ A delay is a component that reads out, on both outputs, the value it has obtained on the corresponding input on the previous call to read. class delay; /* ref: 20 lines */ A latch remembers one bit of information (starting at false): • if both inputs read false, the remembered bit remains unchanged, • if input 0 is false while input 1 is true the remembered bit is set to true, • in all other cases, the remembered bit is set to false. The value on output 0 is the new value of the remembered bit: there is no delay. The value on output 1 is the negation of output 0. class latch; /* 15 lines */ Finally, the cnot gate, or a controlled not gate has the following behaviour: • output 0 always matches input 0, while • output 1 is set to: ∘ input 1 if input 0 is true ∘ negation of input 1 if input 0 is false class cnot; /* ref: 11 lines */ 6.r.6 [while] Uvažme abstraktní syntaktický strom velmi jednoduchého imperativního programovacího jazyka se strukturovaným řízením toku. Nechť existují 3 typy příkazů: 1. zvýšení proměnné o jedničku, a++, 2. cyklus while tvaru while (a != b) stmt, a konečně 3. blok, který je tvořen posloupností příkazů. class statement; using stmt_ptr = std::unique_ptr< statement >; Proměnné budeme označovat písmeny. Rozsah každé proměnné je 0 až 15 včetně; není-li explicitně inicializovaná, její startovní hodnota je 0. Je-li hodnota 15 zvýšena o jedna, výsledkem je opět 0. Metoda eval dostane na vstupu jednak iniciální nastavení proměnných (hodnotu typu state definovaného níže), jednak limit n ≥ 1 na délku výpočtu – tento limit udává kolik může program jako celek vykonat srovnání. Po provedení n srovnání je celý výpočet okamžitě ukončen (tělo cyklu ani žádný jiný příkaz už se neprovede). using state = std::map< char, int >; Konstruktory nechť mají tyto parametry: • stmt_inc přijme název proměnné, kterou má zvýšit, • stmt_while dostane 2 názvy proměnných a tělo ve formě ukazatele typu stmt_ptr, • stmt_block je implicitně zkonstruovaný jako prázdný, ale poskytuje metodu append která na konec bloku přidá příkaz (opět formou stmt_ptr). class stmt_inc; class stmt_while; class stmt_block; 46/90 PB161 Programování v jazyce C++, 16. února 2024 Část 7: Výjimky a princip RAII Demonstrace: 1. exceptions – vyhazování a chytání výjimek 2. stdexcept – typy výjimek ve standardní knihovně 3. semaphore – automatická správa zdrojů Elementární příklady: 1. xxx 2. counter – jednoduché počítadlo instancí 3. coffee – model automatu na kávu Preparatory exercises: 1. fd – POSIX file descriptors 2. loan – database-style transactions with resources 3. library – borrowing books 4. parse – a simple parser which throws exceptions 5. invest – we further stretch the banking story 6. linear – linear equations, with some exceptions Regular exercises: 1. printing – printing with a monthly budget 2. bsearch – a key-value vector which throws on failure 3. enzyme – cellular chemistry with RAII 4. tinyvec † – a vector in a fixed memory buffer 5. lock – a movable mutual exclusion token 6. bounded – a bounded queue that throws when full 7.d: Demonstrace (ukázky) 7.d.1 [exceptions] Exceptions are, as their name suggests, a mechanism for handling unexpected or otherwise exceptional circumstances, typically error conditions. A canonic example would be trying to open a file which does not exist, trying to allocate memory when there is no free memory left and the like. Another common circumstance would be errors during processing user input: bad format, unexpected switches and so on. NB. Do not use exceptions for ‘normal’ control flow, e.g. for terminating loops. That is a really bad idea (even though try blocks are cheap, throwing exceptions is very expensive). This example will be somewhat banal. We start by creating a class which has a global counter of instances attached to it: i.e. the value of counter tells us how many instances of counted exist at any given time. Fair warning, do not do this at home. int counter = 0; struct counted { counted() { ++ counter; } ~counted() { -- counter; } counted( const counted & ) = delete; counted( counted && ) = delete; counted &operator=( const counted & ) = delete; counted &operator=( counted && ) = delete; }; A few functions which throw exceptions and/or create instances of the counted class above. Notice that a throw statement immediately stops the execution and propagates up the call stack until it hits a try block (shown in the main function below). The same applies to a function call which hits an exception: the calling function is interrupted immediately. int f() { counted x; return 7; } int g() { counted x; throw std::bad_alloc(); assert( 0 ); } int h() { throw std::runtime_error( "h" ); } int i() { counted x; g(); assert( 0 ); } int main() /* demo */ { bool caught = false; A try block allows us to detect that an exception was thrown and react, based on the type and attributes of the exception. Otherwise, it is a regular block with associated scope, and behaves normally. try { counted x; assert( counter == 1 ); f(); assert( counter == 1 ); } One or more catch blocks can be attached to a try block: those describe what to do in case an exception of a matching type is thrown in one of the statements of the try block. The catch clause behaves like a prototype of a single-argument function – if it could be ‘called’ with the thrown exception as an argument, it is executed to handle the exception. This particular catch block is never executed, because nothing in the associated try block above throws a matching exception (or rather, any exception at all): catch ( const std::bad_alloc & ) { assert( false ); } The counted instance x above went out of scope: assert( counter == 0 ); Let’s write another try block. This time, the i call in the try block throws, indirectly (via g) an exception of type std::bad_alloc. try { i(); } To demonstrate how catch blocks are selected, we will first add one for std::runtime_error, which will not trigger (the ‘prototype’ does not match the exception type that was thrown): catch ( const std::runtime_error & ) { assert( false ); } As mentioned above, each try block can have multiple catch blocks, so let’s add another one, this time for the bad_alloc that is actually thrown. If the catch matches the exception type, it is executed and propagation of the exception is stopped: it is now handled and execution continues normally after the end of the catch sequence. catch ( const std::bad_alloc & ) { caught = true; } Execution continues here. We check that the catch block was actually executed: assert( caught ); assert( counter == 0 ); // no ‹counted› instances were leaked } 7.d.2 [stdexcept] It is possible to sub-class standard exception classes. For most uses, std::runtime_error is the most appropriate base class. class custom_exception : public std::runtime_error { public: custom_exception() : std::runtime_error( "custom" ) {} }; This demo simply demonstrates some of the standard exception types (i.e. those that are part of the standard library, and which are thrown by standard functions or methods; as long as those methods or functions are not too arcane). int main() /* demo */ 47/90 { try { throw custom_exception(); assert( false ); } As per standard rules, it’s possible to catch exceptions of derived classes (of course including user-defined types) via a catch clause which accepts a reference to a superclass. catch ( const std::exception & ) {} try { std::vector x{ 1, 2 }; Attempting out-of-bounds access through at gives std::out_of_range x.at( 7 ); assert( false ); } catch ( const std::out_of_range & ) {} try { If the string passed to stoi is not a number, we get back an exception of type std::invalid_argument. std::stoi( "foo" ); assert( false ); } catch ( const std::invalid_argument & ) {} try { If an integer is too big to fit the result type, stoi throws std::out_of_range. std::stoi( "123456123456123456" ); assert( false ); } catch ( const std::out_of_range & ) {} try { System-interfacing functions may throw std::system_error. Here, for instance, trying to detach a thread which was not started. std::thread().detach(); assert( false ); } catch ( const std::system_error & ) {} try { Throwing a system_error is the appropriate reaction when dealing with a failure of a POSIX function which sets errno. int fd = ::open( "/does/not/exist", O_RDONLY ); if ( fd < 0 ) throw std::system_error( errno, std::system_category(), "opening /does/not/exist" ); assert( false ); } catch ( const std::system_error & ) {} try { Passing a size that is more than max_size() when constructing or resizing an std::string or an std::vector gives us back an std::length_error. Note that the -1 turns into a really big number in this context. std::string x( -1, 'x' ); assert( false ); } catch ( const std::length_error & ) {} try { std::bitset< 128 > x; x[ 100 ] = true; Trying to convert an std::bitset to an integer type may throw std::overflow_error, if there are bits set that do not fit into the target integer type. x.to_ulong(); assert( false ); } catch ( const std::overflow_error & ) {} } 7.d.3 [semaphore] In this demo, we will implement a simple semaphore. A semaphore is a device which guards a resource of which there are multiple instances, but the number of instances is limited. It is a slight generalization of a mutex (which guards a singleton resource). Internally, semaphore simply counts the number of clients who hold the resource and refuses further requests if the maximum is reached. In a multi-threaded program, semaphores would typically block (wait for a slot to become available) instead of refusing. In a single-threaded program (which is what we are going to use for a demonstration), this would not work. Hence our get method returns a bool, indicating whether acquisition of the lock succeeded. class semaphore { int _available; public: When a semaphore is constructed, we need to know how many instances of the resource are available. explicit semaphore( int max ) : _available( max ) {} Classes which represent resource managers (in this case ‘things that can be locked’ as opposed to ‘locks held’) have some tough choices to make. If they are impossible to copy/move/assign, users will find that they must not appear as attributes in their classes, lest those too become un-copyable (and un-movable) by default. However, this is how the standard library deals with the problem, see std::mutex or std::condition_variable. While it is the safest option, it is also the most annoying. Nonetheless, we will do the same. semaphore( const semaphore & ) = delete; semaphore &operator=( const semaphore & ) = delete; We allow would-be lock holders to query the number of resource instances currently available. Perhaps if none are left, they can make do without one, or they can perform some other activity in the hopes that the resource becomes available later. int available() const { return _available; } Finally, what follows is the ‘low-level’ interface to the semaphore. It is completely unsafe, and it is inadvisable to use it directly, other than perhaps in special circumstances. This being C++, such interfaces are commonly made available. Again see std::mutex for an example. However, it would also be an option to be strict about it, make the following 2 methods private, and declare the RAII class defined below, semaphore_lock, to be a friend of this one. bool get() { if ( _available > 0 ) return _available --; 48/90 PB161 Programování v jazyce C++, 16. února 2024 else return false; } void put() { ++ _available; } }; We will want to write a RAII ‘lock holder’ class. However, since get above might fail, we need a way to indicate the failure in the RAII class as well. But constructors don’t return values: it is therefore a reasonable choice to throw an exception. It is reasonable as long as we don’t expect the failure to be a common scenario. class resource_exhausted : public std::runtime_error { public: resource_exhausted() : std::runtime_error( "semaphore full" ) {} }; Now the RAII class itself. It will need to hold a reference to the semaphore for which it holds a lock (good thing the semaphore is not movable, so we don’t have to think about its address changing). Of course, it must not be possible to make a copy of the resource class: we cannot duplicate the resource, which is a lock being held. However, it does make sense to move the lock to a new owner, if the client so wishes. Hence, both a move constructor and move assignment are appropriate. class semaphore_lock { semaphore *_sem = nullptr; public: To construct a semaphore lock, we understandably need a reference to the semaphore which we wish to lock. You might be wondering why the attribute is a pointer and the argument is a reference. The main difference between references and pointers (except the syntactic sugar) is that references cannot be null. In a correct program, all references always refer to valid objects. It does not make sense to construct a semaphore_lock which does not lock anything. Hence the reference. Why the pointer in the attributes? That will become clear shortly. Before we move on, notice that, as promised, we throw an exception if the locking fails. Hence, no noexcept on this constructor. semaphore_lock( semaphore &s ) : _sem( &s ) { if ( !_sem->get() ) throw resource_exhausted(); } As outlined above, semaphore locks cannot be copied or assigned. Let’s make that explicit. semaphore_lock( const semaphore_lock & ) = delete; semaphore_lock &operator=( const semaphore_lock & ) = delete; The new object (the one initialized by the move constructor) is quite unremarkable. The interesting part is what happens to the ‘old’ (source) instance: we need to make sure that when it is destroyed, it does not release the resource (i.e. the lock held) – the ownership of that has been transferred to the new instance. This is where the pointer comes in handy: we can assign nullptr to the pointer held by the source instance. Then we just need to be careful when we release the resource (in the destructor, but also in the move assignment operator) – we must first check whether the pointer is valid. Also notice the noexcept qualifier: even though the ‘normal’ constructor throws, we are not trying to obtain a new resource here, and there is nothing in the constructor that might fail. This is good, because move constructors, as a general rule, should not throw. semaphore_lock( semaphore_lock &&src ) noexcept : _sem( src._sem ) { src._sem = nullptr; } We now define a helper method, release, which frees up (releases) the resource held by this instance. It will do this by calling put on the semaphore. However, if the semaphore is null, we do nothing: the instance has been moved from, and no longer owns any resources. Why the helper method? Two reasons: 1. it will be useful in both the move assignment operator and in the destructor, 2. the client might need to release the resource before the instance goes out of scope or is otherwise destroyed ‘naturally’ (compare std::fstream::close()). void release() noexcept { if ( _sem ) _sem->put(); } Armed with release, writing both the move assignment and the destructor is easy. The move assignment is also noexcept, which is usually a pretty good idea. semaphore_lock &operator=( semaphore_lock &&src ) noexcept { Self-move is not very useful in this case, forbid it. assert( &src != this ); First release the resource held by the current instance. We cannot hold both the old and the new resource at the same time. release(); Now we reset our _sem pointer and update the src instance – the resource is now in our ownership. _sem = src._sem; src._sem = nullptr; return *this; } ~semaphore_lock() noexcept { release(); } }; int main() /* demo */ { semaphore sem( 3 ); sem.get(); semaphore_lock l1( sem ); bool l4_made = false; try { semaphore_lock l2( sem ); assert( sem.available() == 0 ); semaphore_lock l3 = std::move( l2 ); assert( sem.available() == 0 ); semaphore_lock l4 = std::move( l1 ); assert( sem.available() == 0 ); l4_made = true; semaphore_lock l5( sem ); assert( false ); } catch ( const resource_exhausted & ) {} 49/90 assert( l4_made ); assert( sem.available() == 2 ); // clang-tidy: -clang-analyzer-deadcode.DeadStores } 7.e: Elementární příklady 7.e.2 [counter] int counter = 0; Přidejte konstruktory a destruktor typu counted tak, aby počítadlo counter vždy obsahovalo počet existujících hodnot typu counted. Nezapomeňte na pravidlo pěti (rule of five). struct counted; 7.e.3 [coffee] Implement a coffee machine which gives out a token when the order is placed and takes the token back when it is done… at most one order can be in progress. Throw this when the machine is already busy making coffee. class busy {}; And this when trying to use a default-constructed or already-used token. class invalid {}; Fill in the two classes. Besides constructors and assignment operators, add methods make and fetch to machine, to create and redeem tokens re- spectively. class machine; class token; 7.p: Přípravy 7.p.1 [fd] Dle normy POSIX, otevřením souboru nebo podobného zdroje získáme tzv. popisovač otevřeného souboru (angl. file descriptor), malé celé číslo, které pak lze dále předávat systémovým voláním (např. read nebo write). Není-li již zdroj potřebný, popisovač je nutné uvolnit systémovým voláním close (a to právě jednou – je důležité, aby stejný popisovač nebyl uvolněn dvakrát). Naprogramujte typ fd, který bude popisovač souboru uchovávat, a zároveň zabezpečí, že ho není lze omylem ztratit (aniž bychom ho uvolnili) ani omylem uvolnit vícenásobně. Hodnoty typu fd nechť je možné přesouvat (a přiřazovat přesunutím), a vytvářet dvěma způsoby: 1. funkcí fd_open( path, flags ), která vnitřně použije systémové volání open a výsledný popisovač vrátí jako hodnotu typu fd, 2. funkcí fd_dup( raw_fd ), která přijme číselný (syrový, nechráněný) popisovač a systémovým voláním dup vytvoří jeho chráněnou kopii typu fd. Parametr path je typu const char * a stačí jej přeposlat systémovému volání tak, jak ho obdržíte – není potřeba jej jakkoliv kontrolovat nebo interpretovat. Více informací o voláních open a dup získáte příkazy man 2 open a man 2 dup na stroji aisa. Typ fd nechť má dále metody read a write, které vrátí resp. přijmou, vektor bajtů (jako hodnot typu char). Počet bajtů, které je potřeba načíst, dostane metoda write jako parametr. Více informací o potřebných funkcích získáte opět příkazy man 2 read and man 2 write. Selže-li některé ze systémových volání open, read nebo write, ukončete dotčenou funkci výjimkou std::system_error. Pokus o čtení nebo zápis s použitím neplatného popisovače (implicitně sestrojeného, vykradeného, nebo již uzavřeného) nechť skončí výjimkou std::invalid_argument. 7.p.2 [queue] Naprogramujte typ queue, který bude reprezentovat omezenou frontu celých čísel (velikost fronty je zadaná jako parametr konstruktoru), s metodami: • pop() – vrátí nejstarší vložený prvek, • push( x ) – vloží nový prvek x na konec fronty, • empty() vrátí true je-li fronta prázdná. Metody pop a push nechť v případě selhání skončí výjimkou queue_empty resp. queue_full. Všechny operace musí mít složitost O(1). Metody push ani pop nesmí alokovat dodatečnou paměť. struct queue_empty; struct queue_full; struct queue; 7.p.3 [partition] Napište generický podprogram partition( seq, p ), který přeuspořádá zadanou sekvenci tak, že všechny prvky menší než p budou předcházet všem prvkům rovným p budou předcházet všem prvkům větším než p. Sekvence seq má tyto metody: • size() vrátí počet prvků uložených v sekvenci, • swap( i, j ) prohodí prvky na indexech i a j, • compare( i, p ) srovná prvek na pozici i s hodnotou p: ∘ výsledek -1 znamená, že hodnota na indexu i je menší, ∘ výsledek 0, že je stejná, a konečně ∘ výsledek +1, že je větší než p. Metoda compare může skončit výjimkou. V takovém případě vraťte sekvenci seq do původního stavu a výjimku propagujte dál směrem k volajícímu. Hodnoty typu seq nelze kopírovat, máte ale povoleno použít pro výpočet dodatečnou paměť. Metody size ani swap výjimkou skončit nemohou. 7.p.4 [buckets] Napište generický podprogram buckets( list, vec ), který dostane na vstupu: • referenci na seznam tokenů list, který má tyto metody: ∘ front() – vrátí referenci na první token seznamu, ∘ drop() – odstraní první token v seznamu, ∘ empty() – vrátí true je-li seznam prázdný, • referenci na hodnotu vec neurčeného typu, který má metodu size(), a který lze indexovat celými čísly. Uvnitř vec jsou uloženy kontejnery typu, který poskytuje metodu emplace, která vloží prvek kopií nebo přesunem (podle typu parametru), a metodu pop, která odstraní posledně vložený prvek a vrátí ho hodnotou (provede přesun z kontejneru). Tokeny nelze kopírovat ani přiřazovat kopií, pouze přesouvat. Podprogram bude ze vstupního seznamu list postupně odebírat tokeny a bude je vkládat do vec[ i % vec.size() ], kde i je pořadové číslo odebraného tokenu v původním seznamu list (počínaje nulou). Zabezpečte, aby byl výsledek konzistentní, a to i v případě, kdy dojde k výjimce. Selhat mohou tyto operace (a žádné jiné): • alokace paměti v metodě emplace (v takovém případě není prvek vložen, ani není zavolán jeho konstruktor), • metoda drop (odstranění prvku v tomto případě neproběhne). Zejména se nesmí žádný token ztratit, ani nesmí nikde zůstat přebytečný vykradený (moved from) token. 7.p.5 [invest] Vrátíme se k již známému příkladu s bankovním účtem, který tentokrát rozšíříme o práci s výjimkami – konkrétně pokus o výběr z účtu, na kterém není dostatečný zůstatek, skončí výjimkou. Dále přidáme třídu investment, která je „duální“ k půjčce: při sestrojení odečte peníze z účtu, bude se pravidelně zhodnocovat, a při zničení vrátí investované peníze na původní účet. struct insufficient_funds; Typ account nechť má metody balance, deposit and withdraw. Startovní zůstatek je 0 a musí zůstat za všech okolností nezáporný. Jakýkoliv pokus o jeho snížení pod nulu musí skončit výjimkou insuficient_funds. struct account; Konečně typ investment, kterého konstruktor má 3 parametry: 50/90 PB161 Programování v jazyce C++, 16. února 2024 • odkaz (referenci) na hodnotu typu account, • sumu, kterou hodláme investovat, • roční úrok (jako celé číslo v promile). Při sestrojení musí z cílového účtu odebrat potřebné prostředky a při zničení je musí vrátit, včetně nahromaděných úroků. Metoda next_year připočítá příslušný úrok. struct investment; 7.p.6 [linear] † Napište program, který bude řešit systémy lineárních rovnic o dvou neznámých. Rozhraní bude lehce nekonvenční: přetěžte operátory +, * a == a definujte globální konstanty x a y vhodných typů tak, aby bylo možné rovnice zapisovat způsobem, který vidíte níže v proceduře main. Uvědomte si, že návratový typ == nemusí být bool – naopak, může se jednat o libovolný typ, včetně vlastních. Pro samotnou implementaci funkce solve doporučujeme použít Cramerovo pravidlo. Nemá-li zadaný systém řešení, funkce solve nechť skončí výjimkou no_solution (tuto odvoďte od std::exception). Má-li řešení více, je jedno které vrátíte.20 7.r: Řešené úlohy 7.r.1 [printing] Jobs need resources (printing credits, where 1 page = 1 credit) which must be reserved when the job is queued, but are only consumed at actual printing time; jobs can be moved between queues (printers) by the system, and jobs that are still in the queue can be aborted. The class job represents a document to be printed, along with resources that have already been earmarked for its printing. • The constructor should take a numeric identifier, the id of the user who owns the job, and the number of pages (= credits allocated for the job), • method owner should return the id of the owner, • method page_count should return the number of pages. class job; A single queue instance represents a printer. It should have the following methods: • dequeue: consume (print) the oldest job in the queue and return its id, • enqueue: add a job to the queue, • release( id ): remove the job given by id from the queue and return it 20 Jsou-li oba determinanty pomocných matic A1, A2 nulové, systém má libovolně mnoho řešení. Dejte si ale při jejich vyčíslování pozor na dělení nulou. to the caller, • page_count: number of pages in the queue. You can assume that oldest job has the lowest id. class queue; 7.r.2 [bsearch] V tomto cvičení se vrátíme k oblíbenému vyhledávacímu algoritmu půlením intervalu. Budeme implementovat kontejner, který se podobá na std::map, ale budeme předpokládat, že vyhledávání je mnohem častější než vkládání. Proto budeme preferovat strukturu, kde je vyhledávání podle možnosti co nejrychlejší, i za cenu pomalejšího vkládání. Vhodným kandidátem je seřazené pole21 – hledání je logaritmické podobně jako u vyhledávacího stromu, ale data jsou uložena mnohem kompaktněji a proto je i práce s nimi výrazně rychlejší. Implementujte metody emplace, at a contains, s chováním, které odpovídá typu std::map, s výjimkou emplace, který vrátí pouze hodnotu typu bool (iterátory implementovat nebudeme). Konečně, protože neumíme psát generické třídy, klíče i hodnoty budou pevných typů: int pro klíče a token pro hodnoty. struct token { token( int i ) : _value( i ) {} token( const token & ) = delete; token( token && ) = default; token &operator=( const token & ) = delete; token &operator=( token && ) = default; token &operator=( int v ) { _value = v; return *this; } bool operator==( int v ) const { return _value == v; } private: int _value; }; class flat_map; 7.r.4 [tinyvec] † Implement tiny_vector, a class which works like a vector, but instead of allocating memory dynamically, it uses a fixed-size buffer (32 bytes) which is part of the object itself (use e.g. an std::array of bytes). Like earlier, we will use token as the value type. Provide the following methods: 21 Požadujeme, aby kontejner flat_map po libovolné sekvenci operací používal nejvýše dvě souvislé dynamicky alokované oblasti paměti. Kontejner std::vector používá nejvýše jednu. • insert (take an index and an rvalue reference), • erase (take an index), • back and front, with appropriate return types. In this exercise (unlike in most others), you are allowed to use reinter- pret_cast. class token { int _value; bool _robbed = false; public: static int _count; token( int i ) : _value( i ) { ++ _count; } ~token() { -- _count; } token( const token & ) = delete; token( token &&o ) : _value( o._value ) { ++ _count; assert( !o._robbed ); o._robbed = true; } token &operator=( const token & ) = delete; token &operator=( token &&o ) { assert( !o._robbed ); _value = o._value; _robbed = o._robbed; o._robbed = true; return *this; } token &operator=( int v ) { _value = v; _robbed = false; return *this; } bool operator==( int v ) const { assert( !_robbed ); return _value == v; } }; Throw this if insert is attempted but the element wouldn’t fit into the 51/90 buffer. class insufficient_space {}; Hint: Use uninitialized_* and destroy(_at) functions from the memory header. class tiny_vector; int token::_count = 0; 7.r.5 [lock] Implement class lock which holds a mutex locked as long as it exists. The lock instance can be moved around. For simplicity, the mutex itself is immovable. class mutex { bool _locked = false; public: ~mutex() { assert( !_locked ); } mutex() = default; mutex( const mutex & ) = delete; mutex( mutex && ) = delete; mutex &operator=( const mutex & ) = delete; mutex &operator=( mutex && ) = delete; void lock() { assert( !_locked ); _locked = true; } void unlock() { assert( _locked ); _locked = false; } bool locked() const { return _locked; } }; class lock; 7.d: Demonstrace (ukázky) 7.d.1 [iterate] V této ukázce si předvedeme práci s tzv. funkcemi vyššího řádu (higher order) – tedy funkcemi, kterých parametrem je opět funkce. auto iterate( auto f, auto x, int count ) { We want to build a vector of values, starting with x, then f(x), f(f(x)), and so on. Immediately, we face a problem: what should be the type of the vector? We need to specify the type parameter to declare the variable, and this time we won’t be able to weasel out by just saying auto, since the compiler can’t tell the type without an unambiguously typed initializer. We have two options here: 1. we could omit the type parameter of std::vector and let the compiler deduce only that – this would be written std::vector out{ x } – by putting x into the vector right from the start, the compiler can deduce that the element type should be the same as the type of x, whatever that is, 2. we can use decltype to obtain the type of x and use that to specify the required type parameter for out, i.e.: std::vector< decltype( x ) > out; out.push_back( x ); We build the return vector by repeatedly calling f on the previous value, until we hit count items. for ( int i = 1; i < count; ++ i ) out.push_back( f( out.back() ) ); return out; }; int main() /* demo */ { Besides the missing name and the empty square brackets, the signature of the lambda is similar to a standard function. However, on closer inspection, another thing is missing: the return type. This might be specified using -> type after the argument list, but if it is not, the compiler will, again, deduce the type for us. The return type is commonly omitted. auto f = []( int x ) { We return a value just like in a regular function. Please also note the semicolon after the closing brace: definition of a lambda is an expression, and the variable declaration as a whole needs to be delimited by a semicolon, just like in int x = 7;. return x * x; }; auto g = []( int x ) { return x + 1; }; auto v = iterate( f, 2, 4 ); std::vector< int > expect{ 2, 4, 16, 256 }; assert( v == expect ); std::vector< int > iota = iterate( g, 1, 4 ), iota_expect{ 1, 2, 3, 4 }; assert( iota == iota_expect ); } 7.d.2 [convert] In this example, we will implement a class which behaves like a nullable reference to an integer. We will pretend we are in Java and will throw an exception when the user attempts to use a null reference. We first define the type which we will use to indicate an attempt to use an invalid (null) reference. class null_pointer_exception {}; Now for the reference-like class itself. We need two basic ingredients to provide simple reference-like behaviours: we need to be able to (implicitly) convert a value of type maybe_ref to a value of type int. The other part is the ability to assign new values of type int to the referred-to variable, via instances of the class maybe_ref. class maybe_ref { We hold a pointer internally, since real references in C++ cannot be null. int *_ptr = nullptr; We will also define a helper (internal, private) method which checks whether the reference is valid. If the reference is null, it throws the above exception. void _check() const { if ( !_ptr ) throw null_pointer_exception(); } public: Constructors: the default-constructed maybe_ref instances are nulls, they have nowhere to point. Like real references in C++, we will allow maybe_ref to be initialized to point to an existing value. We take the argument by reference and convert that reference into a pointer by using the unary & operator, in order to store it in _ptr. maybe_ref() = default; maybe_ref( int &i ) : _ptr( &i ) {} As mentioned earlier, we need to be able to (implicitly) convert maybe_ref instances into integers. The syntax to do that is operator type, without mentioning the return type (in this case, the return type is given by the name of the operator, i.e. int here). It is also possible to have reference conversion operators, by writing e.g. operator const int &(). However, we don’t need one of those here because int is small, and we can’t have both since that would cause a lot of ambiguity. operator int() const 52/90 PB161 Programování v jazyce C++, 16. února 2024 { _check(); return *_ptr; } The final part is assignment: as you surely remember, operator= should return a reference to the assigned-to instance. It usually takes a const reference as an argument, but again we do not need to do that here. Below in the demo, we will point out where the assignment operator comes into play. maybe_ref &operator=( int v ) { _check(); *_ptr = v; return *this; } }; int main() /* demo */ { int i = 7; When initializing built-in references, we use int &i_ref = i. We can do the same with maybe_ref, but we need to keep in mind that this syntax calls the maybe_ref( int ) constructor, not the assignment operator. maybe_ref i_ref = i; Let us check that the reference behaves as expected. assert( i_ref == 7 ); /* uses conversion to ‹int› */ i_ref = 3; /* uses the assignment operator */ assert( i_ref == 3 ); /* conversion to ‹int› again */ Check that the original variable has changed too. assert( i == 3 ); Let’s also check that null references behave as expected. bool caught = false; maybe_ref null; Comparison will try to convert the reference to int, but that will fail in _check with an exception. try { assert( null == 7 ); } catch ( const null_pointer_exception & ) { caught = true; } Make sure that the exception was thrown and caught. assert( caught ); caught = false; Same but with assignment into the null referenc. try { null = 2; } catch ( const null_pointer_exception & ) { caught = true; } assert( caught ); } 7.e: Elementární příklady 7.e.1 [iota] Implement a generic function iota, which, given a function f, calls f( start ), f( start + 1 ), … f( end - 1 ), in this order. // void iota( … f, int start, int end ); 7.e.2 [accumulate] Write a function accumulate( f, vec ) which will sum up f(x) for all x in the given std::vector< int > vec. // … 7.p: Přípravy 7.p.1 [kernel] Napište funkci kernel, která spočítá rozklad22 množiny celých čísel s podle jádra funkce f. Jádrem funkce myslíme relaci ekvivalence, kde jsou v relaci právě všechny vzory x daného obrazu f(x). Formálněji (x, y) ∈ kerf ↔ f(x) = f(y). Můžete předpokládat, že návratový typ funkce f je int. Časová složitost nesmí být horší, než O(n⋅logn). auto kernel( const std::set< int > &, auto ); 7.p.2 [bitref] Navrhněte typ bitref, který se bude co nejvíce podobat (nekonstantní) referenci na hodnotu typu bool, ale bude „odkazovat“ jediný bit. Bity číslujeme od toho nejméně významného (který má index 0). Je-li • br hodnota typu bitref, • b hodnota typu bool, • f je funkce, která akceptuje parametr typu bool, • off hodnota typu int v rozsahu 0–7 a konečně, • ptr je dereferencovatelná hodnota typu std::byte *, tyto výrazy musí být dobře utvořené: • bitref( ptr, off ) – vytvoří „bitovou referenci“ na off-tý bit bajtu, 22 Rozkladem množiny X podle ekvivalence R myslíme systém množin S, který tuto relaci respektuje: je-li [a] = {b ∈ X|(a, b) ∈ R}, potom S = {[a]|a ∈ X}. na který ukazuje ptr, • br = b – nastaví odkazovaný bit na hodnotu b, • !br, • br & b, b & br, br & br, • podobně pro ostatní binární operátory: |, &, ^, +, -, *, /, %, • br += b, br += br, podobně *=, /=, &=, ^=, |=, • br == b, b == br, br == br, • podobně ostatní relační operátory: <=, >=, <, >, !=, • br++, br--, ++br, --br, • br && b, b && br, br && br, • br || b, b || br, br || br, • f( br ) – zavolá funkci f s hodnotou odkazovaného bitu jako parametrem. Navíc musí být možné použít br jako podmínku příkazů if, while, for. struct bitref; 7.p.3 [combine] Napište čistou, generickou funkci combine( s, f ) kde s je neprázdný indexovatelný kontejner (má tedy metodu size a lze jej indexovat celými čísly) hodnot libovolného typu, a f je binární asociativní funkce, kde: • velikost výsledku funkce f může mít libovolnou monotonní závislost na velikosti vstupů (např. může platit |f(u, v)| = |u| + |v|), • podobně libovolná (ale stále monotonní) je délka výpočtu funkce f v závislosti na velikostech parametrů. Vaším úkolem je pomocí f sestavit z celé posloupnosti s jedinou hodnotu, a to tak, aby se minimalizoval celkový potřebný čas. Příklad: Je-li f lineární (jak ve velikosti výsledku, tak v čase výpočtu) a prvky s mají velikost 1, celková složitost volání combine by neměla přesáhnout O(n⋅log(n)). Příkladem takové funkce f je spojení dvou hodnot typu std::vector za sebe. Příklad: Je-li f kvadratická (opět ve velikosti výsledku i čase), celková složitost by měla být nejvýše O(n2 ⋅ log(n)). Zde by f mohl být například tenzorový součin. 7.p.4 [nibble] In this exercise, we will implement a class that represents an array of nibbles (half-bytes) stored compactly, using a byte vector as backing storage. We will need 3 classes: one to represent referencelike objects: nibble_ref, another for pointer-like objects: nibble_ptr and finally the container to hold the nibbles: nibble_vec. NB. In this exercise, we will not consider const-ness of values stored in the vector.23 23 In particular, obtaining a pointer (e.g. by using begin) will allow you to change the value that it points to, even if nibble_vec itself was marked const. 53/90 The nibble_ref class needs to remember a reference or a pointer to the byte which contains the nibble that we refer to, and whether it is the upper or the lower nibble. With that information (which should be passed to it via a constructor), it needs to provide: • an assignment operator which takes an uint8_t as an argument, but only uses the lower half of that argument to overwrite the pointed-to nibble, • a conversion operator which allows implicit conversion of a nibble_ref to an uint8_t. class nibble_ref; /* reference implementation: 17 lines */ The nibble_ptr class works as a pointer. Dereferencing a nibble_ptr should result in a nibble_ref. To make nibble_ptr more useful, it should also have: • a pre-increment operator, which shifts the pointer to the next nibble in memory. That is, if it points at a lower nibble, after ++x, it should point to an upper half of the same byte, and after another ++x, it should point to the lower half of the next byte, • an equality comparison operator, which checks whether two nibble_ptr instances point to the same place in memory. class nibble_ptr; /* reference implementation: 18 lines */ And finally the nibble_vec: this class should provide 4 methods: • push_back, which adds a nibble at the end, • begin, which returns a nibble_ptr to the first stored nibble (lower half of the first byte), • end, which returns a nibble_ptr past the last stored nibble (i.e. the first nibble that is not in the container), and finally • get( i ) which returns a nibble_ref to the i-th nibble. class nibble_vec; /* reference implementation: 16 lines */ 7.p.5 [higher] Napište funkce map, zip a fold. Pracovat budeme ve všech případech s libovolnými kontejnery, o kterých je zaručeno, že • mají (konstantní) metody begin a end, které vrací • vhodné iterátory, které ∘ mají prefixový a postfixový operátor ++, ∘ operátory rovnosti ==, != a ∘ operátor dereference (unární *). Žádné jiné metody předpokládat nelze. Funkce map má parametry f (funkce) a kontejner c (s prvky takového typu, aby je bylo možné předat funkci f jako parametr). Výsledkem je std::vector hodnot, které vzniknou voláním f na jednotlivé prvky kontejneru c. // … Funkce zip je podobná, ale f je funkce o dvou parametrech a na vstupu jsou dva kontejnery c a d (nemusí být stejného typu). První parametr funkce f nechť pochází z kontejneru c, ten druhý pak z kontejneru d. Nemají-li kontejnery stejnou délku, přebývající hodnoty v tom delším se ignorují. // … Konečně funkce fold bude mít parametry f, i a c, kde f je binární funkce, i je iniciální hodnota a c je vstupní kontejner. Jsou-li c0 … cn prvky c, výsledek funkce fold pak odpovídá f( … f( f( i, c0 ), c1 ), … cn ). Je-li kontejner c prázdný, výsledkem je i. Parametry funkce f mohou být obecně různých typů, musí být ale kompatibilní s i a c. // … 7.p.6 [comp] † Navrhněte typ, který se bude navenek chovat jako sekvenční kontejner dvojic (std::tuple) čísel, ale vnitřně bude data uchovávat ve dvojici kontejnerů (std::vector) celých čísel. Požadované metody: • begin, end a odpovídající zjednodušený iterátor s: ∘ operátory == a !=, ∘ prefixovým operátorem ++, ∘ operátorem dereference (unární *), kde výsledek operátoru * musí být použitelný jako dvojice čísel, včetně std::get a přiřazení do jednotlivých složek. • size, • push_back, • emplace_back (se dvěma parametry typu int, nebo žádným parametrem), • left a right vrátí konstantní referenci na příslušný vnitřní kontejner (konstantní proto, aby nebylo jejich použitím možné porušit potřebné invarianty). Stačí, aby iterace fungovala pro nekonstantní hodnoty typu components (naprogramovat konstantní i nekonstantní iteraci bez duplikace kódu neu- míme). Nápověda: zvažte, jak využít std::tuple s vhodnými parametry. K vyřešení příkladu stačí už probrané konstrukce. struct components; 7.r: Řešené úlohy 7.r.2 [approx] Remember fib.cpp? We can do a bit better. Let’s decompose our golden() function differently this time. The approx function is a higher-order one. What it does is it calls f() repeatedly to improve the current estimate, until the estimates are sufficiently close to each other (closer than the given precision). The init argument is our initial estimate of the result. double approx( auto f, double init, double prec ); Use approx to compute the golden mean. Note that you don’t need to use the previous estimate in your improvement function. Use by-reference captures to keep state between iterations if you need some. double golden( double prec ); 7.r.4 [sort] Implement an in-place selection sort of a vector of integers, using a comparator passed to the sort routine as an argument. A comparator is a function that is used to compare elements instead of the builtin relational operators. This is useful if your data is sorted in non-standard manner. void selectsort( std::vector< int > &to_sort, auto cmp ); 7.r.6 [bsearch] Implement binary search on a vector, with a twist: the order of the elements is given by a comparator. This is a function that is passed as an argument to search and is used to compare elements instead of the builtin relational operators. This is useful if your data is sorted in non-standard manner. // auto search = []( std::vector< int > &vec, int val, auto cmp ); 54/90 PB161 Programování v jazyce C++, 16. února 2024 Část S.2: Ukazatele, výjimky, OOP Druhá sada přináší příklady zaměřené na objektově-orientované programování, na práci s ukazately a výjimkami a v neposlední řadě na správu zdrojů. 1. a_natural – rozšíření s1/f_natural o dělení, 2. b_treap – jednoduchý vyhledávací strom, 3. c_robots – programujeme roboty na mapě, 4. d_network – simulátor počítačové sítě s přepínači, 5. e_tree – práce s heterogenním stromem, 6. f_scrap – hra o zdrojích a zajímání. Příklad a si vystačí se znalostmi z prvního bloku, příklad b lze vyřešit po nastudování 5. kapitoly, příklady c až d vyžadují znalost 6. kapitoly a konečně příklady e, f vyžadují znalost 7. kapitoly. Opět platí, že řešení nějakého příkladu z této sady může být potřebné pro vyřešení příkladu z poslední sady. S.2.a: natural Tento úkol rozšiřuje s1/f_natural o tyto operace (hodnoty m a n jsou typu natural): • konstruktor, který přijme nezáporný parametr typu double a vytvoří hodnotu typu natural, která reprezentuje dolní celou část parametru, • operátory m / n a m % n (dělení a zbytek po dělení; chování pro n = 0 není definované), • metodu m.digits( n ) která vytvoří std::vector, který bude reprezentovat hodnotu m v soustavě o základu n (přitom nejnižší index bude obsahovat nejvýznamnější číslici), • metodu m.to_double() která vrátí hodnotu typu double, která co nejlépe aproximuje hodnotu m (je-li l = log2(m) − 52 a d = m.to_double(), musí platit m - 2l ≤ natural( d ) a zároveň natural( d ) ≤ m + 2l ; jeli m příliš velké, než aby šlo typem double reprezentovat, chování je nedefinované). Převody z/na typ double musí být lineární v počtu bitů operandu. Dělení může mít složitost nejvýše kvadratickou v počtu bitů levého operandu. Metoda digits smí vůči počtu bitů m, n provést nejvýše lineární počet aritmetických operací (na hodnotách m, n). struct natural; S.2.b: treap Datová struktura treap kombinuje binární vyhledávací strom a binární haldu – hodnotu, vůči které tvoří vyhledávací strom budeme nazývat klíč a hodnotu, vůči které tvoří haldu budeme nazývat priorita. Platí pak: • klíč v každém uzlu je větší než klíč v jeho levém potomkovi, a zároveň je menší, než klíč v pravém potomkovi, • priorita je v každém uzlu větší nebo stejná, jako priority obou potomků. Smyslem haldové části struktury je udržet strom přibližně vyvážený. Algoritmus vložení prvku pracuje takto: 1. na základě klíče vložíme uzel na vhodné místo tak, aby nebyla porušena vlastnost vyhledávacího stromu, 2. je-li porušena vlastnost haldy, budeme rotacemi přesouvat nový uzel směrem ke kořenu, a to až do chvíle, než se tím vlastnost haldy obnoví. Budou-li priority přiděleny náhodně, vložení uzlu do větší hloubky vede i k vyšší pravděpodobnosti, že tím bude vlastnost haldy porušena; navíc rotace, které obnovují vlastnost haldy, zároveň snižují maximální hloubku stromu. Implementujte typ treap, který bude reprezentovat množinu pomocí datové struktury treap a poskytovat tyto operace (t je hodnota typu treap, k, p jsou hodnoty typu int): • implicitně sestrojená instance treap reprezentuje prázdnou množinu, • t.insert( k, p ) vloží klíč k s prioritou p (není-li uvedena, použije se náhodná); metoda vrací true pokud byl prvek skutečně vložen (dosud nebyl přítomen), • t.erase( k ) odstraní klíč k a vrátí true byl-li přítomen, • t.contains( k ) vrátí true je-li klíč k přítomen, • t.priority( k ) vrátí prioritu klíče k (není-li přítomen, chování není definováno), • t.clear() smaže všechny přítomné klíče, • t.size() vrátí počet uložených klíčů, • t.copy( v ), kde v je reference na std::vector< int >, v lineárním čase vloží na konec v všechny klíče z t ve vzestupném pořadí, • metodu t.root(), které výsledkem je ukazatel p, pro který: ∘ p->left() vrátí obdobný ukazatel na levý podstrom, ∘ p->right() vrátí ukazatel na pravý podstrom, ∘ p->key() vrátí klíč uložený v uzlu reprezentovaném p, ∘ p->priority() vrátí prioritu uzlu p, ∘ je-li příslušný strom (podstrom) prázdný, p je nullptr. • konečně hodnoty typu treap nechť je možné přesouvat, kopírovat a také přiřazovat (a to i přesunem).24 Metody insert, erase a contains musí mít složitost lineární k výšce stromu (při vhodné volbě priorit tedy očekávaně logaritmickou k počtu klíčů). Metoda erase nemusí nutně zachovat vazbu mezi klíči a prioritami (tzn. může přesunout klíč do jiného uzlu aniž by zároveň přesunula prioritu). struct treap; S.2.c: robots V této úloze budete programovat jednoduchou hru, ve které se ve volném třírozměrném prostoru pohybují robotické entity tří barev: 1. červený robot (třída robot_red): ∘ není-li uzamčený a na hrací ploše je alespoň jeden cizí zelený robot, uzamkne se na ten nejbližší, jinak stojí na místě, ∘ je-li na nějaký robot uzamčený, přibližuje se přímo k němu (tzn. směr pohybu je vždy v aktuálním směru tohoto robotu), 2. zelený robot (třída robot_green): ∘ je-li nějaký cizí modrý robot ve vzdálenosti do 10 metrů, směruje přímo k tomu nejbližšímu, ∘ je-li nejbližší takový robot dále než 10 metrů, zelený robot se teleportuje do místa, které leží na jejich spojnici, 8 metrů od cílového robotu na jeho vzdálenější straně, ∘ jinak stojí na místě. 3. modrý robot (třída robot_blue): ∘ směruje k nejbližšímu cizímu červenému robotu, existuje-li nějaký, ∘ jinak se poloviční rychlostí pohybuje po přímce ve směru, který měl naposledy, ∘ na začátku hry je otočen směrem k začátku souřadnicového systému (je-li přímo v počátku, chování není určeno). Roboty se pohybují rychlostí 15 m/s. Dostanou-li se dva roboty různých barev a různých vlastníků do vzdálenosti 1 metr nebo menší, jeden z nich zanikne podle pravidla: • červený vítězí nad zeleným, • zelený vítězí nad modrým a konečně • modrý vítězí nad červeným. Hra jako celek nechť je zapouzdřená ve třídě game. Bude mít tyto metody: 24 Verze s přesunem můžete volitelně vynechat (v takovém případě je označte jako smazané). Budou-li přítomny, budou testovány. Implementace přesunu je podmínkou hodnocení kvality známkou A. 55/90 • metoda tick posune čas o 1/60 sekundy, a provede tyto akce: a. všechny roboty se posunou na své nové pozice zároveň, tzn. dotáže-li se nějaký robot na aktuální pozici jiného robotu, dostane souřadnice, které měl tento na konci předchozího tiku, b. vzájemné ničení robotů, které proběhne opět zároveň – sejdou-li se v dostatečné blízkosti roboty všech tří barev, zaniknou všechny. • metoda run simuluje hru až do jejího konce, ∘ tzn. do chvíle, kdy nemůže zaniknout žádný další robot a ∘ vrátí dvojici (počet tiků, hráči), ∘ kde „hráči“ je vektor identifikátorů hráčů, seřazený podle počtu zbývajících robotů; je-li více hráčů se stejným počtem robotů, první bude ten s větším počtem červených, dále zelených a nakonec modrých robotů; je-li stále hráčů více, budou uspořádáni podle svého identifikátoru, • metody add_X pro X = red, green nebo blue, které přidají do hry robota odpovídající barvy, a které mají dva parametry: ∘ počáteční pozici, jako trojici hodnot typu double (zadané v metrech v kartézské soustavě), ∘ nenulový celočíselný identifikátor hráče-vlastníka. struct robot_red; struct robot_green; struct robot_blue; struct game; S.2.d: network Vaším úkolem bude tentokrát naprogramovat simulátor počítačové sítě, s těmito třídami, které reprezentují propojitelné síťové uzly: • endpoint – koncový uzel, má jedno připojení k libovolnému jinému uzlu, • bridge – propojuje 2 nebo více dalších uzlů, • router – podobně jako bridge, ale každé připojení musí být v jiném segmentu. Dále bude existovat třída network, která reprezentuje síťový segment jako celek. Každý uzel patří právě jednomu segmentu. Je-li segment zničen, musí být zničeny (a odpojeny) i všechny jeho uzly. Třída network bude mít tyto metody pro vytváření uzlů: • add_endpoint() – vytvoří nový (zatím nepřipojený) koncový uzel, převezme jeho vlastnictví a vrátí vhodný ukazatel na něj, • add_bridge( p ) – podobně pro p-portový bridge, • add_router( i ) – podobně pro směrovač s i rozhraními. Jsou-li m a n libovolné typy uzlů, musí existovat vhodné metody: • m->connect( n ) – propojí 2 uzly. Metoda je symetrická v tom smyslu, že m->connect( n ) a n->connect( m ) mají tentýž efekt. Metoda vrátí true v případě, že se propojení podařilo (zejména musí mít oba uzly volný port). • m->disconnect( n ) – podobně, ale uzly rozpojí (vrací true v případě, že uzly byly skutečně propojené). • m->reachable( n ) – ověří, zda může uzel m komunikovat s uzlem n (na libovolné vrstvě, tzn. včetně komunikace skrz routery; jedná se opět o symetrickou vlastnost; vrací hodnotu typu bool). Konečně třída network bude mít tyto metody pro kontrolu (a případnou opravu) své vnitřní struktury: • has_loops() – vrátí true existuje-li v síti cyklus, • fix_loops() – rozpojí uzly tak, aby byl výsledek acyklický, ale pro libovolné uzly, mezi kterými byla před opravou cesta, musí platit, že po opravě budou nadále vzájemně dosažitelné. Cykly, které prochází více sítěmi (a tedy prohází alespoň dvěma směrovači), neuvažujeme. class endpoint; class bridge; class router; class network; S.2.e: tree Uvažujme stromovou strukturu, která má 4 typy uzlů, a která představuje zjednodušený JSON: • node_bool – listy typu bool, • node_int – listy typu int, • node_array – indexované celými čísly souvisle od nuly, • node_object – klíčované libovolnými celými čísly. Typ tree pak reprezentuje libovolný takový strom (včetně prázdného a jednoprvkového). Pro hodnoty t typu tree, n libovolného výše uvedeného typu node_X a idx typu int, jsou všechny níže uvedené výrazy dobře utvořené. Práce s hodnotami typu tree: • t.is_null() – vrátí true reprezentuje-li tato hodnota prázdný strom, • *t – platí-li !t.is_null(), jsou (*t) a n záměnné, jinak není definováno, • implicitně sestrojená hodnota reprezentuje prázdný strom, • hodnoty typu tree lze také vytvořit volnými funkcemi make_X, kde výsledkem je vždy strom s jediným uzlem typu node_X (v případě make_bool resp. make_int s hodnotou false resp. 0, není-li v parametru uvedeno jinak). Hodnoty typu node_X lze sestrojit implicitně, a reprezentují false, 0 nebo prázdné pole (objekt). Skalární operace (výsledkem je zabudovaný skalární typ): • n.is_X() – vrátí true, je-li typ daného uzlu node_X (např. is_bool() určí, je-li uzel typu node_bool), • n.size() vrátí počet potomků daného uzlu (pro listy 0), • n.as_bool() vrátí true je-li n uzel typu node_bool a má hodnotu true, nebo je to uzel typu node_int a má nenulovou hodnotu, nebo je to neprázdný uzel typu node_array nebo node_object, • n.as_int() vrátí 1 nebo 0 pro uzel typu node_bool, hodnotu uloženou n uzlu node_int, nebo skončí výjimkou std::domain_error. Operace přístupu k potomkům: • n.get( idx ) vrátí odkaz (referenci) na potomka: ∘ s indexem (klíčem) idx vhodného typu tak, aby s ní bylo možné pracovat jako s libovolnou hodnotou typu node_X, nebo ∘ skončí výjimkou std::out_of_range když takový potomek neexistuje, • n.copy( idx ) vrátí potomka na indexu (s klíčem) idx jako hodnotu typu tree, nebo skončí výjimkou std::out_of_range neexistuje-li takový. Operace, které upravují existující strom: • n.set( idx, t ) nastaví potomka na indexu nebo u klíče i na hodnotu t, přitom samotné t není nijak dotčeno, přitom: ∘ je-li n typu node_array, je rozšířeno dle potřeby tak, aby byl idx platný index, přitom takto vytvořené indexy jsou prázdné), ∘ je-li n typu node_bool nebo node_int, skončí s výjimkou std::do- main_error, • n.take( idx, t ) totéž jako set, ale podstrom je z t přesunut, tzn. metoda take nebude žádné uzly kopírovat a po jejím skončení bude platit t.is_null(). Všechny metody a návratové hodnoty referenčních typů musí správně pracovat s kvalifikací const. Vytvoříme-li kopii hodnoty typu tree, tato bude obsahovat kopii celého stromu. Je-li umožněno kopírování jednotlivých uzlů, nemá určeno konkrétní chování. class node_bool; class node_int; class node_array; class node_object; class tree; 56/90 PB161 Programování v jazyce C++, 16. února 2024 Část 8: Součtové typy Přípravy:25 1. bounds – intervaly platných hodnot, 2. arary – dvourozměrné pole se speciálními řádky, 3. intmap – složené zobrazení celočíselných polí, 4. program – reprezentace jednoduchého programu, 5. eval – vyhodnocení výrazu zadaného stromem, 6. anyarr – dynamické pole hodnot libovolného typu. Rozšířené úlohy: TBD 8.p: Přípravy 8.p.1 [bounds] Implementujte třídu bounds, která si bude pro každý zadaný celočíselný klíč pamatovat rozsah povolených hodnot. Přitom mez typu unbounded bude znamenat, že v daném směru není hodnota příslušná danému klíči nijak omezena. struct unbounded {}; Samotná třída bounds bude mít metody: • set_lower( k, b ) nastaví spodní mez b pro klíč k (b je buď 64b celé číslo, nebo hodnota unbounded{}), • set_upper( k, b ) obdobně, ale horní mez, • set_default_lower( b ) nastaví implicitní dolní mez (platí pro klíče, kterým nebyla nastavena žádná jiná), • set_default_upper( b ) obdobně, ale horní mez, • valid( k, i ) vrátí true právě když hodnota i spadá do mezí platných pro klíč k. Všechny takto zadané intervaly jsou oboustranně otevřené. struct bounds; 8.p.2 [array] Implementujte dvourozměrné pole, kde vnitřní pole na daném indexu může být buď obyčejné pole celých čísel (std::vector), nebo konstantní pole neomezené délky, nebo neomezené pole, kde hodnota na libovolném indexu je rovna tomuto indexu. Metody: • a.get( i, j ) vrátí hodnotu (typu int) na zadaných souřadnicích, nebo vyhodí výjimku std::out_of_range, není-li některý index platný, • a.size() vrátí délku vnějšího pole, • a.size( i ) vrátí délku i-tého vnitřního pole (pro neomezená vnitřní 25 Tato kapitola neobsahuje ukázky ani elementární příklady – potřebné informace naleznete v přednášce. pole vrátí INT_MAX), • po volání a.append_iota() pro libovolné i platí a.get( a.size() - 1, i ) == i, • po volání a.append_const( n ) pro libovolné i platí a.get( a.size() - 1, i ) == n, • pro vektor čísel v volání a.append( v ) vloží v jako poslední prvek vnějšího pole a. struct array; 8.p.3 [intmap] Navrhněte typ, který bude reprezentovat operaci nad polem celých čísel. Vyhodnocení sestavené operace se provede metodou: • eval( v ) – aplikuje operaci na vektor celých čísel v (přepsáním vstupního vektoru), Celkový efekt operace bude lze postupně zadat připojováním elementárních operací „na konec“ stávající operace op (n představuje celé číslo, v představuje vektor celých čísel): • op.add( n ) – přičte ke všem prvkům vstupu hodnotu n (tzn. out[ i ] = in[ i ] + n), • op.add( v ) – cyklicky přičte hodnoty z v ke vstupnímu vektoru (tzn. out[ i ] = in[ i ] + v[ i ]; padne-li i mimo rozsah vektoru v, pokračuje se opět prvním prvkem v, atd. – můžete předpokládat, že v není prázdné), • op.rotate( n ) – přesune prvek na indexu i na index i + n (tzn. out[ i + n ] = in[ i ]; je-li i + n mimo meze, použije se vhodný index v mezích tak, aby operace realizovala rotaci), • op.pop() – zapomene posledně vloženou elementární operaci. struct intmap; 8.p.4 [program] Implementujte typ program, který bude reprezentovat výpočet nad stavem určeným dvojicí celých čísel. Na konec stávajícího výpočtu je možné přidat další krok metodou append, která přijme libovolnou funkci, které lze předat 2 celá čísla. struct program; 8.p.5 [eval] Máte zadané následující typy, které reprezentují jednoduchý aritmetický výraz. struct constant; struct add; struct subtract; struct multiply; struct divide; using expr = std::variant< constant, add, subtract, multiply, divide >; using expr_ptr = std::unique_ptr< expr >; struct constant { constant( int v ) : value( v ) {} int value = 0; }; struct binary { expr_ptr left, right; binary( expr a, expr b ); }; struct add : binary { using binary::binary; }; struct subtract : binary { using binary::binary; }; struct multiply : binary { using binary::binary; }; struct divide : binary { using binary::binary; }; binary::binary( expr a, expr b ) : left{ std::make_unique< expr >( std::move( a ) ) }, right{ std::make_unique< expr >( std::move( b ) ) } {} Vaším úkolem je naprogramovat funkci eval, která takto zadaný výraz vyhodnotí na celé číslo. 8.p.6 [anyarr] Naprogramujte typ any_array, který bude reprezentovat dynamické pole libovolných hodnot, a bude mít tyto metody: • size – vrátí počet uložených hodnot, • append – přijme hodnotu libovolného typu a vloží ji na konec pole, • transform_int – přijme libovolnou unární funkci int f( int ), a každou uloženou hodnotu x typu int upraví na f( x ) (přitom ostatní hodnoty nezmění), • remove_integers – odstraní hodnoty typu int, • remove_floats – odstraní hodnoty typu float a double, • equals – přijme index i a hodnotu libovolného typu v a vrátí true je-li na indexu i uložena hodnota stejného typu jako v a tyto hodnoty se rovnají. Metody remove_integers a remove_floats musí mít nejvýše lineární časovou složitost, zatímco metoda equals konstantní. 57/90 struct any_array; 8.r: Řešené úlohy 8.r.1 [null] V tomto cvičení se budeme zabývat kontejnery, ve kterých mohou některé hodnoty chybět – takové hodnoty budeme reprezentovat pomocí std::nullopt. V následovných funkcích nechť platí, že výsledek operace, kde alespoň jeden operand je std::nullopt je opět std::nullopt. Implementujte: • filter, která ze zadané sekvence odstraní prázdné hodnoty, • zip, která dostane dvě posloupnosti a funkci, kterou po dvojicích aplikuje a tím vytvoří novou posloupnost (jako hodnotu typu std::vector), • join, která ze zadaných posloupností a binárního predikátu vytvoří posloupnost dvojic (kartézský součin), ale jen takových, které splňují zadaný predikát. Hodnotu std::nullopt interpretovanou jako pravdivostní hodnotu chápeme jako ekvivalent false. 8.r.2 [rel] Naprogramujte typ, který bude reprezentovat symetrickou binární relaci na celých číslech s těmito metodami: • add poznamená, že zadaná čísla jsou v relaci, • test ověří, zda jsou zadaná čísla v relaci, • get vrátí množinu čísel, která jsou se zadaným v relaci, • set_filter nastaví filtr (binární predikát) – pomyslně z relace odstraní dvojice, které predikát nesplňují, • unset_filter zruší nastavený filtr. Pozor, nastavením filtru se nemění sestavená relace, pouze dotazy na ni. Je-li filtr odstraněn, relace se tím vrátí do původního stavu. struct relation; 8.r.3 [robot] Navrhněte typy program a grid, které budou reprezentovat jednoduchého programovatelného robota, který se pohybuje v neomezené dvourozměrné mřížce. Políčka v mřížce mají dva stavy: označeno a neoznačeno. Na začátku jsou všechna políčka neoznačena, robot stojí na souřadnicích 0, 0 a je orientován horizontálně. Program lze rozšířit metodou append, která přijme jako parametr libovolný typ příkazu. Sestavený program lze vykonat volnou funkcí run, která dostane jako druhý parametr referenci na hodnotu typu grid. Funkce run jednak upraví vstupní mřížku, jednak vrátí koncové souřadnice robota. Příkaz walk robota posune o příslušný počet políček podle aktuální orien- tace: • horizontální – kladná čísla znamenají východ, • vertikální – kladná čísla znamenají sever. Která vzdálenost se použije závisí na tom, je-li startovní políčko označené. struct walk { int if_marked = 0; int if_unmarked = 0; }; Příkaz turn robota přepíná mezi horizontálním a vertikálním směrem pohybu. struct turn {}; Příkaz toggle změní označenost políčka, na kterém robot aktuálně stojí. Jeli příznak sticky nastaven na true, již označené políčko zůstane označené. struct toggle { bool sticky = false; }; struct program; struct grid; std::tuple< int, int > run( const program &, grid & ); 8.r.4 [sumseq] V tomto cvičení budeme programovat funkce, které pracují se součtovými posloupnostmi, které mohou obsahovat prvky dvou typů (budeme je označovat jako levý a pravý). Konkrétní reprezentace takové posloupnosti není určena – musí být pouze kompatibilní mezi jednotlivými funkcemi. První funkcí, kterou naprogramujeme, bude select – má 3 parametry, 2 obyčejné posloupnosti (obecně různých typů) a binární funkci choose. Výsledkem bude součtová posloupnost, kde na každé pozici bude hodnota ze stejné pozice některé vstupní posloupnosti – která to bude určí funkce choose, které návratová hodonta je typu choice: enum class choice { left, right }; // … Dále definujeme funkce left a right, které ze zadané „součtové“ posloupnosti vyberou prvky pouze levého (pravého) typu a vrátí je jako obyčejnou posloupnost (reprezentovanou jako std::vector). // … Konečně definujeme funkci map, která obdrží součtovou posloupnost a dvě funkce, které zobrazí hodnoty levého/pravého typu na libovolný společný typ. Výsledkem je obyčejná posloupnost vhodného typu (opět reprezentovaná jako std::vector). // … 58/90 PB161 Programování v jazyce C++, 16. února 2024 Část 9: Knihovna algoritmů Přípravy:26 1. set – množinové operace, 2. wsort – paralelní řazení dvou posloupností, 3. distinct – počítání unikátních prvků, 4. reorder – stabilní řazení s malou množinou klíčů, 5. strides – řazení již seřazených běhů, 6. intervals – jednoduchý algoritmus v duchu std. 9.p: Přípravy 9.p.1 [set] Implementujte množinu libovolných celých čísel, s těmito ope- racemi: • sjednocení operátorem |, • průnik operátorem &, • rozdíl operátorem -, • uspořádání inkluzí relačními operátory. Všechny výše uvedené operace musí být nejvýše lineární v součtu velikostí vstupních množin. Typ set doplňte metodami add (přidá prvek) a has (ověří přítomnost prvku), které mají nejvýše logaritmickou složitost. struct set; 9.p.2 [wsort] Implementujte generický podprogram weighted_sort( seq1, seq2, weight ) kde: 1. seq1 a seq2 jsou dva stejně dlouhé vektory prvků blíže neurčeného typu (nemusí být možné je kopírovat), 2. weight je funkce, která jako parametry přijme jeden prvek ze seq1 a jeden prvek ze seq2 a přiřadí jim celočíselnou váhu. Vaším úkolem je paralelně přeuspořádat obě sekvence seq1, seq2 tak, aby na konci pro každý platný index i ≥ 1 platilo: weight( seq₁[ i - 1 ], seq₂[ i - 1 ] ) <= weight( seq₁[ i ], seq₂[ i ] ) Paralelnost přeuspořádání v tomto případě znamená, že: • byl-li původní prvek seq1[ i ] přesunut na novou pozici seq1[ j ], • byl zároveň prvek seq2[ i ] přesunut na pozici seq2[ j ]. 9.p.3 [distinct] Napište čistou, generickou funkci distinct( s ), která 26 Tato kapitola neobsahuje ukázky ani elementární příklady – potřebné informace naleznete v přednášce. spočítá, kolik různých prvků se objevuje ve vzestupně seřazené posloupnosti s. Zadaná posloupnost je hodnota typu, který poskytuje metody begin a end; výsledné iterátory lze efektivně (v konstantním čase) posouvat o libovolný počet pozic (např. funkcí std::next) a lze také efektivně získat vzdálenost dvou iterátorů (např. funkcí std::distance). Prvky nemusí být možné kopírovat, ale lze je libovolně srovnávat a přesouvat. Funkce musí pracovat v čase nejvýše O(k ⋅ log(n)), kde k je počet různých prvků (výsledek volání distinct) a n je délka vstupní posloupnosti. 9.p.4 [reorder] Napište podprogram reorder( s, weight ), který pro zadanou posloupnost s a funkci weight na místě přeuspořádá s tak, že pro u < w budou prvky s váhou u předcházet prvkům s váhou w. Zároveň pro prvky se stejnou váhou platí, že se objeví ve stejném pořadí, v jakém byly v původní posloupnosti s. Podprogram musí pracovat v čase nejvýše O(k ⋅ n) kde k je počet různých vah, které se objeví na vstupu, a n je délka posloupnosti s. Je také povoleno využít lineární množství dodatečné paměti. 9.p.5 [strides] Naprogramujte generickou proceduru stride_sort( seq, key ), která seřadí vstupní posloupnost seq podle klíče zadaného unární funkcí key, a to v čase O(k ⋅ log(k) + n), kde k je počet již správně seřazených běhů. Běhy ve vstupní posloupnosti se nepřekrývají, tzn. pro libovolné dva běhy s, t platí buď max(s) < min(t) nebo naopak max(t) < min(s). Pro zcela seřazený vstup je k = 1. Zvažte jak, a za jakou cenu, by šlo algoritmus zobecnit tak, aby se vstupní běhy mohly překrývat (a zároveň zůstal pro malá k výrazně efektivnější než obecné řazení). 9.p.6 [intervals] Naprogramujte proceduru intervals, která z posloupnosti k dvojic (zleva uzavřených, zprava otevřených intervalů) vytvoří vzestupně seřazenou posloupnost n prvků tak, že každá hodnota, která spadá do některého vstupního intervalu, se ve výstupní posloupnosti objeví právě jednou. Procedura intervals bude mít rozhraní podobné standardním algoritmům: • vstupem bude dvojice (rozsah) iterátorů, které zadávají sekvenci dvojic–intervalů (std::tuple), • a výstupní iterátor, do kterého zapíše výslednou posloupnost. Můžete předpokládat, že prvky (a tedy i intervaly zadané jako jejich dvojice) lze kopírovat a přiřazovat. Algoritmus by měl mít složitost O(n+ k ⋅ log(k)). 9.r: Řešené úlohy 9.r.1 [priority] Navrhněte typ sched_queue, který bude udržovat prioritní frontu hodnot typu task, uspořádanou podle složky priority. Mají-li dva prvky stejnou prioritu, přednost má ten s nižším id. Typ task nemodi- fikujte. struct task { int priority, static_priority, id; }; Typ sched_queue nechť poskytuje tyto operace: • add vloží prvek do fronty, • peek vrátí konstantní odkaz na prvek s nejvyšší prioritou, • demote podobně, ale vrátí nekonstantní odkaz a zároveň prvku sníží prioritu o jedna, • reset nastaví prioritu všech prvků na jejich hodnotu static_priority. Všechny operace s výjimkou peek mohou zneplatnit reference vrácené některým předchozím voláním peek nebo demote. struct sched_queue; 9.r.2 [join] Naprogramujte generickou proceduru join, která bude mít 5 parametrů: • dvě relace A, B na n resp. m hodnotách typu T, reprezentované jako std::vector hodnot typu std::array, • číslo j < n určující sloupec (index) v první relaci a číslo k < m podobně určující sloupec ve druhé relaci, • výstupní vektor vhodného typu, do kterého zapíše výsledek. Procedura nesmí vstupní parametry (zejména vstupní relace) nijak měnit. Hodnoty v relacích lze kopírovat a implicitně sestrojovat. Výsledkem bude jediná relace R na n + m − 1 hodnotách taková, že: • (a0, …, aj, …, an, b0, …, bk−1, bk+1, …, bm) ∈ R právě když • (a0, …, an) ∈ A a zároveň • (b0, …, bk−1, aj, bk+1, …bm) ∈ B. V relačních databázových systémech se této operaci říká přirozené spojení (za předpokladu, že i, j označují jediný společný sloupec). 9.r.3 [sorted] Napište čistou funkci sorted_ranges, která na vstupu dostane kontejner (nebo rozsah ve smyslu std::range) a kladné číslo n. 59/90 Vstupní posloupnost interpretujte jako obdélníkové pole šířky n. Můžete předpokládat: • iterátory lze efektivně posouvat o zadaný počet pozic (např. funkcí std::next), • délka vstupní posloupnosti je dělitelná n. Výstupem bude std::vector dvojic (index, délka), které pro každý řádek vstupu udávají nejdelší uspořádanou posloupnost nacházející se na tomto řádku. Je-li takových posloupností víc, použije se index první z nich. Celková složitost nechť je O(m) kde m je délka vstupní posloupnosti. 9.r.4 [rotsort] Napište funkci rotate_sort, která dostane podobně jako v předchozím cvičení na vstupu kontejner (nebo rozsah ve smyslu std::range) a kladné číslo n; vstupní posloupnost pak interpretuje jako obdélníkové pole šířky n. Můžete předpokládat, že: • iterátory lze efektivně posouvat o zadaný počet pozic (např. funkcí std::next), • délka vstupní posloupnosti je dělitelná n. Funkce ověří, je-li možné každý řádek vzestupně seřadit jednou rotací (tzn. existuje-li k takové, že posunem prvků o k mod n doprava nebo doleva vznikne seřazená posloupnost). Je-li to možné, tyto rotace provede a vrátí true. Jinak vstupní sekvenci nijak nemodifikuje a vrátí false. Celková složitost nechť je O(m) kde m je délka vstupní posloupnosti. 9.r.5 [permute] Given a number n and a base b, find all numbers whose digits (in base b) are a permutation of the digits of n and return them as a vector of integers. Each such number should appear exactly once. Examples: (125)₁₀ → { 125, 152, 215, 251, 512, 521 } (1f1)₁₆ → { (1f1)₁₆, (f11)₁₆, (11f)₁₆ } (20)₁₀ → { 20, 2 } std::vector< unsigned > permute_digits( unsigned n, int base ); std::vector< unsigned > to_digits( unsigned n, int base, int fill = 0 ) { std::vector< unsigned > ds; while ( n > 0 || fill > 0 ) { ds.push_back( n % base ); n /= base; -- fill; } return ds; } 60/90 PB161 Programování v jazyce C++, 16. února 2024 Část 10: Iterators XXX Demonstrations: 1. queue – an iterable queue 2. split – chop up a string_view into pieces 3. glob – iterate over matches of a pattern in a string Elementary exercises: 1. iota – an iterable sequence of integers 2. view – iterate a slice of an existing collection 3. skip – iterate every n-th item (a stride) of a collection Preparatory exercises: 1. seq – generic sequences 2. filter – filtered sequences 3. zip – iterate two sequences in lockstep 4. nibble – a fixed-size nibble array 5. tree – in-order iteration of a tree 6. scan – generalized prefix sum Regular exercises: 1. map – applying a function to a sequence 2. range – views with a shared backing store 3. permute – iterate all permutations of a sequence 4. critbit – iterate a ‘critbit’ binary trie in order 5. matrix † – iterate a compact rectangular array 6. bits – iterate bits in a word 10.d: Demonstrace (ukázky) 10.d.1 [split] Let’s implement a pure function split, which given a string view s and a delimiter delim, produces a pair of string_views a and b such that: • delim is not in a, • and either ∘ s == a + delim + b if delim was present in s, ∘ s == a and b is empty otherwise using split_view = std::pair< std::string_view, std::string_view >; split_view split( std::string_view s, char delim ) { size_t idx = s.find( delim ); if ( idx == s.npos ) return { s, "" }; else return { s.substr( 0, idx ), s.substr( idx + 1, s.npos ) }; } int main() /* demo */ { auto [ a, b ] = split( "hello world", ' ' ); assert( a == "hello" ); assert( b == "world" ); auto [ c, d ] = split( "hello world", '!' ); assert( c == "hello world" ); assert( d == "" ); return 0; } 10.d.2 [glob] V této ukázce budeme implementovat jednoduché srovnání řetězce se vzorkem, který má dva typy zástupných znaků: • * nahrazuje libovolnou posloupnost znaků (i prázdnou), • % funguje stejně, ale místo nejdelší možné posloupnosti vybere nejkratší možnou (rozdíl se projeví pouze v přiřazení podřetězců jednotlivým zástupným znakům; viz také main). Speciální znaky lze „vypnout“ tím, že jim předepíšeme znak \. Krom samotné informace, zda zadaný řetězec vzorku vyhovuje, budeme také požadovat řetězce, které ve zpracovaném textu odpovídaly jednotlivým zástupným znakům (např. proto, abychom je mohli něčím nahradit) – těmto budeme říkat „zachycené“ (angl. captured). Samotné hledání vzorku implementujeme rekurzivně. Aktuální „pohled“ do vzorku i do řetězce budeme reprezentovat typem std::string_view, stejně tak zachycené podřetězce. Parametr pat reprezentuje vzorek, který musíme v řetězci str nalézt. Rekurzivní volání odstraňují znaky ze začátku pat a/nebo str tak, aby tyto obsahovaly dosud nezpracované sufixy. Parametry c_idx a c_len popisují právě řešený zástupný znak – c_idx je jeho pořadové číslo (a tedy index v parametru capture, kterému odpovídá) a c_len je délka prozatím zachyceného podřetězce (je-li c_len nula, žádný zástupný znak aktivní není.) bool glob_match( std::string_view pat, std::string_view str, std::vector< std::string_view > &capture, int c_idx, int c_len ) { Nejprve vyřešíme triviální případy: prázdný řetězec vyhovuje prázdnému vzorku, nebo vzorku který obsahuje jediný zástupný znak, naopak je-li jedna strana neprázdná a druhá prázdná, shoda je vyloučena. if ( str.empty() && ( pat == "" || pat == "*" || pat == "%" ) ) return true; if ( pat.empty() || str.empty() ) return false; Nemáme-li rozpracovaný žádný zástupný znak, poznačíme si možný začátek zachyceného řetězce. Není-li následující znak zástupný, poznačený začátek se při zpracování dalšího znaku vzorku posune. Nemůžeme se zde rozhodovat podle prvního znaku vzorku, protože ten již může být rozpracovaný. if ( !c_len ) capture[ c_idx ] = str; Zpracování zástupného znaku má dvě možná pokračování: zachycení můžeme ukončit a pokračovat ve srovnání vzorku dalším znakem. V takovém případě se posuneme se na další index c_idx a vynulujeme c_len. Posun na další znak vzorku realizuje metoda substr typu std::string_view – první parametr je index, od kterého má nový pohled začínat, druhý (volitelný) určuje délku nového pohledu (implicitně je maximální možná, tzn. do konce „rodičovského“ pohledu). Vede-li navíc ukončení záchytu k celkovému úspěchu, uložíme výsledný zachycený řetězec na příslušný index seznamu capture. auto end_capture = [&] { auto p_suf = pat.substr( 1 ); bool m = glob_match( p_suf, str, capture, c_idx + 1, 0 ); if ( m ) capture[ c_idx ] = capture[ c_idx ].substr( 0, c_len ); return m; }; Druhou možností je rozpracovaný zachycený řetězec prodloužit o jeden znak a pokračovat tak v zpracování aktivního zástupného znaku. Vzorek tedy zachováme (první znak je stále zástupný) a ve zpracovaném řetězci se o jeden znak posuneme (opět metodou substr). auto extend_capture = [&] 61/90 { auto s_suf = str.substr( 1 ); return glob_match( pat, s_suf, capture, c_idx, c_len + 1 ); }; Začíná-li vzorek speciálním znakem (zástupným nebo \), zpracujeme jej. Nejkratší shodu se pokusíme přednostně ukončit a prodloužíme ji pouze v případě, kdy toto rozhodnutí nevede k nalezení shody. Naopak hladovou (nejdelší) shodu se prioritně pokusíme prodloužit, a ukončíme ji pouze v situaci, kdy by prodloužení zabránilo nalezení shody. switch ( pat[ 0 ] ) { case '%': return end_capture() || extend_capture(); case '*': return extend_capture() || end_capture(); case '\\': pat.remove_prefix( 1 ); break; default: ; } Je-li vzorek nebo řetězec vyčerpán, nebo vzorek začíná obyčejným znakem, který se neshoduje s odpovídajícím znakem řetězce, shodu jsme nenašli. if ( pat.empty() || str.empty() || pat[ 0 ] != str[ 0 ] ) return false; V opačném případě je možné dosud nalezenou shodu prodloužit o jeden znak a pokračovat ve zpracování sufixů. pat.remove_prefix( 1 ); str.remove_prefix( 1 ); return glob_match( pat, str, capture, c_idx, 0 ); } Před samotným rekurzivním zpracováním si nachystáme seznam zachycených řetězců (abychom nemuseli při zpracování jednotlivých znaků neustále kontrolovat, máme-li v capture dostatek místa). Pro pohodlnější použití zachycené řetězce předáme volajícímu jako součást návratové hodnoty. auto glob_match( std::string_view pat, std::string_view str ) { std::vector< std::string_view > capture; char prev = 0; for ( char c : pat ) { if ( ( c == '*' || c == '%' ) && prev != '\\' ) capture.emplace_back(); prev = c; } bool match = glob_match( pat, str, capture, 0, 0 ); if ( !match ) capture.clear(); return std::tuple{ match, capture }; } int main() /* demo */ { using sv_vec = std::vector< std::string_view >; bool m; sv_vec capture; std::tie( m, capture ) = glob_match( "%.*", "x.y.z" ); assert(( m && capture == sv_vec{ "x", "y.z" } )); std::tie( m, capture ) = glob_match( "*.%", "x.y.z" ); assert(( m && capture == sv_vec{ "x.y", "z" } )); std::tie( m, capture ) = glob_match( "%.%", "x.y.z" ); assert(( m && capture == sv_vec{ "x", "y.z" } )); std::tie( m, capture ) = glob_match( "\\%.%", "%.y.z" ); assert(( m && capture == sv_vec{ "y.z" } )); std::tie( m, capture ) = glob_match( "%.%", "x:y:z" ); assert(( !m && capture == sv_vec{} )); } 10.d.3 [splitter] † In this demo, we will implement a full-featured input iterator: the most basic kind of iterator that can be used to obtain values (as opposed to updating them, as done by an output iterator). A common task is to split a string into words, lines or other delimiterseparated items. This is one of the cases where the standard library does not offer any good solutions: hence, we will roll our own. The class will be called splitter and will take 2 parameters: the string (string_view to be exact) to be split, and the delimiter (for simplicity limited to a single character). The splitter is based on string_view to make the whole affair ‘zero-copy’: the string data is never copied. The downside is that the input string (the one being split) must ‘outlive’ the splitter instance. We will re-use the split function from previous demo and use it as the ‘workhorse’ of the splitter. using split_view = std::pair< std::string_view, std::string_view >; split_view split( std::string_view s, char delim ) { size_t idx = s.find( delim ); if ( idx == s.npos ) return { s, {} }; else return { s.substr( 0, idx ), s.substr( idx + 1, s.npos ) }; } The splitter class itself doesn’t do much: its main role is to create iterators, via begin and end. To this end, it must of course remember the input string and the delimiter. struct splitter { using value_type = std::string_view; std::string_view _str; char _delim; Real iterators must provide operator-> – the one that is invoked when we say iter->foo(). For this particular use-case, this is a little vexing: operator-> must return either a raw pointer, or an instance of a class with overloaded operator->. Clearly, that chain must end somewhere – sooner or later, we must have an address of the item to which the iterator points. This is inconvenient, because we want to construct that item ‘on the fly’ – whenever it is needed – and return it from the dereference operator (unary *) by value, not by reference. The above two requirements are clearly contradictory: as you surely know, returning the address of a local variable won’t do. This is where proxy comes into play: its role is to hold a copy of the item that has sufficiently long lifetime. Later, we will return proxy instances by value, hence proxy itself will be a temporary object. Since temporary objects live until the ‘end of statement’ (i.e. until the nearest semicolon, give or take), we can return the address of its own attribute. That address will be good until the entire statement finishes executing, which is good enough: that means when we write iter>foo(), the proxy constructed by operator->, and hence the string_view stored in its attribute, will still exist when foo gets executed. struct proxy { value_type v; value_type *operator->() { return &v; } }; struct iterator { There are 5 ‘nested types’ that iterators must provide. The probably most important is value_type, which is the type of the element that we get when we dereference the iterator. 62/90 PB161 Programování v jazyce C++, 16. února 2024 using value_type = splitter::value_type; The iterator_category type describes what kind of iterator this is, so that generic algorithms can take advantage of the extra guarantees that some iterators provide. This is a humble input iterator, and hence gets std::input_iterator_tag as its category. using iterator_category = std::input_iterator_tag; The remaining 3 types exist to make writing generic algorithms somewhat easier. The difference_type is what you would get by subtracting two iterators – the ‘default’ is ptrdiff_t, so we use that, even though our iterators cannot be subtracted. using difference_type = std::ptrdiff_t; The last two are ‘decorated’ versions of value_type: a pointer, which is straightforward (but do remember to take const-ness into account)… using pointer = value_type *; … and a reference (same const caveat applies). However, you might find it surprising that the latter is not actually a reference type in this case. Why? Because reference is defined as ‘what the dereference operator returns’, and our operator* returns a value, not a reference. Input iterators have an exception here: all higher iterator types (forward, bidirectional and random) must make reference an actual reference type. using reference = value_type; Now, finally, for the implementation. The data members (and the constructor, and the assignment operator) are all straightforward. The _str attribute represents the reminder of the string that still needs to be split, and will be an empty string for the end iterator. Remember that string_view does not hold any data, so we are not making copies of the input string. std::string_view _str; char _delim; iterator( std::string_view s, char d ) : _str( s ), _delim( d ) {} iterator( const iterator & ) = default; iterator &operator=( const iterator & ) = default; The pre-increment and post-increment operators are reasonably simple. As is usual, we implement the latter in terms of the former. iterator &operator++() { _str = split( _str, _delim ).second; return *this; } iterator operator++( int ) { auto orig = *this; ++*this; return orig; } Dereference would be unremarkable, except for the part where we return a value instead of a reference (we could use the reference nested type here to make it clear we are adhering to iterator requirements, but that would be likely more confusing, considering how reference is not a reference). Do note the const here. value_type operator*() const { return split( _str, _delim ).first; } This is what gets called when we write iter->foo. See proxy above for a detailed explanation of how and why this works. Also, const again. proxy operator->() const { return { **this }; } Finally, equality. There is a trap or two that we need to avoid: first and foremost, string_view comparison operators compare content (i.e. the actual strings) – this is not what we want, since it could get really slow, even though it would ‘work’. The other possible trap is that on many implementations, string literals with equal content get equal addresses, i.e. the begin of two different std::string_view( "" ) instances would compare equal, but this is not guaranteed by the standard. It just happens to work by accident on many systems. bool operator==( const iterator &o ) const { return ( _str.empty() && o._str.empty() ) || ( _str.begin() == o._str.begin() ); } }; auto begin() const { return iterator( _str, _delim ); } auto end() const { return iterator( {}, _delim ); } splitter( std::string_view str, char delim ) : _str( str ), _delim( delim ) {} }; int main() /* demo */ { auto s = splitter( "quick brown fox", ' ' ); auto e = std::vector{ "quick", "brown", "fox" }; auto iseq = [&]{ return std::equal( s.begin(), s.end(), e.begin(), e.end() ); }; assert( iseq() ); s = splitter( "", ' ' ); assert( !iseq() ); e.clear(); assert( iseq() ); s = splitter( "hello", ' ' ); e = std::vector{ "hello" }; assert( iseq() ); s = splitter( "hello", 'l' ); e = std::vector{ "he", "", "o" }; assert( iseq() ); } 10.e: Elementární příklady 10.e.1 [digraph] We will write a simple function, digraph_freq, which accepts a string and computes the frequency of all (alphabetic) digraphs. The exact signature is up to you, in particular the return type. The only requirement is that the returned value can be indexed using strings and this returns the count (or 0 if the input string is not a correct digraph). This must also work on const instances of the return value. For examples see main. Define digraph_freq here, along with any helper functions or classes. 10.p: Přípravy 10.p.1 [rewrap] V tomto příkladu budeme pracovat s textem. Procedura rewrap dostane odkaz (referenci) na řetězec, který obsahuje text složený ze slov oddělených mezerami (U+0020) nebo znaky nového řádku (U+000A). Dva nebo více znaků konce řádku těsně za sebou chápeme jako oddělovač odstavců. Dále dostane celočíselný počet sloupců (znaků) cols, který určují, jak dlouhý může být jeden řádek. Procedura pak přeformátuje vstupní řetězec 63/90 tak, aby: 1. bylo zachováno rozdělení na odstavce, 2. jednotlivý řádek textu nepřesáhl cols, 3. zachová celistvost slov (tzn. smí měnit pouze mezery a znaky nového řádku), 4. každý řádek byl nejdelší možný. Můžete předpokládat, že žádné slovo není delší než cols a že každá mezera sousedí po obou stranách se slovem. void rewrap( std::u32string &str, int cols ); 10.p.2 [grammar] Regulární gramatika má pravidla tvaru A → xB nebo A → x kde A, B jsou neterminály a x je terminál. Navrhněte typ grammar, kterého hodnoty lze implicitně sestrojit a který má tyto 2 metody: • add_rule, které lze předat 2 nebo 3 parametry: a. jeden znak, který reprezentuje levou stranu (velké písmeno anglické abecedy) a b. jeden terminál (malé písmeno), nebo jeden terminál a jeden netermi- nál, • generate, která přijme 2 parametry: startovní neterminál a kladné celé číslo, které reprezentuje maximální délku slova; výsledkem bude seznam (std::vector) všech řetězců (slov), které lze takto vygenerovat, a to v lexikografickém pořadí. class grammar; 10.p.3 [linear] Ve cvičení 07/p6_linear jsme napsali jednoduchý program, který řeší systémy lineárních rovnic o dvou neznámých. Tento program nyní rozšíříme o načítání vstupu z řetězce. Naprogramujte čistou funkci solve, která má jediný parametr (řetězec předaný jako hodnota typu std::string_view). Vstup obsahuje právě dvě rovnice, na každém řádku jedna, se dvěma jednopísmennými proměnnými a celočíselnými koeficienty. Každý člen je oddělen od operátorů (+ a -) a znaku rovnosti mezerami, jednotlivý člen (koeficient včetně znaménka a případná proměnná) naopak mezery neobsahuje. Není-li vstup v očekávaném formátu, situaci řešte jak uznáte za vhodné (můžete např. ukončit funkci výjimkou no_parse). Výsledkem bude dvojice čísel typu double. Pořadí výsledku nechť je abecední (např. dvojice x, y). Jinak se funkce solve chová stejně, jak je popsáno v zmiňovaném příkladu 07/p6_linear. struct no_solution : std::exception {}; struct no_parse : std::exception {}; std::pair< double, double > solve( std::string_view eq ); 10.p.4 [words] Napište čistou funkci, která spočítá naivní rozdělení textu na jednotlivá slova.27 Budeme uvažovat pouze bílé vs nebílé znaky (kódové body), a za slova budeme považovat libovolnou neprázdnou sekvenci nebílých znaků. Unicode obsahuje tyto bílé znaky (označené vlastností White_Space): U+0009 – U+000D, U+0020, U+0085, U+00A0, U+1680, U+2000 – U+200A, U+2028, U+2029, U+202F, U+205F, U+3000. Dále budeme považovat za bílé i znaky U+200B, U+2060, které logicky (ale ne vizuálně) slova oddělují (tyto znaky vlastností White_Space označeny nejsou). Vstupem funkce je pohled na text, výstupem funkce je seznam (std::vector) pohledů, které vymezují jednotlivá identifikovaná slova. std::vector< std::u32string_view > words( std::u32string_view ); 10.p.5 [chords] V tomto cvičení se budeme zabývat hudebními akordy. Nezapomeňte kód dekomponovat do smysluplných celků. Tzv. „západní“ hudební tradice používá 12 tónů. Sousední tóny jsou vzdálené jeden půltón (100 centů). Základní akordy jsou vystavěny z malých (300 centů) a velkých (400 centů) tercií. Budeme se zabývat pouze akordy v základním tvaru, tzn. základní tón je zároveň basovým. K zápisu budeme používat německé názvosloví: • c, d, e, f, g, a, h jsou „základní“ tóny bez posuvek (♮), • cis, dis, eis = f, fis, gis, ais, his = c → s křížkem (♯), • ces = h, des, es, fes = e, ges, as, b → tóny s béčkem (♭). Základní noty (♮) jsou vzdálené 200 centů, s výjimkou dvojic e/f a h/c, které jsou vzdálené pouze 100 centů. Béčko odečítá a křížek přičítá k hodnotě noty 100 centů. Zjednodušená pravidla pro používání názvů tónů při stavbě akordů: • v tóninách C, G, D, A, E, H, Fis, Cis → používáme křížky, • tóniny F, B, Es, As, Des, Ges, Ces → používáme béčka, • béčka a křížky (v základních akordech) nemícháme, • dvojitá béčka a křížky neuvažujeme, • místo eis, his, ces, fes, použijeme f, c, h, e. Čistá kvinta je 700 centů, zatímco malá septima je 1000 centů. Intervaly (vyjádřené v centech) skládáme sčítáním mod 1200, přitom konvenčně chápeme tón c jako nulu. Je-li například základní tón g, tzn. 700 centů, přičtením čisté kvinty dostaneme 1400 mod 1200 = 200 centů, neboli tón d. Mezi tóny g a d je tedy 27 Skutečná segmentace textu je velmi složitá a prakticky jediná možnost je použít stávající knihovny, pro C++ např. ICU4C (balík knihoven, který má dohromady cca 100MiB a jen hlavičkové soubory mají cca 120 tisíc řádků). interval čisté kvinty. Durový kvintakord stavíme od základního tónu tóniny přidáním velké tercie a další malé tercie, například c → c e g nebo e → e gis h. std::string major_5( std::string key ); Mollový kvintakord stavíme od sexty (900 centů) paralelní durové tóniny přidáním malé tercie a další velké tercie, např. c → a c e, nebo e → cis e gis. std::string minor_5( std::string key ); Dominantní septakord stavíme od kvinty durové tóniny, např. v tónině C bude postaven na tónu g, a vznikne přidáním jedné velké a dvou malých tercií (celkem 4 tóny), například tedy f → c e g b. std::string dominant_7( std::string key ); 10.p.6 [fixnum] V tomto cvičení implementujeme čísla s pevnou desetinnou čárkou, konkrétně se 6 desítkovými číslicemi před a dvěma za desetinnou čárkou. Čísla budou tedy tvaru 123456.78. Typ bad_format budeme používat jako výjimku, která indikuje, že pokus o načtení čísla z řetězce selhalo. struct bad_format; Typ fixnum nechť poskytuje tyto operace: • sčítání, odečítání a násobení (operátory +, - a *), • sestrojení z celého čísla, • sestrojení z řetězce zadaného v desítkové soustavě (desetinná část je nepovinná) – je-li řetězec neplatný, konstruktor nechť skončí výjimkou bad_format, • srovnání dvou hodnot operátory == a !=. Všechny aritmetické operace nechť zaokrouhlují směrem k nule na nejbližší reprezentovatelné číslo. struct fixnum; 10.r: Řešené úlohy 10.r.1 [expr] Napište čistou funkci expr_valid, která rozhodne, je-li vstupní řetězec výrazem, který vyhovuje následující gramatice: expr = ws, term, { ws, '+', term } ; term = ws, factor, { ws, '*', factor } ; factor = ws, ( '(', expr, ws, ')' | ident | num ) ; ws = { ? std::isspace ? } ; 64/90 PB161 Programování v jazyce C++, 16. února 2024 ident = letter, { letter } ; num = digit, { digit } ; letter = 'a' | 'b' | … | 'z' ; digit = '0' | '1' | … | '9' ; bool expr_valid( std::string_view ); 10.r.2 [stmt] Napište čistou funkci stmt_valid, která rozhodne, je-li vstupní řetězec příkazem jazyka zadaného následující gramatikou: stmt = 'if', expr, 'then', stmt | 'while', expr, 'do', stmt | 'begin', stmt, { stmt }, 'end' | 'set', ident, ':=', expr | 'skip' ; expr = atom | atom '+' atom | atom '-' atom ; atom = '0' | '1' | ident ; Terminály (a neterminál ident) jsou od sebe na vstupu odděleny právě jednou mezerou. Pro ident použijte tuto definici: ident = letter, { letter } ; letter = 'a' | 'b' | … | 'z' ; bool stmt_valid( std::string_view ); 10.r.4 [pretty] In this exercise, we will write a pretty-printer for simple arithmetic expressions, with 3 operation types: addition, multiplication and equality, written as +, * and = respectively. The goal is to print the expression with as few parentheses as possible. Assume full associativity for all operations. The precedence order is the usual one: multiplication binds most tightly, while equality most loosely. The formatting is done by calling a print method on the root of the expression to be printed. class node; class addition; class multiplication; class equality; class constant; using node_ptr = std::unique_ptr< node >; node_ptr read( std::string_view expr ); 10.r.5 [json] The goal of this exercise is to implement a printer for JSON, invoked as a print method available on each node. It should take no arguments and return an instance of std::string. For simplicity, the only scalar values you need to implement are integers. Then there are 2 composite data types: • arrays, which represent a sequence of arbitrary values, • objects, which map strings to arbitrary values. Both composite types are heterogeneous (the items can be of different types). They are formatted as follows: • array: [ 1, [ 2, 3 ], 4 ], • object: { "key1": 7, "key2": [ 1, 2 ] }. To further simplify matters, we will not deal with line breaks or indentation – format everything as a single line. class node; using node_ptr = std::unique_ptr< node >; using node_ref = const node &; The number class is to be constructed from an int, has no children, and needs no methods besides print. class number; The object and array classes represent composite JSON data. They should be both default-constructible, resulting in an empty collection. Both should have an append method: for object, it takes an std::string (the key) and a node_ptr, while for array, only the latter. In both cases, print items in the order in which they were appended. Duplicated keys are ignored (i.e. first occurrence wins). class object; class array; 65/90 Část 11: Vstup a výstup Demonstrations: 1. files – opening files, reading and writing strings 2. streams – from values to strings and back 3. format – overloading formatting operators Elementary exercises: 1. force – input and output operators Preparatory exercises: 1. set – read and write sets of numbers, 2. fixnum – fixed point numbers with formatted IO, 3. tmpfile – an auto-erasing temporary file 4. parse – parser for a simple low-level language, 5. grep – print matching lines, 6. csv – parse comma-separated numeric data. Regular exercises: 1. xxx 2. xxx 3. xxx 4. xxx 5. json – format a string → string map as JSON 6. cpp † – a very simple C preprocessor 11.d: Demonstrace (ukázky) 11.d.1 [files] This example will be brief: we will show how to open a file for reading and fetch a line of text. We will then write that line of text into a new file and read it back again to check that things worked. We will split up the example into functions for 2 reasons: first, to make it easier to follow, and second, to take advantage of RAII: the file streams will close the underlying resource when they are destroyed. In this case, that will be at the end of each function. std::string read( const char *file ) { The default method of doing IO in C++ is through streams. Reading files is done through a stream of type std::ifstream, which is short for input file stream. The constructor of ifstream takes the name of the file to open. We will use a file given to us by the caller. std::ifstream f( file ); The simplest method to read text from a file is using std::getline, which will fetch a single line at a time, into an std::string. We need to prepare the string in advance, since it is passed into std::getline as an output argument. std::string line; The std::getline function returns a reference to the stream that was passed to it. Additionally, the stream can be converted to bool to find out whether everything is okay with it. If the reading fails for any reason, it will evaluate to false. The newline character is discarded. if ( !std::getline( f, line ) ) In real code, we would of course want to handle errors, because opening files is something that can fail for a number of reasons. Here, we simply assume that everything worked. assert( false ); return line; } Next comes a function which demonstrates writing into files. void write( const char *file, std::string line ) { To write data into a file, we can use std::ofstream, which is short for output file stream. The output file is created if it does not exist. std::ofstream f( file ); Writing into a file is typically done using operators for formatted output. We will look at those in more detail in the next section. For now, all we need to know that writing an object into a stream is done like this: f << line; We will also want to add the newline character that getline above chomped. We have two options: either use the "\n" string literal, or std::endl – a so-called stream manipulator which sends a newline character and asks the stream to send the bytes to the operating system. Let’s try the more idiomatic approach, with the manipulator: f << std::endl; At this point, the file is automatically closed and any outstanding data is sent to the operating system. } int main() /* demo */ { We first use read to get the first line of a random file. std::string line = read( "zz.include.txt" ); And we check that the line we got is what we expect. Remember the stripped newline. assert( line == "#ifdef foo" ); Now we write the line into another file. After you run this example, you can inspect files.out with an editor. It should contain a copy of the first line of this file. write( "d5_files.out", line ); Finally, we use read again to read "file.out" back, and check that the same thing came back. std::string check = read( "d5_files.out" ); assert( check == line ); } 11.d.2 [streams] File streams are not the only kind of IO streams that are available in the standard library. There are 3 ‘special’ streams, called std::cout, std::cerr and std::cin. Those are not types, but rather global variables, and represent the standard output, the standard error output and the standard input of the program. However, the first two are instances of std::ostream and the third is an instance of std::istream. We don’t know about class inheritance yet, but it is probably not a huge stretch to understand that instances of std::ofstream (output file stream) are also at the same time instances of std::ostream (general output stream). The same story holds for std::ifstream (input file stream) and std::istream (general input stream). There is another pair of classes: std::ostringstream and std::istringstream. Those streams are not attached to OS resources, but to instances of std::string: in other words, when you write to an ostringstream, the resulting bytes are not sent to the operating system, but are instead appended to the given string. Likewise, when you read from an istringstream, the data is not pulled from the operating system, but instead come from an std::string. Hopefully, you can see the correspondence between files (the content of which are byte sequences stored on disk) and strings (the content of which are byte sequences stored in RAM). 66/90 PB161 Programování v jazyce C++, 16. února 2024 In any case, string streams are ideal for playing around, because we can use the same tools as we always do: create some simple instances, apply operations and use assert to check that the results are what we expect. String-based streams are defined in the header sstream. Everything that we will do with string streams applies to other types of streams too (i.e. the 3 special streams mentioned earlier, and all file streams). Like in the previous example, we will split up the demonstration into a few sections, mainly to avoid confusion over variable names. We will first demonstrate reading from streams. We have already seen std::getline, so let’s start with that. It is probably noteworthy that it works on any input stream, not just std::ifstream. void getline_1() { std::istringstream istr( "a string\nwith 2 lines\n" ); std::string s; assert( std::getline( istr, s ) ); assert( s == "a string" ); assert( std::getline( istr, s ) ); assert( s == "with 2 lines" ); assert( !std::getline( istr, s ) ); assert( s.empty() ); } We can also override the delimiter character for std::getline, to extract delimited fields from input streams. void getline_2() { std::istringstream istr( "colon:separated fields" ); std::string s; assert( std::getline( istr, s, ':' ) ); assert( s == "colon" ); assert( std::getline( istr, s, ':' ) ); assert( s == "separated fields" ); assert( !std::getline( istr, s, ':' ) ); } So far so good. Our other option is so-called formatted input. The standard library doesn’t offer much in terms of ready-made overloads for such inputs: there is one for strings, which extracts individual words (like the scanf specifier %s, if you remember that from C, but the C++ version is actually safe and it is okay to use it). Then there is an instance for char, which extracts a single character (regardless of whether it is a whitespace character or not) and a bunch of overloads for various numeric types. void formatted_input() { std::istringstream istr( "integer 123 float 3.1415 s t" ); std::string s, t; int i; float f; istr >> s; assert( s == "integer" ); istr >> i; assert( i == 123 ); istr >> s; assert( s == "float" ); Notice that float numbers are not very exact. They are usually just 32 bits, which means 24 bits of precision, which is a bit less than 8 decimal digits. istr >> f; assert( std::fabs( f - 3.1415 ) < 1e-7 ); The last thing we want to demonstrate with regards to the formatted input operators is that we can chain them. The values are taken from left to right (behind the scenes, this is achieved by the formatted input operator returning a reference to its left operand. istr >> s >> t; assert( s == "s" && t == "t" ); When we reach the end of the stream (i.e. the end of the buffer, or of the file), the stream will indicate an error. A stream in error condition converts to false in a bool context. assert( !( istr >> s ) ); } Output is actually quite a bit simpler than input. It is almost always reasonable to use formatted output, since strings are simply copied to the output without alterations. void formatted_output() { std::ostringstream a, b, c; a << "hello world"; To read the buffer associated with an output string stream, we use its method str. Of course, this method is not available on other stream types: in those cases, the characters are written to files or to the terminal and we cannot access them through the stream anymore. assert( a.str() == "hello world" ); Like with formatted input, output can be chained. b << 123 << " " << 3.1415; assert( b.str() == "123 3.1415" ); When writing delimited values to an output stream, it is often desirable to only put the delimiter between items and not after each item: this is an endless source of headaches. Here is a trick to do it without too much typing: int i = 0; for ( int v : { 1, 2, 3 } ) c << ( i++ ? ", " : "" ) << v; assert( c.str() == "1, 2, 3" ); } 11.d.3 [format] We have seen the basics of input and output, and that formatted input and output is realized using operators. Like many other operators in C++, those operators can be overloaded. We will show how that works in this example. We will revisit the cartesian class from last week, to represent complex numbers in algebraic form, i.e. as a sum of a real and an imaginary number. We do not care about arithmetic this time: we will only implement a constructor and the formatted input and output operators. We will, however, need equality so that we can write test cases. class cartesian { double real, imag; public: We have seen default arguments before: those are used when no value is supplied by the caller. This also allows instances to be default-constructed. cartesian( double r = 0, double i = 0 ) : real( r ), imag( i ) {} The comparison is fuzzy, due to the limited precision available in double. friend bool operator==( cartesian a, cartesian b ) { return std::fabs( a.real - b.real ) < 1e-10 && std::fabs( a.imag - b.imag ) < 1e-10; } Now the formatted output, which is a little easier than the input. Since the first operand of this operator is not an instance of cartesian, the operator cannot be implemented as a method. It must either be a function outside the class, or use the ‘friend trick’. Since we will need to access private attributes in the operator, we will use the friend syntax here. The return type and the type of the first argument are pretty much given and are always the same. You could consider them part of the syntax. The second argument is an instance of our class (this would often be passed as a const reference). 67/90 friend std::ostream &operator<<( std::ostream &o, cartesian c ) { We will use 27.3±7.1*i as the output format. We can use ‘simpler’ overloads of the << operator to build up ours: this is a fairly common practice. We write to the ostream instance given to us in the argument. We must not forget to return that instance to our caller. o << c.real; if ( c.imag >= 0 ) o << "+"; return o << c.imag << "*i"; } The input operator is similar. It gets a reference to an std::istream as an argument (and has to pass it along in the return value). The main difference is that the object into which we read the data must be passed as a non-constant (i.e. mutable) reference, since we need to change it. friend std::istream &operator>>( std::istream &i, cartesian &c ) { Like above, we will build up our implementation from simpler overloads of the same operator (which all come from the standard library). The formatted input operators for numbers do not require that the number is followed by whitespace, but will stop at a character which can no longer be part of the number. A + or - character in the middle of the number qualifies. i >> c.real; We will slightly abuse the flexibility of the formatted input operator for double values: it accepts numbers starting with an explicit + sign, hence we do not need to check the sign ourselves. Just read the imaginary part. i >> c.imag; We do need to deal with the trailing *i though. char ch; When formatted input fails, it should set a failbit in the input stream. This is how the if ( stream >> value ) construct works. if ( !( i >> ch ) || ch != '*' || !( i >> ch ) || ch != 'i' ) i.setstate( i.failbit ); And as mentioned above, we need to return a reference to the input stream. return i; } }; int main() /* demo */ { std::ostringstream ostr; ostr << cartesian( 1, 1 ); We first check that the output behaves as we expected. assert( ostr.str() == "1+1*i" ); We write a few more complex numbers into the stream, using operator chaining. ostr << " " << cartesian( 3, 0 ) << " " << cartesian( 1, -1 ) << " " << cartesian( 0, 0 ); assert( ostr.str() == "1+1*i 3+0*i 1-1*i 0+0*i" ); We now construct an input stream from the string which we created above, and check that the values can be read back. std::istringstream istr( ostr.str() ); cartesian a, b, c; Let’s read back the first number and check that the result makes sense. assert( istr >> a ); assert( a == cartesian( 1, 1 ) ); We can also check that chaining works as expected, using the remaining numbers in the string. assert( istr >> a >> b >> c ); assert( a == cartesian( 3, 0 ) ); assert( b == cartesian( 1, -1 ) ); assert( c == cartesian( 0, 0 ) ); We can reset an istringstream by calling its str method with a new buffer. We want to demonstrate that trying to read an ill-formatted complex number will fail. std::istringstream bad1( "7+3*j" ); assert( !( bad1 >> a ) ); std::istringstream bad2( "7" ); assert( !( bad2 >> a ) ); } 11.e: Elementární příklady 11.e.1 [force] This week in the physics department, we will deal with formatting and parsing vectors (forces, just to avoid confusion with std::vector... for now). The class will be called force, and it should have a constructor which takes 3 values of type double and a default constructor which constructs a 0 vector. In addition to that, it should have a (fuzzy) comparison operator and formatting operators, both for input and for output. Use the following format: [F_x F_y F_z], that is, a left square bracket, then the three components of the force separated by spaces, and a closing square bracket. Do not forget to set failbit in the input stream if the format does not match expectations. class force; 11.p: Přípravy 11.p.1 [set] Implementujte typ set, který reprezentuje množinu libovolných celých čísel, s operacemi: • add – přidá prvek, • has – ověří přítomnost prvku, • size – vrátí počet prvků. Dále nechť je hodnoty typu set možné číst z a zapisovat do vstupněvýstupních proudů. Na výstupu budou mít množiny tuto formu: {} { 1 } { 1, 2 } Na vstupu akceptujte i varianty s jiným počtem bílých znaků (včetně žád- ných). struct set; 11.p.2 [fixnum] V tomto příkladu se vrátíme k typu fixnum z předchozí kapitoly. Jedná se o typ, který reprezentuje čísla s pevnou desetinnou čárkou, konkrétně tvaru 123456.78, se 6 desítkovými číslicemi před a dvěma za desetinnou čárkou, a s těmito operacemi: • sčítání, odečítání a násobení (operátory +, - a *), • sestrojení z celého čísla (implicitně nula), • přiřazení kopií, • srovnání dvou hodnot operátory == a !=, • čtení a zápis čísel z vstupně-výstupních proudů. Všechny aritmetické operace nechť zaokrouhlují směrem k nule na nejbližší reprezentovatelné číslo. struct fixnum; 11.p.3 [tmpfile] We will implement a simple wrapper around std::fstream 68/90 PB161 Programování v jazyce C++, 16. února 2024 that will act as a temporary file. When the object is destroyed, use std::remove to unlink the file. Make sure the stream is closed before you unlink the file. The tmp_file class should have the following interface: • a constructor which takes the name of the file • method write which takes a string and replaces the content of the file with that string; this method should flush the data to the operating system (e.g. by closing the stream) • method read which returns the current content of the file • method stream which returns a reference to an instance of std::fstream (i.e. suitable for both reading and writing) Calling both stream and write on the same object is undefined behaviour. The read method should return all data sent to the file, including data written to stream() that was not yet flushed by the user. class tmp_file; 11.p.4 [parse] Write a simple parser for an assembly-like language with one instruction per line (each taking 2 operands, separated by spaces, where the first is always a register and the second is either a register or an ‘immediate’ number). The opcodes (instructions) are: add, mul, jnz, the registers are rax, rbx and rcx. The result is a vector of instruction instances (see below). Set r_2 to reg::immediate if the second operand is a number. If the input does not conform to the expected format, throw no_parse, which includes a line number with the first erroneous instruction and the kind of error (see enum error), as public attributes line and type, respectively. If multiple errors appear on the same line, give the one that comes first in the definition of error. You can add attributes or methods to the structures below, but do not change the enumerations. enum class opcode { add, mul, jnz }; enum class reg { rax, rbx, rcx, immediate }; enum class error { bad_opcode, bad_register, bad_immediate, bad_structure }; struct instruction { opcode op; reg r_1, r_2; int32_t immediate; }; struct no_parse { int line; error type; }; std::vector< instruction > parse( const std::string & ); #include 11.p.5 [grep] To practice working with IO streams a little, we will write a two simple functions which reads lines from an input stream, process them a little and possibly print them out or their part into an output stream. The grep function checks, for every line on the input, whether it matches a given pattern (i.e. the pattern is a substring of the line) and if it does (and only if it does) copies the line to the output stream. void grep( std::string pattern, std::istream &, std::ostream & ); The other function to add is called cut and it will process the lines differently: it splits each line into fields separated by the character delim and only prints the column given by col. Unlike the cut program, index columns starting at 0. If there are not enough columns on a given line, print an empty line. void cut( char delim, int col, std::istream &, std::ostream & ); 11.p.6 [csv] In this exercise, we will deal with CSV files: we will implement a class called csv which will read data from an input stream and allow the user to access it using the indexing operator. The exception to throw in case of format error. class bad_format; The constructor should accept a reference to std::istream and the expected number of columns. In the input, each line contains integers separated by value. The constructor should throw an instance of bad_format if the number of columns does not match. Additionally, if x is an instance of csv, then x.at( 3, 1 ) should return the value in the third row and first column. class csv; 11.r: Řešené úlohy 11.r.5 [json] You are given a single-level string → string dictionary. Turn it into a single string, using JSON as the format. Take care to escape special characters – at least double quote and the escape character (backslash) itself. In JSON, key order is not important – emit them in iteration (alphabetic) order. Put a single space after each ‘element’: after the opening brace, after colons and after commas, except if the input is empty, in which case the output should be just {}. using str_dict = std::map< std::string, std::string >; std::string to_json( const str_dict &dict ); 11.r.6 [cpp] Implement a (very simplified) C preprocessor which supports #include "foo" (without a search path, working directory only), #define without a value, #undef, #ifdef and #endif. The input is provided in a file, but the output should be returned as a string. PS: Do not include line and filename information that cpp normally adds to files. std::string cpp( const std::string &filename ); If you run this program with a parameter, it’ll preprocess that file and print the result to stdout. Feel free to experiment. int main( int argc, const char **argv ) { if ( argc >= 2 ) std::cout << cpp( argv[ 1 ] ); else { std::string actual_1 = cpp( "zz.preproc_1.txt" ), expect_1 = "included foo\n" "included bar\n" "xoo\n" "foo\n", actual_2 = cpp( "zz.preproc_2.txt" ), expect_2 = "included bar\n" "included baz\n" "included bar\n"; assert( actual_1 == expect_1 ); assert( actual_2 == expect_2 ); } return 0; } 69/90 Část S.3: Součtové typy, řetězce 1. a_machine – jednoduchý virtuální stroj s pamětí, 2. b_chess – hrajeme šach, 3. c_real – reálná čísla (dále rozšiřuje s2/a_natural), 4. d_json – reprezentace JSON-u použitím std::variant, 5. e_robots – rozšíření s2/c_robots o programovatelné roboty, 6. f_network – načítání vstupu pro simulátor z s2/d_network. V příkladech a až c využijete zejména znalosti z prvních dvou bloků, vyžadují navíc pouze výčtové typy (enum) z 9. kapitoly. Příklad d vyžaduje znalosti 9. kapitoly a příklady e, f vyžadují znalost 11. kapitoly. S.3.a: machine V této úloze budete programovat jednoduchý registrový stroj (model počítače). Stroj bude mít libovolný počet celočíselných registrů a paměť adresovatelnou po bajtech. Registry jsou indexované od 1 po INT_MAX. Každá instrukce jmenuje dva registry a jednu přímo zakódovanou hodnotu (angl. immediate). V každém registru je uložena hodnota typu int32_t, tzn. velikost strojového slova jsou 4 slabiky (bajty). V paměti jsou slova uložena tak, že nejvýznamnější slabika má nejnižší adresu (tzv. MSB-first). Počáteční hodnoty registrů i paměti jsou nuly. Stroj má následovné instrukce (kdykoliv je reg_X použito v popisu, myslí se tím samotný registr – jeho hodnota nebo úložišě – nikoliv jeho index; sloupec reg_2 má opačný význam, vztahuje se k indexu uloženému v instrukci). opcode reg_2 description mov ≥ 1 kopíruj hodnotu z reg_2 do reg_1 = 0 nastav reg_1 na immediate add ≥ 1 ulož reg_1 + reg_2 do reg_1 = 0 přičti immediate do reg_1 mul ≥ 1 ulož reg_1 * reg_2 do reg_1 jmp = 0 skoč na adresu uloženou v reg_1 ≥ 1 skoč na reg_1 je-li reg_2 nenulové load ≥ 1 načti hodnotu z adresy reg_2 do reg_1 stor ≥ 1 zapiš hodnotu reg_1 na adresu reg_2 halt = 0 zastav stroj s výsledkem reg_1 ≥ 1 totéž, ale pouze je-li reg_2 nenulový Každá instrukce je v paměti uložena jako 4 slova (adresy slov rostou zleva doprava). Vykonání instrukce, která není skokem, zvýší programový čítač o 4 slova. reg_2reg_1immediateopcode Vykonání jednotlivé instrukce smí zabrat nejvýše konstantní čas, krom případů, kdy tato přistoupí k dosud nepoužité adrese nebo registru. Paměť potřebná pro výpočet by měla být v nejhorším případě úměrná součtu nejvyšší použité adresy a nejvyššího použitého indexu registru. enum class opcode { mov, add, mul, jmp, load, stor, hlt }; struct machine { Čtení a zápis paměti po jednotlivých slovech. std::int32_t get( std::int32_t addr ) const; void set( std::int32_t addr, std::int32_t v ); Spuštění programu, počínaje adresou nula. Vrátí hodnotu uloženou v reg_1 zadaném instrukcí hlt, která výpočet ukončila. std::int32_t run(); }; S.3.b: chess Cílem tohoto úkolu je naprogramovat běžná pravidla šachu. Předepsané typy position, piece_type, player ani result není dovoleno upravovat. struct position { int file; /* sloupec („písmeno“) – a = 1, b = 2, ... */ int rank; /* řádek („číslo“) – 1, 2, …, 8 */ position( int file, int rank ) : file( file ), rank( rank ) {} bool operator== ( const position &o ) const = default; auto operator<=>( const position &o ) const = default; }; enum class piece_type { pawn, rook, knight, bishop, queen, king }; enum class player { white, black }; Metoda play může mít jeden z následujících výsledků. Možnosti jsou uvedeny v prioritním pořadí, tzn. skutečným výsledkem je vždy první aplikovatelná možnost. capture tah byl platný a sebral soupeřovu figuru ok tah byl platný no_piece na pozici from není žádná figura bad_piece figura na pozici from nepatří hráči bad_move tah není pro tuto figuru platný blocked tah je blokován jinou figurou lapsed braní mimochodem již nelze provést has_moved některá figura rošády se už hýbala in_check hráč byl v šachu a tah by jej neodstranil would_check tah by vystavil hráče šachu bad_promote pokus o proměnu na pěšce nebo krále Pokus o braní mimochodem v situaci, kdy jsou figury ve špatné pozici, je bad_move. Krom výsledku has_moved může pokus o rošádu skončit těmito chybami: • blocked – v cestě je nějaká figura, • in_check – král je v šachu, • would_check – král by prošel nebo skončil v šachu. enum class result { capture, ok, no_piece, bad_piece, bad_move, blocked, lapsed, in_check, would_check, has_moved, bad_promote }; struct piece { player owner; piece_type type; }; using occupant = std::optional< piece >; class chess { public: Sestrojí hru ve výchozí startovní pozici. První volání metody play po sestrojení hry indikuje tah bílého hráče. chess(); 70/90 PB161 Programování v jazyce C++, 16. února 2024 Metoda play přesune figuru z pole from na pole to: • umístí-li tah pěšce do jeho poslední řady (řada 8 pro bílého, resp. 1 pro černého), je proměněn na figuru zadanou parametrem promote (jinak je tento argument ignorován), • rošáda je zadána jako pohyb krále o více než jedno pole, • je-li výsledkem chyba (cokoliv krom capture nebo ok), stav hry se nezmění a další volání play provede tah stejného hráče. result play( position from, position to, piece_type promote = piece_type::pawn ); Metoda at vrátí stav zadaného pole. occupant at( position ) const; }; S.3.c: real Předmětem této úlohy je naprogramovat typ real, který reprezentuje reálné číslo s libovolnou přesností a rozsahem. Z hodnot: • a, b typu real, • k typu int nechť je lze utvořit tyto výrazy, které mají vždy přesný výsledek: • a + b, a - b, a * b, a / b, • a += b, a -= b, a *= b, a /= b, • a == b, a != b, a < b, a <= b, a > b, a >= b, • -a – opačná hodnota, • a.abs() – absolutní hodnota, • a.reciprocal() – převrácená hodnota (není definováno pro 0), • a.power( k ) – mocnina (včetně záporné). Výrazy, které nemají přesnou explicitní (číselnou) reprezentaci jsou parametrizované požadovanou přesností p typu real: • a.sqrt( p ) – druhá odmocnina, • a.exp( p ) – exponenciální funkce (se základem e), • a.log1p( p ) – přirozený logaritmus ln(1 + a), kde a ∈ (−1, 1). Přesností se myslí absolutní hodnota rozdílu skutečné (přesné) a reprezentované hodnoty. Pro aproximaci odmocnin je vhodné použít NewtonovuRaphsonovu metodu (viz ukázka z prvního týdne). Pro aproximaci transcendentálních funkcí (exponenciála a logaritmus) lze s výhodou použít příslušných mocninných řad. Nezapomeňte ověřit, že řady v potřebné oblasti konvergují. Při určování přesnosti (počtu členů, které je potřeba sečíst) si dejte pozor na situace, kdy členy posloupnosti nejprve rostou a až poté se začnou zmenšovat. Konečně je-li d hodnota typu double, nechť jsou přípustné tyto konverze: • real x( d ), static_cast< real >( d ), Poznámka: abyste se vyhnuli problémům s nejednoznačnými konverzemi, je vhodné označit konverzní konstruktory a operátory pro hodnoty typu double klíčovým slovem explicit. struct real; S.3.d: json Naprogramujte syntaktický analyzátor pro zjednodušený JSON: v naší verzi nebudeme vůbec uvažovat „uvozovkované“ řetězce – skaláry budou pouze čísla, klíče budou slova bez uvozovek (a tedy například nebudou moct obsahovat mezery). Celý dokument se tedy skládá z objektů (mapování klíč-hodnota ve složených závorkách), polí (seznamů hodnot v hranatých závorkách) a celých čísel. Gramatika ve formátu EBNF: (* toplevel elements *) value = blank, ( integer | array | object ), blank ; integer = [ '-' ], digits | '0' ; array = '[', valist, ']' | '[]' ; object = '{', kvlist, '}' | '{}' ; (* compound data *) valist = value, { ',', value } ; kvlist = kvpair, { ',', kvpair } ; kvpair = blank, key, blank, ':', value ; (* lexemes *) digits = nonzero, { digit } ; nonzero = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ; digit = '0' | nonzero ; key = keychar, { keychar } ; keychar = ? ASCII upper- or lower-case alphabetical character ? ; blank = { ? ASCII space, tab or newline character ? } ; Pro implementaci neterminálu blank můžete použít funkci std::isspace. Rozhraní nechť je nasledovné: struct json_value; using json_ptr = std::unique_ptr< json_value >; using json_ref = const json_value &; enum class json_type { integer, array, object }; struct json_error { const char *what() const; }; Typ json_value reprezentuje načtenou stromovou strukturu dokumentu. Klademe na něj tyto požadavky: • metoda item_at nechť skončí výjimkou std::out_of_range neexistuje-li požadovaný prvek, • je-li metodě item_at objektu typu json_type::object předáno číslo n, vrátí n-tou hodnotu v abecedním pořadí klíčů, přitom odpovídající klíč lze získat metodou key_at, • metoda length neselhává (pro celočíselné uzly vrátí nulu). struct json_value { virtual json_type type() const = 0; virtual int int_value() const = 0; virtual json_ref item_at( int ) const = 0; virtual json_ref item_at( std::string_view ) const = 0; virtual std::string key_at( int i ) const = 0; virtual int length() const = 0; virtual ~json_value() = default; }; Čistá funkce json_parse analyzuje dokument a vytvoří odpovídající stromovou strukturu, nebo skončí výjimkou json_error: • nevyhovuje-li řetězec zadané gramatice gramatice, • objeví-li se v kterémkoliv objektu zdvojený klíč. json_ptr json_parse( std::string_view ); Konečně čistá funkce json_validate rozhodne, je-li vstupní dokument správně utvořený (tzn. odpovídá zadané gramatice). Tato funkce nesmí skončit výjimkou (krom std::bad_alloc v situaci, kdy během analýzy dojde paměť). bool json_validate( std::string_view ); S.3.e: robots Uvažme hru s2/c_robots – Vaším úkolem bude nyní naprogramovat jednoduchý interpret, který bude hru řídit. Vstupní program sestává ze tří částí: 1. deklarace, které popisují jak roboty ve hře a jejich startovní pozice, tak případné pomocné proměnné, 2. příkazy, které se provedou jednou na začátku hry, 3. příkazy, které se provedou každý tik, dokud hra neskončí. Program by mohl vypadat například takto: 71/90 std::string_view example_1 = R"(with a = red 1 @ -5.0 0 0 b = red 1 @ 5.0 0 0 c = red 2 @ 0.0 0 0 g1 = green 2 @ -9.6 0 0 g2 = green 2 @ 9.6 0 0 init let g1 chase a let g2 chase b repeat )"; std::string_view example_2 = R"(with r = red 2 @ 0.0 0 0 g = green 2 @ 0.0 0 0 b = blue 1 @ -9.6 0 0 tick = 0 init let r chase g let g go_to @ 1.0 0 0 repeat if tick > 9 if g is_alive let b chase g set tick := tick + 1 )"; Následuje gramatika ve formátu EBNF, která popisuje syntakticky platné programy; terminály gramatiky jsou tokeny, které jsou od sebe vždy odděleny alespoň jednou mezerou, nebo předepsaným koncem řádku. prog = 'with', { decl }, 'init', { stmt }, 'repeat', { stmt } ; decl = ident, '=', init, '\n' ; init = color, num, coord | coord | num ; color = 'red' | 'green' | 'blue' ; coord = '@', num, num, num ; stmt = cmd, '\n' | 'if', cond, stmt ; cmd = 'let', ident, 'chase', ident | 'let', ident, 'go_to', expr | 'set', ident, ':=', expr | 'do', stmt, { stmt }, 'end' ; cond = atom, '=', atom | atom, '<', atom | atom, '>', atom | ident, 'is_alive' ; expr = atom | atom, '+', atom | atom, '-', atom | atom, '*', atom | '[', expr, ']' | '(', expr, ')' ; atom = ident | coord | num; Krom terminálních řetězců ('red' a pod.) považujeme za tokeny také symboly num a ident, zadané těmito pravidly: num = [ '-' ], digit, { digit }, [ '.', { digit } ] ; ident = letter, { letter | digit } digit = '0' | '1' | … | '9' ; letter = 'a' | 'b' | … | 'z' | '_' ; V programu se objevují hodnoty tří typů: 1. čísla (hodnoty typu double), 2. trojice čísel (reprezentuje pozici nebo směr), 3. odkaz na robota. S hodnotami (a proměnnými, které hodnoty daných typů aktuálně obsahují), lze provádět tyto operace: 1. čísla lze sčítat, odečítat, násobit a srovnávat (neterminály expr a cond), 2. trojice lze sčítat, odečítat a srovnat (ale pouze rovností), 3. roboty lze posílat za jiným robotem nebo na zadané souřadnice (příkaz let), 4. operace hranaté závorky hodnotu zjednodušuje: ∘ [ robot ] je aktuální pozice robota (trojice), ∘ [ point ] je Euklidovská vzdálenost bodu od počátku, resp. velikost směrového vektoru ([ p1 - p2 ] tak spočítá vzdálenost bodů p1 a p2. Operace, které nejsou výše popsané (např. pokus o sečtení robotů), nemají určené chování. Totéž platí pro pokus o použití nedeklarované proměnné (včetně přiřazení do ní). Podobně není určeno chování, nevyhovuje-li vstupní program zadané gramatice. Robot, kterému bylo uloženo pronásledovat (chase) jiného robota, bude na tohoto robota zamčen, až než mu bude uložen jiný cíl, nebo cílový robot zanikne. Nemá-li robot žádný jiný příkaz, stojí na místě (bez ohledu na barvu). Program je předán metodě run třídy game jako hodnota typu std::string_view, návratová hodnota i zde nezmíněná pravidla zůstavají v platnosti z příkladu v druhé sadě. S.3.f: network struct network; Navrhněte textový formát pro ukládání informací o sítích tak, jak byly definované v příkladu s2/e_network, který bude mít tyto vlastnosti: • do jednoho řetězce musí být možno uložit libovolný počet sítí, které mohou být vzájemně propojené směrovači, • výsledek načtení z řetězce nesmí být možné odlišit (použitím veřejného rozhraní) od hodnot, kterých uložením řetězec vznikl, • obsah řetězce musí být plně určen aktuálním stavem vstupních sítí, bez ohledu na posloupnost operací, kterými vznikly – zejména tedy nezmí záležet na pořadí přidávání a propojování (případně rozpojování) uzlů,28 • jako speciální případ předchozího, načtení a následovné uložení sítě musí být idempotentní (výsledný řetězec je identický jako ten, se kterým jsme začali). Rozhraní je dané těmito dvěma čistými funkcemi (zejména se žádná síť nesmí změnit použitím funkce serialize): std::string serialize( const std::list< network > & ); std::list< network > deserialize( std::string_view ); Aby se Vám serializace snáze implementovala, přidejte metodám add_bridge a add_router parametr typu std::string_view, který reprezentuje neprázdný identifikátor sestavený z číslic a anglických písmen. Identifikátor je unikátní pro danou síť a typ uzlu. Konečně aby bylo možné s načtenou sítí pracovat, přidejte metody endpoints, bridges a routers, které vrátí vždy std::vector ukazatelů vhodného typu. Konkrétní pořadí uzlů v seznamu není určeno. 28 Samozřejmě záleží na pořadí, ve kterém jsou sítě předány serializační funkci – serializace sítí a, b se může obecně lišit od serializace b, a. 72/90 PB161 Programování v jazyce C++, 16. února 2024 Část K: Vzorová řešení K.1: Týden 1 K.1.e.1 [factorial] int factorial( int n ) { int r = 1; for ( ; n > 0; n-- ) r *= n; return r; } K.1.e.2 [concat] std::uint64_t concat( std::uint64_t a, std::uint64_t b, int b_bits ) { return a << b_bits | b; } K.1.e.3 [zeros] int zeros( int n, int base, int &order ) { int result = 0; for ( int current_order = 0; n; current_order ++ ) { if ( n % base == 0 ) { order = current_order; ++ result; } n /= base; } return result; } K.1.e.4 [normalize] void normalize( int &p, int &q ) { int a = std::max( p, q ), b = std::min( p, q ); while ( b > 0 ) { a = a % b; std::swap( a, b ); } p /= a; q /= a; } K.1.r.1 [bitwise] bool table( std::uint8_t op, bool a, bool b, bool c ) { int shift = ( a ? 4 : 0 ) + ( b ? 2 : 0 ) + ( c ? 1 : 0 ); return op & 1 << shift; } auto bitwise( std::uint8_t op, auto a, auto b, auto c ) { decltype( a ) r = 0; for ( decltype( a ) mask = 1; mask != 0; mask <<= 1 ) if ( table( op, a & mask, b & mask, c & mask ) ) r |= mask; return r; } K.1.r.2 [euler] long phi( long n ) { long r = n; long p = 2; while ( p <= n ) { if ( n % p == 0 ) { r *= p - 1; r /= p; } while ( n % p == 0 ) n /= p; ++ p; } return r; } K.1.r.3 [hamcode] int even_parity( std::uint8_t x ) { bool r = true; for ( ; x != 0; x >>= 1 ) if ( x & 1 ) r = !r; return r; } bool h84_decode( std::uint8_t data, std::uint8_t &out ) { bool ok = even_parity( data ) && even_parity( data & 0b01010101 ) && even_parity( data & 0b00110011 ) && even_parity( data & 0b00001111 ); if ( ok ) out = ( data & 7 ) | ( data & 16 ) >> 1; return ok; } K.1.r.5 [cellular] bool table( bool a, bool b, bool c ) { return ( a && !c ) || ( !a && !b && !c ); } bool bit( auto word, int pos ) { const int bitsize = 8 * sizeof( word ); const decltype( word ) one = 1; if ( pos < 0 || pos >= bitsize ) return false; else return word & one << pos; } auto cellular_step( auto word ) { const int bitsize = 8 * sizeof( word ); 73/90 decltype( word ) result = 0, new_bit; for ( int i = 0; i < bitsize; ++i ) { new_bit = table( bit( word, i + 1 ), bit( word, i ), bit( word, i - 1 ) ); result |= new_bit << i; } return result; } K.2: Týden 2 K.2.e.1 [fibonacci] void fibonacci( std::vector< int > &v, int n ) { v.clear(); if ( n > 0 ) v.push_back( 1 ); if ( n > 1 ) v.push_back( 1 ); for ( int i = 2; i < n; ++ i ) v.push_back( v[ i - 1 ] + v[ i - 2 ] ); } K.2.e.2 [reflexive] using _pub_::relation; relation reflexive( const relation &r ) { relation out = r; for ( auto [ x, y ] : r ) { out.emplace( x, x ); out.emplace( y, y ); } return out; } K.2.e.3 [unique] std::vector< int > unique( const std::vector< int > &v ) { std::vector< int > out; std::set< int > seen; for ( int x : v ) if ( !seen.count( x ) ) { out.push_back( x ); seen.insert( x ); } return out; } K.2.r.1 [mode] int mode( const std::vector< int > &in ) { std::map< int, int > freq; int max_val = 0, max_freq = 0; for ( int x : in ) freq[ x ] ++; for ( auto [ v, f ] : freq ) if ( f > max_freq ) { max_val = v; max_freq = f; } return max_val; } K.2.r.2 [sssp] using _pub_::graph; std::map< int, int > shortest( const graph &g, int initial ) { std::map< int, int > dist; std::queue< int > queue; queue.push( initial ); dist[ initial ] = 0; while ( !queue.empty() ) { int from = queue.front(); queue.pop(); for ( auto to : g.at( from ) ) { if ( dist.count( to ) ) continue; dist[ to ] = dist[ from ] + 1; queue.push( to ); } } return dist; } K.2.r.3 [solve] bool recurse( int pos, std::vector< bool > &visited, const std::vector< int > &jumps ) { if ( pos == int( jumps.size() ) ) { int cnt = std::count( visited.begin(), visited.end(), true ); return int( jumps.size() ) == cnt; } if ( pos < 0 || pos >= int( visited.size() ) || visited[ pos ] ) return false; visited[ pos ] = true; bool won = recurse( pos - jumps[ pos ], visited, jumps ) || recurse( pos + jumps[ pos ], visited, jumps ); visited[ pos ] = false; return won; } bool solve( std::vector< int > jumps ) { std::vector< bool > visited( jumps.size(), false ); return recurse( 0, visited, jumps ); } K.2.r.4 [buckets] using _pub_::bucket; std::vector< int > sort( const std::vector< int > &stones, const std::vector< bucket > &buckets ) { std::vector< int > out( buckets.size(), 0 ); for ( int s : stones ) for ( size_t i = 0; i < buckets.size(); ++ i ) { auto [ min, max ] = buckets[ i ]; if ( s >= min && s <= max ) out[ i ] += s; } return out; } K.2.r.6 [flood] 74/90 PB161 Programování v jazyce C++, 16. února 2024 using _pub_::grid; int flood( const grid &pixels, int width, int x0, int y0, bool fill ) { grid work = pixels; int count = 0; int height = pixels.size() / width; std::queue< std::pair< int, int > > todo; if ( pixels.size() % width ) ++ height; while ( static_cast< int >( work.size() ) < width * height ) work.push_back( false ); auto flip = [&]( int x, int y ) { int idx = y * width + x; if ( x >= 0 && x < width && y >= 0 && y < height && work[ idx ] != fill ) { todo.emplace( x, y ); work[ idx ] = fill; ++ count; } }; flip( x0, y0 ); while ( !todo.empty() ) { auto [ x, y ] = todo.front(); todo.pop(); for ( int dx : { -1, 0, 1 } ) for ( int dy : { -1, 0, 1 } ) if ( dx || dy ) flip( x + dx, y + dy ); } return count; } K.3: Týden 3 K.3.e.2 [cartesian] This is a solution that uses the friend syntax. For a solution which uses the method syntax, see cartesian.alt.cpp. struct cartesian { double real, imag; friend cartesian operator+( cartesian a, cartesian b ) { You may not know this syntax yet. In a return statement, braces without a type name call the constructor of the return type. I.e. { a, b } in this context is the same as cartesian( a, b ). return { a.real + b.real, a.imag + b.imag }; } friend cartesian operator-( cartesian a, cartesian b ) { return { a.real - b.real, a.imag - b.imag }; } friend cartesian operator-( cartesian a ) { return { -a.real, -a.imag }; } friend bool operator==( cartesian a, cartesian b ) { return a.real == b.real && a.imag == b.imag; } }; cartesian make_cartesian( double r, double i ) { return { .real = r, .imag = i }; } K.3.r.1 [poly] struct poly { std::vector< int > cs; void set( int p, int c ) { cs.resize( std::max( degree(), p + 1 ), 0 ); cs[ p ] = c; } int get( int p ) const { return p < degree() ? cs[ p ] : 0; } int degree() const { return cs.size(); } static void check_bounds( int64_t v ) { brq::precondition( v >= INT_MIN && v <= INT_MAX ); } poly operator+( const poly &o ) const { poly rv; for ( int i = 0; i < std::max( degree(), o.degree() ); ++i ) { check_bounds( int64_t( get( i ) ) + o.get( i ) ); rv.set( i, get( i ) + o.get( i ) ); } return rv; } poly operator*( const poly &o ) const { poly rv; for ( int i = 0; i < degree(); ++i ) for ( int j = 0; j < o.degree(); ++j ) { check_bounds( int64_t( get( i ) ) * o.get( j ) ); check_bounds( rv.get( i + j ) + int64_t( get( i ) ) * o.get( j ) ); rv.set( i + j, rv.get( i + j ) + get( i ) * o.get( j ) ); } return rv; } bool operator==( const poly &o ) const { for ( int i = 0; i < std::max( degree(), o.degree() ); ++i ) if ( get( i ) != o.get( i ) ) return false; return true; } }; K.3.r.2 [qsort] struct array { 75/90 std::vector< int > vec; int get( int i ) const { return vec[ i ]; } void append( int x ) { vec.push_back( x ); } int partition( int pivot, int low, int high ) { while ( vec[ low ] < pivot ) /* the pivot must be in there */ ++ low; int p_index = low; shuffle anything < pivot to the front while remembering where (in the second half) we stashed the pivot itself for ( int i = low + 1; i < high; ++i ) { if ( vec[ i ] < pivot ) std::swap( vec[ low++ ], vec[ i ] ); if ( vec[ i ] == pivot ) p_index = i; } put the pivot in its place between the partitions std::swap( vec[ p_index ], vec[ low ] ); return low; } void partition( int pivot ) { partition( pivot, 0, vec.size() ); } void sort( int low, int high ) { if ( high - low <= 1 ) return; int pivot = vec[ low ]; /* whatever */ int p_index = partition( pivot, low, high ); sort( low, p_index ); sort( p_index + 1, high ); } void sort() { sort( 0, vec.size() ); } }; K.3.r.3 [ttt] struct tictactoe { int player = -1; std::array< int, 9 > board{}; int index( int x, int y ) const { return x * 3 + y; } int read( int x, int y ) const { return board[ index( x, y ) ]; } void play( int x, int y ) { board[ index( x, y ) ] = player; player *= -1; } int all_of( int x, int y, int dx, int dy ) const { int w = read( x, y ); for ( int i = 0; i < 3; ++i ) if ( w != read( x + dx * i, y + dy * i ) ) return 0; return w; } int winner() const { for ( int i = 0; i < 3; ++i ) { if ( int w = all_of( i, 0, 0, 1 ); w != 0 ) return w; if ( int w = all_of( 0, i, 1, 0 ); w != 0 ) return w; } if ( int w = all_of( 0, 0, 1, 1 ); w != 0 ) return w; if ( int w = all_of( 0, 2, 1, -1 ); w != 0 ) return w; return 0; } }; K.3.r.4 [flight] struct flight { using bound_t = std::tuple< double, double >; double climb = 0, y = 0; int x = 0; std::vector< bound_t > bounds{ { -10, 10 } }; bool flying = true; void append( double l, double h ) { brq::precondition( l < h ); bounds.emplace_back( l, h ); } int size() const { return bounds.size(); } bool finished() const { return x == size() - 1; } void set_climb( double c ) { climb = c; } bool clear() const { auto [ l, h ] = bounds[ x ]; return l < y && y < h; } bool move( int l ) { if ( !flying ) return false; while ( l-- > 0 && x < size() - 1 ) { ++ x; y += climb; if ( !clear() ) return flying = false; } return true; } }; K.3.r.5 [qfield] struct qf; qf make_qf( int a_nom, int a_den, int b_nom, int b_den ); qf make_qf( rat a, rat b ); struct qf { 76/90 PB161 Programování v jazyce C++, 16. února 2024 rat u, v; friend bool operator==( const qf &a, const qf &b ) = default; friend qf operator+( const qf &a, const qf &b ) { return make_qf( a.u + b.u, a.v + b.v ); } friend qf operator*( const qf &a, const qf &b ) { auto two = make_rat( 2, 1 ); return make_qf( a.u * b.u + two * a.v * b.v, a.u * b.v + a.v * b.u ); } }; qf make_qf( int a_nom, int a_den, int b_nom, int b_den ) { return make_qf( make_rat( a_nom, a_den ), make_rat( b_nom, b_den ) ); } qf make_qf( rat u, rat v ) { return qf{ .u = u, .v = v }; } K.3.r.6 [life] using _pub_::grid; bool updated( int x, int y, const grid &cells ) { int count = 0; bool alive = cells.count( { x, y } ); for ( int dx : { -1, 0, 1 } ) for ( int dy : { -1, 0, 1 } ) if ( dx || dy ) count += cells.count( { x + dx, y + dy } ); return alive ? count == 2 || count == 3 : count == 3; } grid life( const grid &cells, int n ) { if ( n == 0 ) return cells; grid todo, ngen; for ( auto [ x, y ] : cells ) for ( int dx : { -1, 0, 1 } ) for ( int dy : { -1, 0, 1 } ) todo.emplace( x + dx, y + dy ); for ( auto [ x, y ] : todo ) if ( updated( x, y, cells ) ) ngen.emplace( x, y ); return life( ngen , n - 1 ); } K.4: Týden 4 K.4.e.3 [force] struct force { double x, y, z; /* cartesian components of the force */ force( double x, double y, double z ) : x( x ), y( y ), z( z ) {} We only define multiplication by a scalar (double) from left, since we only need that here, but it would be equally valid to flip the operand types (and define scalar multiplication on the right). friend force operator*( double s, force f ) { return { s * f.x, s * f.y, s * f.z }; } Bog-standard vector addition. friend force operator+( force a, force b ) { return { a.x + b.x, a.y + b.y, a.z + b.z }; } Fuzzy vector equality. Two vectors are equal when all their components are equal. friend bool operator==( force a, force b ) { return std::fabs( a.x - b.x ) < 1e-10 && std::fabs( a.y - b.y ) < 1e-10 && std::fabs( a.z - b.z ) < 1e-10; } }; K.5: Týden 5 K.5.e.3 [iota] struct iota_iterator { using iterator = iota_iterator; int _val; bool operator==( iterator o ) const { return _val == o._val; }; bool operator!=( iterator o ) const { return _val != o._val; }; iota_iterator &operator++() { ++ _val; return *this; } int operator*() const { return _val; } }; struct iota { int _start, _end; iota_iterator begin() const { return { _start }; } iota_iterator end() const { return { _end }; } iota( int s, int e ) : _start( s ), _end( e ) {} }; K.5.r.2 [zipper] struct node { using ptr = std::unique_ptr< node >; int value; ptr next; node( int v, ptr n ) : value( v ), next( std::move( n ) ) {} }; class zipper { int _focus; using node_ptr = std::unique_ptr< node >; node_ptr _left, _right; public: zipper( int f ) : _focus( f ) {} bool shift( node_ptr &a, node_ptr &b ) { auto new_b = std::move( b->next ); auto new_a = std::move( b ); new_a->next = std::move( a ); 77/90 std::swap( new_a->value, _focus ); b = std::move( new_b ); a = std::move( new_a ); return true; } void push( node_ptr &p, int v ) { p = std::make_unique< node >( v, std::move( p ) ); } bool shift_left() { return _left ? shift( _right, _left ) : false; } bool shift_right() { return _right ? shift( _left, _right ) : false; } void insert_left( int v ) { push( _left, v ); } void insert_right( int v ) { push( _right, v ); } int &focus() { return _focus; } int focus() const { return _focus; } }; K.5.r.4 [diff] struct node { using ptr = std::shared_ptr< node >; enum op_t { cnst, var, add, mul, exp } op; int num = 0; ptr l, r; }; class expr { public: node::ptr ptr; expr() : ptr( std::make_shared< node >() ) {} expr( int c ) : expr() { ptr->num = c; ptr->op = node::cnst; } expr( node::op_t o, node::ptr l = nullptr, node::ptr r = nullptr ) : expr() { ptr->op = o; ptr->l = l; ptr->r = r; } expr( node::ptr p ) :ptr( p ) {} friend expr expnat( expr e ) { return { node::exp, e.ptr }; } friend expr operator+( expr a, expr b ) { return { node::add, a.ptr, b.ptr }; } friend expr operator*( expr a, expr b ) { return { node::mul, a.ptr, b.ptr }; } }; const expr x{ node::var }; double eval( expr e, double v ) { switch ( e.ptr->op ) { case node::cnst: return e.ptr->num; case node::var: return v; case node::add: return eval( e.ptr->l, v ) + eval( e.ptr->r, v ); case node::mul: return eval( e.ptr->l, v ) * eval( e.ptr->r, v ); case node::exp: return std::exp( eval( e.ptr->l, v ) ); } abort(); } expr diff( expr e ) { switch ( e.ptr->op ) { case node::cnst: return { 0 }; case node::var: return { 1 }; case node::add: return diff( e.ptr->l ) + diff( e.ptr->r ); case node::mul: return diff( e.ptr->l ) * e.ptr->r + diff( e.ptr->r ) * e.ptr->l; case node::exp: return e * diff( e.ptr->l ); } abort(); } K.6: Týden 6 K.6.r.2 [circuit] The base class. We keep track of the inputs using raw pointers, since we do not own them. We use a protected virtual method to implement the ‘business logic’ that changes from class to class, while the outside interface is defined entirely using standard (non-virtual) methods. class component { component *left = nullptr, *right = nullptr; protected: virtual bool eval( bool, bool ) = 0; public: void connect( int n, component &c ) { ( n ? right : left ) = &c; } bool read() { return eval( left ? left->read() : false, right ? right->read() : false ); } virtual ~component() = default; }; The NAND gate and the source component are trivial enough. class nand : public component { bool eval( bool x, bool y ) override { return !( x && y ); } }; class source : public component { bool eval( bool, bool ) override { return true; } }; The delay component provides one bit of memory. Reading the component will cause the value to be updated (read always calls eval internally). This class is also the reason why eval cannot be marked const. 78/90 PB161 Programování v jazyce C++, 16. února 2024 class delay : public component { bool _value = false; bool eval( bool x, bool ) override { bool rv = _value; _value = x; return rv; } }; K.6.r.6 [while] class statement; using state = std::map< char, int >; using stmt_ptr = std::unique_ptr< statement >; class statement { public: std::string print() const { brq::string_builder b; b << "\n"; print( b, 0 ); return b.buffer(); } virtual void print( brq::string_builder &, int ) const = 0; state eval( state s, int counter ) const { update( s, counter ); assert( counter >= 0 ); return s; } virtual void update( state &s, int &ctr ) const = 0; virtual ~statement() = default; }; class stmt_inc : public statement { char _var; public: stmt_inc( char v ) : _var( v ) {} void print( brq::string_builder &b, int i ) const override { b << brq::pad( i, ' ' ) << brq::mark << brq::rawchr( _var ) << "++\n"; } void update( state &s, int &counter ) const override { if ( !counter ) return; auto &v = s[ _var ]; ++ v; v %= 16; } }; class stmt_while : public statement { char _v_1, _v_2; stmt_ptr _body; public: stmt_while( char v1, char v2, stmt_ptr b ) : _v_1( v1 ), _v_2( v2 ), _body( std::move( b ) ) {} void print( brq::string_builder &b, int i ) const override { b << brq::pad( i, ' ' ) << brq::mark << "while " << brq::rawchr( _v_1 ) << " != " << brq::rawchr( _v_2 ) << "\n"; _body->print( b, i + 2 ); } void update( state &s, int &counter ) const override { while ( counter && --counter && s[ _v_1 ] != s[ _v_2 ] ) _body->update( s, counter ); } }; class stmt_block : public statement { std::vector< stmt_ptr > _body; public: void append( stmt_ptr stmt ) { _body.emplace_back( std::move( stmt ) ); } void print( brq::string_builder &b, int i ) const override { for ( const auto &s : _body ) s->print( b, i ); } void update( state &s, int &ctr ) const override { for ( const auto &stmt : _body ) stmt->update( s, ctr ); } }; K.7: Týden 7 K.7.e.2 [counter] using _pub_::counter; struct counted { counted() { ++ counter; } counted( const counted & ) { ++counter; } counted( counted && ) { ++counter; } counted &operator=( const counted & ) = default; counted &operator=( counted && ) = default; ~counted() { --counter; } }; K.7.r.1 [printing] class job { int _id; int _owner; int _pages; public: job( int id, int owner, int pages ) : _id( id ), _owner( owner ), _pages( pages ) {} job( job && ) = default; job( const job & ) = delete; job &operator=( const job & ) = delete; int id() const { return _id; } 79/90 int page_count() const { return _pages; } int owner() const { return _owner; } }; class queue { std::map< int, job > _jobs; int _count = 0; public: int dequeue() { const auto &item = *_jobs.begin(); int id = item.first; _count -= item.second.page_count(); _jobs.erase( id ); return id; } void enqueue( job &&j ) { int id = j.id(); _count += j.page_count(); _jobs.emplace( id, std::move( j ) ); } job release( int id ) { job rv( std::move( _jobs.at( id ) ) ); _jobs.erase( id ); _count -= rv.page_count(); return rv; } int page_count() const { return _count; } }; K.7.r.2 [bsearch] using _pub_::token; class flat_map { std::vector< std::pair< int, token > > _data; public: std::pair< int, bool > index( int k ) const { int low = 0, high = _data.size(); while ( low < high ) { int mid = ( low + high ) / 2; if ( k < _data[ mid ].first ) high = mid; else if ( k > _data[ mid ].first ) low = mid + 1; else return { mid, true }; } assert( low <= int( _data.size() ) ); assert( low == int( _data.size() ) || _data[ low ].first != k ); return { low, false }; } bool contains( int k ) const { return index( k ).second; } bool emplace( int k, int v ) { auto [ idx, found ] = index( k ); if ( !found ) _data.emplace( _data.begin() + idx, std::move( k ), v ); return !found; } int index_or_throw( int k ) const { auto [ idx, found ] = index( k ); if ( !found ) throw std::out_of_range( "indexing flat_map" ); return idx; } token &at( int k ) { return _data[ index_or_throw( k ) ].second; } const token &at( int k ) const { return _data[ index_or_throw( k ) ].second; } }; K.7.r.4 [tinyvec] using _pub_::token; using _pub_::insufficient_space; struct tiny_vector { std::array< uint8_t, 32 > _mem; int _count = 0; token *slot( int i ) { assert( i >= 0 ); assert( i < _count ); return reinterpret_cast< token * >( _mem.begin() ) + i; } const token *slot( int i ) const { return reinterpret_cast< const token * >( _mem.begin() ) + i; } const token &front() const { return *slot( 0 ); } const token &back() const { return *slot( _count - 1 ); } token &front() { return *slot( 0 ); } token &back() { return *slot( _count - 1 ); } ~tiny_vector() { while ( _count ) erase( _count - 1 ); } void erase( int idx ) { std::destroy_at( slot( idx ) ); for ( int i = idx; i < _count - 1; ++i ) { std::uninitialized_move_n( slot( i + 1 ), 1, slot( i ) ); std::destroy_at( slot( i + 1 ) ); } -- _count; } void insert( int idx, token &&v ) 80/90 PB161 Programování v jazyce C++, 16. února 2024 { const unsigned count = _count; if ( count == _mem.size() / sizeof( token ) ) throw insufficient_space(); ++ _count; for ( int i = _count - 1; i > idx; --i ) { std::uninitialized_move_n( slot( i - 1 ), 1, slot( i ) ); std::destroy_at( slot( i - 1 ) ); } std::uninitialized_move_n( &v, 1, slot( idx ) ); } }; K.7.r.5 [lock] using _pub_::mutex; class lock { mutex *_mutex; public: lock( mutex &m ); ~lock(); lock( const lock & ) = delete; lock &operator=( const lock & ) = delete; lock( lock && ); lock &operator=( lock && ); }; lock::lock( mutex &m ) : _mutex( &m ) { _mutex->lock(); } lock::~lock() { if ( _mutex ) _mutex->unlock(); } lock::lock( lock &&o ) : _mutex( o._mutex ) { o._mutex = nullptr; } lock &lock::operator=( lock &&o ) { if ( _mutex ) _mutex->unlock(); _mutex = o._mutex; o._mutex = nullptr; return *this; } K.8: Týden 8 K.8.e.2 [accumulate] auto accumulate( auto f, const std::vector< int > &vec ) { int sum = 0; for ( int x : vec ) sum += f( x ); return sum; }; K.9: Týden 9 K.9.r.1 [null] auto filter( const auto &seq ) { std::vector< std::decay_t< decltype( **seq.begin() ) > > out; for ( const auto &x : seq ) if ( x.has_value() ) out.push_back( *x ); return out; } auto zip( const auto &seq_a, const auto &seq_b, auto f ) { using res_t = decltype( f( **seq_a.begin(), **seq_b.begin() ) ); std::vector< std::optional< std::decay_t< res_t > > > out; auto it_a = seq_a.begin(); auto it_b = seq_b.begin(); while ( it_a != seq_a.end() && it_b != seq_b.end() ) { if ( it_a->has_value() && it_b->has_value() ) out.push_back( f( **it_a, **it_b ) ); else out.push_back( std::nullopt ); ++ it_a; ++ it_b; } return out; } auto join( const auto &seq_a, const auto &seq_b, auto f ) { using type_a = std::decay_t< decltype( **seq_a.begin() ) >; using type_b = std::decay_t< decltype( **seq_b.begin() ) >; std::vector< std::tuple< type_a, type_b > > out; for ( const auto &a : seq_a ) for ( const auto &b : seq_b ) if ( a.has_value() && b.has_value() && f( *a, *b ) ) out.emplace_back( *a, *b ); return out; } K.9.r.2 [rel] struct relation { std::map< int, std::set< int > > rel; std::function< bool( int, int ) > filter; void set_filter( auto f ) { filter = f; } void unset_filter() { filter = {}; } bool test( int a, int b ) const { return ( !filter || filter( a, b ) ) && rel.contains( a ) && rel.at( a ).contains( b ); } auto get( int a ) const { std::set< int > out; if ( rel.contains( a ) ) for ( int b : rel.at( a ) ) if ( !filter || filter( a, b ) ) out.insert( b ); return out; 81/90 } void add( int a, int b ) { rel[ a ].insert( b ); rel[ b ].insert( a ); } }; K.9.r.3 [robot] using _pub_::walk; using _pub_::turn; using _pub_::toggle; struct program { using command = std::variant< walk, turn, toggle >; std::vector< command > commands; void append( auto c ) { commands.emplace_back( c ); } }; struct grid { using position_t = std::tuple< int, int >; int x = 0, y = 0; bool horizontal = true; std::set< position_t > marked; position_t robot() const { return { x, y }; } bool on_marked() const { return marked.contains( robot() ); } void exec( toggle t ) { if ( on_marked() ) { if ( !t.sticky ) marked.erase( robot() ); } else marked.insert( robot() ); } void exec( turn ) { horizontal = !horizontal; } void exec( walk w ) { auto &coord = horizontal ? x : y; coord += on_marked() ? w.if_marked : w.if_unmarked; } }; std::tuple< int, int > run( const program &p, grid &g ) { for ( auto cmd : p.commands ) std::visit( [&]( auto c ) { g.exec( c ); }, cmd ); return { g.x, g.y }; } K.9.r.4 [sumseq] using _pub_::choice; auto select( const auto &l, const auto &r, auto choose ) { using type_l = std::decay_t< decltype( *l.begin() ) >; using type_r = std::decay_t< decltype( *r.begin() ) >; std::vector< std::variant< type_l, type_r > > out; auto it_l = l.begin(); auto it_r = r.begin(); while ( it_l != l.end() && it_r != r.end() ) { switch ( choose( *it_l, *it_r ) ) { case choice::left: out.emplace_back( *it_l ); break; case choice::right: out.emplace_back( *it_r ); break; } ++ it_l; ++ it_r; } return out; } auto project( const auto &seq, auto proj ) { using type = std::decay_t< decltype( *proj( &*seq.begin() ) ) >; std::vector< type > out; for ( const auto &x : seq ) if ( auto ptr = proj( &x ) ) out.push_back( *ptr ); return out; } auto left( const auto &seq ) { return project( seq, []( auto *x ) { return std::get_if< 0 >( x ); } ); } auto right( const auto &seq ) { return project( seq, []( auto *x ) { return std::get_if< 1 >( x ); } ); } auto map( const auto &seq, auto left, auto right ) { using common_t = decltype( left( std::get< 0 >( *seq.begin() ) ) ); std::vector< std::decay_t< common_t > > out; for ( const auto &x : seq ) if ( auto ptr = std::get_if< 0 >( &x ) ) out.push_back( left( *ptr ) ); else out.push_back( right( *std::get_if< 1 >( &x ) ) ); return out; } K.10: Týden 10 K.10.r.1 [priority] using _pub_::task; bool sched_cmp( const task &a, const task &b ) { auto key = []( const task &t ) { return std::pair( t.priority, -t.id ); }; 82/90 PB161 Programování v jazyce C++, 16. února 2024 return key( a ) < key( b ); } struct sched_queue { std::vector< task > tasks; bool complete = true; void fix() { if ( !complete ) std::push_heap( tasks.begin(), tasks.end(), sched_cmp ); complete = true; } void add( task t ) { fix(); tasks.push_back( t ); complete = false; fix(); } task &demote() { fix(); std::pop_heap( tasks.begin(), tasks.end(), sched_cmp ); complete = false; -- tasks.back().priority; return tasks.back(); } void reset() { for ( auto &t : tasks ) t.priority = t.static_priority; std::make_heap( tasks.begin(), tasks.end(), sched_cmp ); complete = true; } const task &peek() const { const auto &top = tasks.front(), &demoted = tasks.back(); if ( complete || sched_cmp( demoted, top ) ) return top; else return demoted; } }; K.10.r.2 [join] void join( auto a, auto b, unsigned i, unsigned j, auto &out ) { auto a_cmp = [&]( const auto &u, const auto &v ) { return u[ i ] < v[ i ]; }; auto b_cmp = [&]( const auto &u, const auto &v ) { return u[ j ] < v[ j ]; }; std::sort( a.begin(), a.end(), a_cmp ); std::sort( b.begin(), b.end(), b_cmp ); auto it_a = a.begin(); auto it_b = b.begin(); for ( ; it_a != a.end(); ++it_a ) { while ( it_b != b.end() && ( *it_b )[ j ] < ( *it_a )[ i ] ) ++it_b; for ( auto it = it_b; it != b.end() && ( *it )[ j ] == ( *it_a )[ i ]; ++it ) { auto &out_row = out.emplace_back(); unsigned col = 0; unsigned n = it_a->size(); unsigned m = it->size(); for ( col = 0; col < n; ++ col ) out_row[ col ] = ( *it_a )[ col ]; for ( ; col < n + m - 1; ++ col ) out_row[ col ] = ( *it )[ col - n < j ? col - n : col - n + 1 ]; } } } K.10.r.3 [sorted] auto sorted_ranges( const auto &in, int n ) { std::vector< std::tuple< int, int > > out; auto row_b = in.begin(), row_e = row_b + n; while ( row_b != in.end() ) { int len_max = 0; int idx_max; auto it = row_b; while ( it != row_e ) { auto next = std::is_sorted_until( it, row_e ); int len = std::distance( it, next ); if ( len > len_max ) { idx_max = std::distance( row_b, it ); len_max = len; } it = next; } out.emplace_back( idx_max, len_max ); std::advance( row_b, n ); std::advance( row_e, n ); } return out; } K.10.r.4 [rotsort] bool rotate_sort( auto &seq, int n ) { std::vector< int > rot; for ( auto it = seq.begin(); it != seq.end(); std::advance( it, n ) ) { auto end = std::next( it, n ); auto mid = std::is_sorted_until( it, end ); if ( mid == end ) rot.push_back( 0 ); else if ( std::is_sorted( mid, end ) && *it >= *std::next( it, n - 1 ) ) rot.push_back( std::distance( it, mid ) ); else return false; } 83/90 auto b = seq.begin(); for ( unsigned i = 0; i < rot.size(); ++i ) std::rotate( std::next( b, i * n ), std::next( b, i * n + rot[ i ] ), std::next( b, i * n + n ) ); return true; } K.10.r.5 [permute] using _pub_::to_digits; unsigned from_digits( const std::vector< unsigned > &digits, int base ) { unsigned r = 0; for ( unsigned d : digits ) { r *= base; r += d; } return r; } std::vector< unsigned > permute_digits( unsigned n, int base ) { std::set< unsigned > r; auto digits = to_digits( n, base ); std::sort( digits.begin(), digits.end() ); do r.insert( from_digits( digits, base ) ); while ( std::next_permutation( digits.begin(), digits.end() ) ); return std::vector< unsigned >( r.begin(), r.end() ); } K.11: Týden 11 K.11.e.1 [digraph] struct strmap { std::map< std::string, int > m; int operator[]( std::string s ) const { return m.contains( s ) ? m.find( s )->second : 0; } void add( std::string s ) { m[ s ] ++; } }; strmap digraph_freq( std::string_view s ) { strmap m; for ( size_t i = 0; i < s.size() - 1; ++i ) if ( std::isalpha( s[ i ] ) && std::isalpha( s[ i + 1 ] ) ) m.add( std::string( s.substr( i, 2 ) ) ); return m; } K.11.r.1 [expr] struct validate { std::string_view todo; char peek() { return todo.empty() ? 0 : todo[ 0 ]; } char fetch() { auto c = peek(); todo.remove_prefix( 1 ); return c; } void ws() { while ( std::isspace( peek() ) ) fetch(); } bool maybe_ident() { int i = 0; while ( std::isalpha( peek() ) ) ++i, fetch(); return i; } bool maybe_num() { int i = 0; while ( std::isdigit( peek() ) ) ++ i, fetch(); return i; } bool factor() { ws(); if ( peek() == '(' ) { fetch(); if ( !expr() ) return false; if ( ws(), fetch() != ')' ) return false; return true; } else return maybe_num() || maybe_ident(); } bool list( auto f, char c ) { ws(); if ( !f() ) return false; while ( ws(), peek() == c ) if ( fetch(), !f() ) return false; return true; } bool expr() { return list( [&]{ return term(); }, '+' ); } bool term() { return list( [&]{ return factor(); }, '*' ); } bool top() { return expr() && todo.empty(); } }; bool expr_valid( std::string_view s ) { return validate{ s }.top(); } K.11.r.2 [stmt] struct validate { std::string_view todo; 84/90 PB161 Programování v jazyce C++, 16. února 2024 std::string_view peek() { while ( !todo.empty() && std::isspace( todo[ 0 ] ) ) todo.remove_prefix( 1 ); unsigned i = 0; while ( i < todo.size() && !std::isspace( todo[ i ] ) ) ++ i; return todo.substr( 0, i ); } std::string_view fetch() { auto rv = peek(); todo.remove_prefix( rv.size() ); return rv; } bool is_ident( std::string_view s ) { for ( auto c : s ) if ( !std::isalpha( c ) ) return false; return true; } bool ident() { return is_ident( fetch() ); } bool atom() { auto next = fetch(); if ( next == "0" || next == "1" ) return true; else return is_ident( next ); } bool expr() { if ( !atom() ) return false; if ( peek() == "+" || peek() == "-" ) { fetch(); if ( !atom() ) return false; } return true; } bool stmt() { auto next = fetch(); if ( next == "if" ) { if ( !expr() ) return false; if ( fetch() != "then" ) return false; if ( !stmt() ) return false; } else if ( next == "while" ) { if ( !expr() ) return false; if ( fetch() != "do" ) return false; if ( !stmt() ) return false; } else if ( next == "set" ) { if ( !ident() ) return false; if ( fetch() != ":=" ) return false; if ( !expr() ) return false; } else if ( next == "begin" ) { if ( !stmt() ) return false; while ( peek() != "end" ) if ( !stmt() ) return false; if ( fetch() != "end" ) assert( false ); } else if ( next == "skip" ) return true; else return false; return true; } bool top() { return stmt() && todo.empty(); } }; bool stmt_valid( std::string_view s ) { return validate{ s }.top(); } K.12: Týden 12 K.12.e.1 [force] class force { double x = 0, y = 0, z = 0; public: force( double x, double y, double z ) : x( x ), y( y ), z( z ) {} force() = default; bool operator==( const force &f ) const { return std::fabs( f.x - x ) < 1e-10 && std::fabs( f.y - y ) < 1e-10 && std::fabs( f.z - z ) < 1e-10; } friend std::ostream &operator<<( std::ostream &o, const force &f ) { return o << "[" << f.x << " " << f.y << " " << f.z << "]"; } friend std::istream &operator>>( std::istream &i, force &f ) { char ch; if ( !( i >> ch ) || ch != '[' ) i.setstate( i.failbit ); i >> f.x >> f.y >> f.z; if ( !( i >> ch ) || ch != ']' ) i.setstate( i.failbit ); return i; } }; K.12.r.5 [json] using _pub_::str_dict; 85/90 std::string escape( const std::string &in ) { std::ostringstream ostr; for ( char c : in ) { if ( c == '\\' || c == '"' ) ostr << "\\"; ostr << c; } return ostr.str(); } std::string to_json( const str_dict &dict ) { std::ostringstream ostr; ostr << "{"; bool comma = false; for ( auto [ k, v ] : dict ) { ostr << ( comma ? ", " : " " ); comma = true; ostr << "\"" << escape( k ) << "\": \"" << escape( v ) << "\""; } ostr << ( comma ? " }" : "}" ); return ostr.str(); } K.12.r.6 [cpp] inline auto split( const std::string &sv, char delim ) { if ( auto offset = sv.find( delim ); offset != sv.npos ) return std::pair( sv.substr( 0, offset ), sv.substr( offset + 1, sv.npos ) ); else return std::pair( sv, std::string() ); } class preprocessor { std::set< std::string > defs; std::stack< bool > _emit; public: std::string out; bool emit() const { return _emit.empty() || _emit.top(); } void process( const std::string &line ) { auto [ dir, args ] = split( line, ' ' ); if ( dir == "#ifdef" ) _emit.push( defs.count( args ) ); if ( dir == "#endif" ) _emit.pop(); if ( emit() ) { if ( dir == "#define" ) defs.insert( args ); if ( dir == "#undef" ) defs.erase( args ); if ( dir == "#include" ) read( args.substr( 1, args.size() - 2 ) ); } } void read( const std::string &filename ) { std::ifstream in( filename ); /* NB. Fails quietly. */ std::string line; while ( std::getline( in, line ) ) if ( !line.empty() && line[ 0 ] == '#' ) process( line ); else if ( emit() ) out += line + "\n"; } }; std::string cpp( const std::string &filename ) { preprocessor p; p.read( filename ); return p.out; } 86/90 PB161 Programování v jazyce C++, 16. února 2024 Část T: Technické informace Tato kapitola obsahuje informace o technické realizaci předmětu, a to zejména: • jak se pracuje s kostrami úloh, • jak sdílet obrazovku (terminál) ve cvičení, • jak se odevzdávají úkoly, • kde najdete výsledky testů a jak je přečtete, • kde najdete hodnocení kvality kódu (učitelské recenze), • jak získáte kód pro vzájemné recenze. T.1: Informační systém Informační systém tvoří primární „rozhraní“ pro stahování studijních materiálů, odevzdávání řešení, získání výsledků vyhodnocení a čtení recenzí. Zároveň slouží jako hlavní komunikační kanál mezi studenty a učiteli, prostřednictvím diskusního fóra. T.1.1 Diskusní fórum Máte-li dotazy k úlohám, organizaci, atp., využijte k jejich položení prosím vždy přednostně diskusní fórum.29 Ke každé kapitole a ke každému příkladu ze sady vytvoříme samostatné vlákno, kam patří dotazy specifické pro tuto kapitolu nebo tento příklad. Pro řešení obecných organizačních záležitostí a technických problémů jsou podobně v diskusním fóru nachystaná vlákna. Než položíte libovolný dotaz, přečtěte si relevantní část dosavadní diskuse – je možné, že na stejný problém už někdo narazil. Máte-li ve fóru dotaz, na který se Vám nedostalo do druhého pracovního dne reakce, připomeňte se prosím tím, že na tento svůj příspěvek odpovíte. Máte-li dotaz k výsledku testu, nikdy tento výsledek nevkládejte do příspěvku (podobně nikdy nevkládejte části řešení příkladu). Učitelé mají přístup k obsahu Vašich poznámkových bloků, i k Vámi odevzdaným souborům. Je-li to pro pochopení kontextu ostatními čtenáři potřeba, odpovídající učitel chybějící informace doplní dle uvážení. T.1.2 Stažení koster Kostry naleznete ve studijních materiálech v ISu: Student → PB161 → Studijní materály → Učební materiály. Každá kapitola má vlastní složku, pojmenovanou 00 (tento úvod a materiály k nultému cvičení), 01 (první běžná kapitola), 02, …, 12. Veškeré soubory stáhnete jednoduše tak, že na složku kliknete pravým tlačítkem a vyberete možnost Stáhnout jako ZIP. Stažený soubor rozbalte a můžete řešit. T.1.3 Odevzdání řešení Vypracované příklady můžete odevzdat do odevzdá- 29 Nebojte se do fóra napsat – když si s něčím nevíte rady a/nebo nemůžete najít v materiálech, rádi Vám pomůžeme nebo Vás nasměrujeme na místo, kde odpověď naleznete. várny v ISu: Student → PB161 → Odevzdávárny. Pro přípravy používejte odpovídající složky s názvy 01, …, 12. Pro příklady ze sad pak s1_a_csv, atp. (složky začínající s1 pro první, s2 pro druhou a s3 pro třetí sadu). Soubor vložíte výběrem možnosti Soubor – nahrát (první ikonka na liště nad seznamem souborů). Tímto způsobem můžete najednou nahrát souborů několik (například všechny přípravy z dané kapitoly). Vždy se ujistěte, že vkládáte správnou verzi souboru (a že nemáte v textovém editoru neuložené změny). Pozor! Všechny vložené soubory se musí jmenovat stejně jako v kostrách, jinak nebudou rozeznány (IS při vkládání automaticky předřadí Vaše UČO – to je v pořádku, název souboru po vložení do ISu neměňte) . O každém odevzdaném souboru (i nerozeznaném) se Vám v poznámkovém bloku log objeví záznam. Tento záznam i výsledky testu syntaxe by se měl objevit do několika minut od odevzdání (nemáte-li ani po 15 minutách výsledky, napište prosím do diskusního fóra). Archiv všech souborů, které jste úspěšně odevzdali, naleznete ve složce Private ve studijních materiálech (Student → PB161 → Studijní materiály → Private). T.1.4 Výsledky automatických testů Automatickou zpětnou vazbu k odevzdaným úlohám budete dostávat prostřednictvím tzv. poznámkových bloků v ISu. Ke každé odevzdávárně existuje odpovídající poznámkový blok, ve kterém naleznete aktuální výsledky testů. Pro přípravy bude blok vypadat přibližně takto: testing verity of submission from 2022-09-17 22:43 CEST subtest p1_foo passed [0.5] subtest p2_bar failed subtest p3_baz failed subtest p4_quux passed [0.5] subtest p5_wibble passed [0.5] subtest p6_xyzzy failed {bližší popis chyby} verity test failed testing syntax of submission from 2022-09-17 22:43 CEST subtest p1_foo passed subtest p2_bar failed {bližší popis chyby} subtest p3_baz failed {bližší popis chyby} subtest p4_quux passed subtest p5_wibble passed subtest p6_xyzzy passed syntax test failed testing sanity of submission from 2022-09-17 22:43 CEST subtest p1_foo passed [ 1] subtest p2_bar failed subtest p3_baz failed subtest p4_quux passed [ 1] subtest p5_wibble passed [ 1] subtest p6_xyzzy passed [ 1] sanity test failed best submission: 2022-09-17 22:43 CEST worth *5.5 point(s) Jednak si všimněte, že každý odstavec má vlastní časové razítko, které určuje, ke kterému odevzdání daný výstup patří. Tato časová razítka nemusí být stejná. V hranatých závorkách jsou uvedeny dílčí body, za hvězdičkou na posledním řádku pak celkový bodový zisk za tuto kapitolu. Také si všimněte, že best submission se vztahuje na jedno konkrétní odevzdání jako celek: v situaci, kdy odstavec „verity“ a odstavec „sanity“ nemají stejné časové razítko, nemusí být celkový bodový zisk součtem všech dílčích bodů. O konečném zisku rozhoduje vždy poslední odevzdání před příslušným termínem (opět jako jeden celek).30 Výstup pro příklady ze sad je podobný, uvažme například: testing verity of submission from 2022-10-11 21:14 CEST subtest foo-small passed subtest foo-large passed verity test passed [ 10] testing syntax of submission from 2022-10-14 23:54 CEST subtest build passed syntax test passed testing sanity of submission from 2022-10-14 23:54 CEST subtest foo passed sanity test passed best submission: 2022-10-11 21:14 CEST worth *10 point(s) 30 Můžete si tak odevzdáním nefunkčních řešení na poslední chvíli snížit výsledný bodový zisk. Uvažte situaci, kdy máte v pátek 4 body za sanity příkladů p1, p2, p3, p6 a 1 bod za verity p1, p2. V sobotu odevzdáte řešení, kde p1 neprochází sanity testem, ale p4 ano a navíc projdou verity testy příklady p4 a p6. Váš výsledný zisk bude 5.5 bodu. Tento mechanismus Vám nikdy nesníží výsledný bodový zisk pod již jednou dosaženou hranici „best submission“. 87/90 Opět si všimněte, že časová razítka se mohou lišit (a v případě příkladů ze sady bude k této situaci docházet poměrně často, vždy tedy nejprve ověřte, ke kterému odevzdání se který odstavec vztahuje a pak až jej dále interpretujte). T.1.5 Recenze Vám adresované recenze, podobně jako archiv odevzdaných souborů, naleznete ve složce Private ve studijních materiálech (Student → PB161 → Studijní materiály → Private). Shrnutí bodového zisku za tyto recenze pak naleznete v poznámkovém bloku reviews. T.1.6 Další poznámkové bloky Blok corr obsahuje záznamy o manuálních bodových korekcích (např. v situaci, kdy byl Váš bodový zisk ovlivněn chybou v testech). Podobně se zde objeví záznamy o penalizaci za opisování. Blok log obsahuje záznam o všech odevzdaných souborech, včetně těch, které nebyly rozeznány. Nedostanete-li po odevzdání příkladu výsledek testů, ověřte si v tomto poznámkovém bloku, že soubor byl správně rozeznán. Blok misc obsahuje záznamy o Vaší aktivitě ve cvičení (netýká se bodů za vzájemné recenze ani vnitrosemestrální testy). Nemáte-li před koncem cvičení, ve kterém jste řešili příklad u tabule, záznam v tomto bloku, připomeňte se svému cvičícímu. Konečně blok sum obsahuje souhrn bodů, které jste dosud získali, a které ještě získat můžete. Dostanete-li se do situace, kdy Vám ani zisk všech zbývajících bodů nebude stačit pro splnění podmínek předmětu, tento blok Vás o tom bude informovat. Tento blok má navíc přístupnou statistiku bodů – můžete tak srovnat svůj dosavadní bodový zisk se svými spolužáky. Je-li blok sum v rozporu s pravidly uvedenými v tomto dokumentu, přednost mají pravidla zde uvedená. Podobně mají v případě nesrovnalosti přednost dílčí poznámkové bloky. Dojde-li k takovéto neshodě, informujte nás o tom prosím v diskusním fóru. Případná známka uvedená v poznámkovém bloku sum je podobně pouze informativní – rozhoduje vždy známka zapsaná v hodnocení předmětu. T.2: Studentský server aisa Použití serveru aisa pro odevzdávání příkladů je zcela volitelné a vše potřebné můžete vždy udělat i prostřednictvím ISu. Nevíte-li si s něčím z níže uvedeného rady, použijte IS. Na server aisa se přihlásíte programem ssh, který je k dispozici v prakticky každém moderním operačním systému (v OS Windows skrze WSL31 – Windows Subsystem for Linux). Konkrétní příkaz (za xlogin doplňte ten svůj): $ ssh xlogin@aisa.fi.muni.cz 31 Jako alternativu, nechcete-li z nějakého důvodu WSL instalovat, lze použít program putty. Program se zeptá na heslo: použijte to fakultní (to stejné, které používáte k přihlášení na ostatní fakultní počítače, nebo např. ve fadmin-u nebo fakultním gitlab-u). T.2.1 Pracovní stanice Veškeré instrukce, které zde uvádíme pro použití na stroji aisa platí beze změn také na libovolné školní UNIX-ové pracovní stanici (tzn. z fakultních počítačů není potřeba se hlásit na stroj aisa, navíc mají sdílený domovský adresář, takže svoje soubory z tohoto serveru přímo vidíte, jako by byly uloženy na pracovní stanici). T.2.2 Stažení koster Aktuální zdrojový balík stáhnete příkazem: $ pb161 update Stažené soubory pak naleznete ve složce ~/pb161. Je bezpečné tento příkaz použít i v případě, že ve své kopii již máte rozpracovaná řešení – systém je při aktualizaci nepřepisuje. Došlo-li ke změně kostry u příkladu, který máte lokálně modifikovaný, aktualizovanou kostru naleznete v souboru s dodatečnou příponou .pristine, např. 01/e2_concat.cpp.pristine. V takovém případě si můžete obě verze srovnat příkazem diff: $ diff -u e2_concat.cpp e2_concat.cpp.pristine Případné relevantní změny si pak již lehce přenesete do svého řešení. Krom samotného zdrojového balíku Vám příkaz pb161 update stáhne i veškeré recenze (jak od učitelů, tak od spolužáků). To, že máte k dispozici nové recenze, uvidíte ve výpisu. Recenze najdete ve složce ~/pb161/reviews. T.2.3 Odevzdání řešení Odevzdat vypracované (nebo i rozpracované) řešení můžete ze složky s relevantními soubory takto: $ cd ~/pb161/01 $ pb161 submit Přidáte-li přepínač --wait, příkaz vyčká na vyhodnocení testů fáze „syntax“ a jakmile je výsledek k dispozici, vypíše obsah příslušného poznámkového bloku. Chcete-li si ověřit co a kdy jste odevzdali, můžete použít příkaz $ pb161 status nebo se podívat do informačního systému (blíže popsáno v sekci T.1). Pozor! Odevzdáváte-li stejnou sadu příprav jak v ISu tak prostřednictvím příkazu pb161, ujistěte se, že odevzdáváte vždy všechny příklady. T.2.4 Sdílení terminálu Řešíte-li příklad typu r ve cvičení, bude se Vám pravděpodobně hodit režim sdílení terminálu s cvičícím (který tak bude moct promítat Váš zdrojový kód na plátno, případně do něj jednoduše zasáhnout). Protože se sdílí pouze terminál, budete se muset spokojit s negrafickým textovým editorem (doporučujeme použít micro, případně vim umíte-li ho ovládat). Spojení navážete příkazem: $ pb161 beamer Protože příkaz vytvoří nové sezení, nezapomeňte se přesunout do správné složky příkazem cd ~/pb161/NN. T.2.5 Recenze Příkaz pb161 update krom zdrojového balíku stahuje také: 1. zdrojové kódy, které máte možnost recenzovat, a to do složky ~/pb161/to_re- view, 2. recenze, které jste na svůj kód obdrželi (jak od spolužáků, tak od vyučujících), a to do stávajících složek zdrojového balíku (tzn. recenze na příklady z první kapitoly se Vám objeví ve složce ~/pb161/01 – že se jedná o recenzi poznáte podle jména souboru, který bude začínat uživatelským jménem autora recenze, např. xrockai.00123.p1_nhamming.cpp). Chcete-li vypracované recenze odeslat: 1. přesuňte se do složky ~/pb161/to_review a 2. zadejte příkaz pb161 submit, případně doplněný o seznam souborů, které hodláte odeslat (jinak se odešlou všechny, které obsahují jakýkoliv přidaný komentář). T.3: Kostry úloh Pracujete-li na studentském serveru aisa, můžete pro překlad jednotlivých příkladů použít přiložený soubor makefile, a to zadáním příkazu $ make příklad kde příklad je název souboru bez přípony (např. tedy make e1_factorial). Tento příkaz postupně: 1. přeloží Vaše řešení překladačem gcc, 2. spustí přiložené testy, 3. spustí kontrolu nástrojem valgrind. Selže-li některý krok, další už se provádět nebude. Povede-li se překlad v prvním kroku, v pracovním adresáři naleznete spustitelný soubor s názvem příklad, se kterým můžete dále pracovat (např. ho ladit/krokovat nástrojem gdb). Existující přeložené soubory můžete smazat příkazem make clean (vynutíte tak jejich opětovný překlad a spuštění všech kontrol). T.3.1 Textový editor Na stroji aisa je k dispozici jednoduchý editor micro, který má podobné ovládání jako klasické textové editory, které pracují v grafickém režimu, a který má slušnou podporu pro práci se zdrojovým kódem. Doporučujeme zejména méně pokročilým. Další možností jsou samozřejmě pokročilé editory vim a emacs. Mimo lokálně dostupné editory si můžete ve svém oblíbeném editoru, který 88/90 PB161 Programování v jazyce C++, 16. února 2024 máte nainstalovaný u sebe, nastavit režim vzdálené editace (použitím protokolu ssh). Minimálně ve VS Code je takový režim k dispozici a je uspokojivě funkční. T.3.2 Vlastní prostředí Každý příklad je zcela obsažen v jednom standardním zdrojovém souboru, proto je jejich překlad velmi jednoduchý. Pravděpodobně každé IDE zvládne s příklady bez problémů pracovat (spouštět, ladit, atp.), musí ale běžet na systému typu POSIX (splňují všechny OS krom Windows – zde ale můžete využít WSL a případně jeho integraci do VS Code32 ). Krom IDE můžete také použít dodaný soubor makefile, pouze si v nadřazené složce (tzn. vedle složek 01, 02, atd.) vytvořte soubor local.mk, ve kterém nastavíte, jak se na Vašem systému spouští potřebné příkazy. Implicitní nastavení je toto, a funguje-li Vám, není potřeba soubor local.mk vůbec vytvářet: CC = cc VALGRIND = valgrind Můžete samozřejmě příklady překládat a kontrolovat i ručně. 32 https://code.visualstudio.com/docs/remote/wsl 89/90 Část U: Doporučení k zápisu kódu Tato sekce rozvádí obecné principy zápisu kódu s důrazem na čitelnost a korektnost. Samozřejmě žádná sada pravidel nemůže zaručit, že napíšete dobrý (korektní a čitelný) program, o nic více, než může zaručit, že napíšete dobrou povídku nebo namalujete dobrý obraz. Přesto ve všech těchto případech pravidla existují a jejich dodržování má obvykle na výsledek pozitivní dopad. Každé pravidlo má samozřejmě nějaké výjimky. Tyto jsou ale výjimkami proto, že nastávají výjimečně. Některá pravidla připouští výjimky častěji než jiná: 1 Dekompozice Vůbec nejdůležitější úlohou programátora je rozdělit problém tak, aby byl schopen každou část správně vyřešit a dílčí výsledky pak poskládat do korektního celku. A. Kód musí být rozdělen do ucelených jednotek (kde jednotkou rozumíme funkci, typ, modul, atd.) přiměřené velikosti, které lze studovat a používat nezávisle na sobě. B. Jednotky musí být od sebe odděleny jasným rozhraním, které by mělo být jednodušší a uchopitelnější, než kdybychom použití jednotky nahradili její definicí. C. Každá jednotka by měla mít jeden dobře definovaný účel, který je zachycený především v jejím pojmenování a případně rozvedený v komentáři. D. Máte-li problém jednotku dobře pojmenovat, může to být známka toho, že dělá příliš mnoho věcí. E. Jednotka by měla realizovat vhodnou abstrakci, tzn. měla by být obecná – zkuste si představit, že dostanete k řešení nějaký jiný (ale dostatečně příbuzný) problém: bude Vám tato konkrétní jednotka k něčemu dobrá, aniž byste ji museli (výrazně) upravovat? F. Má-li jednotka parametr, který fakticky identifikuje místo ve kterém ji používáte (bez ohledu na to, je-li to z jeho názvu patrné), je to často známka špatně zvolené abstrakce. Máte-li parametr, který by bylo lze pojmenovat called_from_bar, je to jasná známka tohoto problému. G. Daný podproblém by měl být vyřešen v programu pouze jednou – nedaří-li se Vám sjednotit různé varianty stejného nebo velmi podobného kódu (aniž byste se uchýlili k taktice z bodu d), může to být známka nesprávně zvolené dekompozice. Zkuste se zamyslet, není-li možné problém rozložit na podproblémy jinak. 2 Jména Dobře zvolená jména velmi ulehčují čtení kódu, ale jsou i dobrým vodítkem při dekompozici a výstavbě abstrakcí. A. Všechny entity ve zdrojovém kódu nesou anglická jména. Angličtina je univerzální jazyk programátorů. B. Jméno musí být výstižné a popisné: v místě použití je obvykle jméno náš hlavní (a často jediný) zdroj informací o jmenované entitě. Nutnost hledat deklaraci nebo definici (protože ze jména není jasné, co volaná funkce dělá, nebo jaký má použitá proměnná význam) čtenáře nesmírně zdržuje.33 C. Jména lokálního významu mohou být méně informativní: je mnohem větší šance, že význam jmenované entity si pamatujeme, protože byla definována před chvílí (např. lokální proměnná v krátké funkci). D. Obecněji, informační obsah jména by měl být přímo úměrný jeho rozsahu platnosti a nepřímo úměrný frekvenci použití: globální jméno musí být informativní, protože jeho definice je „daleko“ (takže si ji už nepamatujeme) a zároveň se nepoužívá příliš často (takže si nepamatujeme ani to, co jsme se dozvěděli, když jsme ho potkali naposled). E. Jméno parametru má dvojí funkci: krom toho, že ho používáme v těle funkce (kde se z pohledu pojmenování chová podobně jako lokální proměnná), slouží jako dokumentace funkce jako celku. Pro parametry volíme popisnější jména, než by zaručovalo jejich použití ve funkci samotné – mají totiž dodatečný globální význam. F. Některé entity mají ustálené názvy – je rozumné se jich držet, protože čtenář automaticky rozumí jejich významu, i přes obvyklou stručnost. Zároveň je potřeba se vyvarovat použití takovýchto ustálených jmen pro nesouvisející entity. Typickým příkladem jsou iterační proměnné i a j. G. Jména s velkým rozsahem platnosti by měla být také zapamatovatelná. Je vždy lepší si přímo vzpomenout na jméno funkce, kterou právě potřebuji, než ho vyhledávat (podobně jako je lepší znát slovo, než ho jít hledat ve slovníku). H. Použitý slovní druh by měl odpovídat druhu entity, kterou pojmenovává. Proměnné a typy pojmenováváme přednostně podstatnými jmény, funkce přednostně slovesy. I. Rodiny příbuzných nebo souvisejících entit pojmenováváme podle společného schématu (table_name, table_size, table_items – nikoliv např. items_in_table; list_parser, string_parser, set_parser; find_min, find_max, erase_max – nikoliv např. erase_maximum nebo erase_greatest nebo max_re- move). J. Jména by měla brát do úvahy kontext, ve kterém jsou platná. Neopakujte typ proměnné v jejím názvu (cars, nikoliv list_of_cars ani set_of_cars) nemá-li tento typ speciální význam. Podobně jméno nadřazeného typu nepatří do jmen jeho metod (třída list by měla mít metodu length, nikoliv list_length). K. Dávejte si pozor na překlepy a pravopisné chyby. Zbytečně znesnad- 33 Nejde zde pouze o samotný fakt, že je potřeba něco vyhledat. Mohlo by se zdát, že tento problém řeší IDE, které nás umí „poslat“ na příslušnou definici samo. Hlavní zdržení ve skutečnosti spočívá v tom, že musíme přerušit čtení předchozího celku. Na rozdíl od počítače je pro člověka „zanořování“ a zejména pak „vynořování“ na pomyslném zásobníku docela drahou operací. ňují pochopení a (zejména v kombinaci s našeptávačem) lehce vedou na skutečné chyby způsobené záměnou podobných ale jinak napsaných jmen. Navíc kód s překlepy v názvech působí značně neprofesionálně. 3 Stav a data Udržet si přehled o tom, co se v programu děje, jaké jsou vztahy mezi různými stavovými proměnnými, co může a co nemůže nastat, je jedna z nejtěžších částí programování. TBD: Vstupní podmínky, invarianty, … 4 Řízení toku Přehledný, logický a co nejvíce lineární sled kroků nám ulehčuje pochopení algoritmu. Časté, komplikované větvení je naopak těžké sledovat a odvádí pozornost od pochopení důležitých myšlenek. TBD. 5 Volba algoritmů a datových struktur TBD. 6 Komentáře Nejde-li myšlenku předat jinak, vysvětlíme ji doprovodným komentářem. Čím těžší myšlenka, tím větší je potřeba komentovat. A. Podobně jako jména entit, komentáře které jsou součástí kódu píšeme anglicky.34 B. Případný komentář jednotky kódu by měl vysvětlit především „co“ a „proč“ (tzn. jaký plní tato jednotka účel a za jakých okolností ji lze použít). C. Komentář by také neměl zbytečně duplikovat informace, které jsou k nalezení v hlavičce nebo jiné „nekomentářové“ části kódu – jestli máte například potřebu komentovat parametr funkce, zvažte, jestli by nešlo tento parametr lépe pojmenovat nebo otypovat. D. Komentář by neměl zbytečně duplikovat samotný spustitelný kód (tzn. neměl by se zdlouhavě zabývat tím „jak“ jednotka vnitřně pracuje). Zejména jsou nevhodné komentáře typu „zvýšíme proměnnou i o jedna“ – komentář lze použít k vysvětlení proč je tato operace potřebná – co daná operace dělá si může kažďý přečíst v samotném kódu. 7 Formální úprava TBD. 34 Tato sbírka samotná představuje ústupek z tohoto pravidla: smyslem našich komentářů je naučit Vás poměrně těžké a často nové koncepty, a její cirkulace je omezená. Zkušenost z dřívějších let ukazuje, že pro studenty je anglický výklad značnou bariérou pochopení. Přesto se snažte vlastní kód komentovat anglicky – výjimku lze udělat pouze pro rozsáhlejší komentáře, které byste jinak nedokázali srozumitelně formulovat. V praxi je angličtina zcela běžně bezpodmínečně vyžadovaná.