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:
Ve studijních materiálech 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“.
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í.
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ě.
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í.
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.2 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.
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/z₁“ – první kolo známek za kvalitu kódu sady X,
„sX/op“ – termín pro opravná odevzdání sady X,
„sX/z₂“ – 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).
Abyste předmět úspěšně ukončili, musíte v každém bloku3 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.4
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 účast5 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 ú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).
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ů).
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.
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).
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:
do odevzdávárny s názvem NN v ISu (např. 01),
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í
kapitoly,
„01/p“ – poslední (sobotní) termín odevzdání příprav z 1.
kapitoly,
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:
analyzovat problémy, na které jste při samostatné domácí práci
narazili, a zejména prodiskutovat, jak je vyřešit,
ř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):
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.
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.
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í).
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.
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:
do odevzdávárny s názvem sN_úkol v ISu (např. s1_a_queens),
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.
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,
„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.
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).
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/z₁“ – obdržíte známky za první sadu,
„s1/op“ – termín pro odevzdání opravených řešení 1. sady,
„s1/z₂“ – 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í.
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).
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).
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
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íny6 jsou:
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.
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í.
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 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 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í).
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).
fibonacci – iterativní výpočet Fibonacciho čísel,
comb – výpočet kombinačního čísla,
hamming – hammingova vzdálenost dvojkového zápisu,
root † – výpočet -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:
factorial – spočtěte faktoriál zadaného čísla,
concat – zřetězení binárního zápisu dvou čísel,
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:
nhamming – Hammingova vzdálenost s libovolným základem,
balanced – ciferné součty ve vyvážených soustavách,
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.
bitwise – ternární bitové operátory,
euler – Eulerova funkce (počet nesoudělných čísel),
hamcode – kód pro detekci chyb Hamming(8,4),
cbc – cipher block chaining,
cellular – celulární automat nad celým číslem,
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.
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 clang-tidy
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.
V této ukázce naprogramujeme klasický ukázkový algoritmus, totiž
výpočet -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á:
popisuje návratovou hodnotu (zejména její typ),
udává název podprogramu a
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:
typu, případně klíčového slova auto,
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),
volitelného inicializátoru, který popisuje počáteční
hodnotu proměnné.
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:
klíčové slovo for,
hlavička cyklu, uzavřená v kulatých závorkách,
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,
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),
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),
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-hodnotu7 –
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ů):
určí návratovou hodnotu podprogramu (tato se získá
vyhodnocením výrazu uvedeného po klíčovém slově
return),
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ředstavě. 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.
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é.
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:
kde . 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 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 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:
A tedy:
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í není
potřeba vyčíslovat. Víme jistě, že výsledek bude celé číslo,
tzn. všechny faktory se musí pokrátit s nějakými
faktory . 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
největší kombinační číslo s daným , největší
mezivýsledek při výpočtu bude . Využijeme-li
navíc symetrie , můžeme tuto mez zlepšit na
a zároveň zabezpečit, že . 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
resp. , 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 nebo . 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 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í.
Postup implementovaný podprogramem comb nám umožňuje
spočítat, za pomoci 64-bitových proměnných, všechna
kombinační čísla pro , a to i přesto, že nejen , ale 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 ) );
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 &.8 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.9
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é inicializovat 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.
++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 );
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.
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ší.
† V této poslední ukázce bude naším cílem spočítat celočíselnou
-tou odmocninu zadaného nezáporného čísla . Nejprve ale
budeme potřebovat dvě pomocné funkce: celočíselný dvojkový
logaritmus a -tou mocninu. Vyzbrojeni těmito funkcemi pak
budeme schopni odmocninu vypočítat tzv. Newtonovou-Raphsonovou
metodou.
Celočíselný dvojkový logaritmus čísla definujeme jako
největší celé číslo takové, že . Za povšimnutí stojí,
že pro takové neexistuje – proto tuto funkci
definujeme pouze pro kladná .
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 , který obsahuje člen pro každý
nenulový bit . Dvojkovým logaritmem bude právě nejvyšší
mocnina dvojky, která se v tomto rozvoji objeví: jistě pak
platí, že , stačí se ujistit, že takto získané je
největší možné. Uvažme tedy, že existuje nějaké a
zároveň . Pak se ale musí nutně objevit ve
dvojkovém rozvoji čísla , což je spor s tím, že 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“).10 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 jiné nutnosti definovat hodnotu pro .
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
platí kde . Za cenu výpočtu
jedné druhé mocniny – – 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ší: pro . V rekurzivním zápisu bychom
mohli tento vztah přímo použít, v tom iterativním ale
nastane drobný problém: faktor 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 :
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 dopadl takto:
Uvažme ještě výpočet . 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ě:
}
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ší takové, že
.
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:
metoda nám umožní rychle nalézt takové, že pro
zadané platí ,
potřebujeme k tomu samozřejmě definici ,
dále její první derivaci
a počáteční odhad hledané hodnoty .
Výpočet opakovaně zlepšuje aktuální odhad , a to pomocí
vzorce:
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
Je-li , pak jistě , což je ale přesně
definice -té odmocniny (prozatím té reálné). Potřebujeme
ještě derivaci, která je naštěstí velmi jednoduchá:
protože je celočíselná konstanta (pro bychom
ovšem narazili na problém). Celkem tedy:
Nebo výhodněji (přechodem na společný jmenovatel a krácením
mocnin ):
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ě
). Využijeme k tomu dříve definovaný dvojkový logaritmus.
Protože , můžeme hledanou odmocninu
odhadnout jako pro . Také si
všimneme, že tento odhad leží na stejné straně jediného
stacionárního bodu funkce (totiž ) 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 , je funkce 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 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í: najdeme-li 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:
z konvexnosti plyne, že odhad, který je příliš velký, se
musí ke skutečnému výsledku provedením iterace přiblížit,
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
.
while ( true )
{
const auto t = ( n - 1 ) * s + k / int_pow( s, n - 1 );
const auto s_next = t / n;
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 );
}
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 bitů). Je-li zápis čísla b delší než
b_bits, výsledek není definován.
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.
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.
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).
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.
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.
Najděte nejmenší nezáporné číslo 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í . Protože potřebný zápis 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 , vyberte ten nejkratší možný.
Příklad: pro word = 0x100000001 bude hledané , 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 );
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 ,
pětkovou , 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 ve vyvážené soustavě o základu
probíhá podobně, jako v té klasické se stejným základem. Nejprve
si připomeneme klasický algoritmus. Nastavíme a
opakujeme:
cifru získáme jako zbytek po dělení základem ,
spočítáme tak, že vydělíme základem ,
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 není povolenou číslicí. Všimneme si, že
musí po každém kroku platit (přímo z definice použitých operací):
Tuto rovnost musíme zachovat, ale zároveň potřebujeme, aby
bylo platnou číslicí. To zajistíme jednoduše tak, že od
odečteme a přičteme místo toho jedničku k (tím se
součet jistě nezmění, protože jsme jedno ubrali a jedno
přidali).
Vstupem pro problém subset sum je množina povolených čísel
a hledaný součet . Řešením je pak podmnožina taková,
že součet jejích prvků je právě .
V tomto příkladu budeme pracovat pouze s množinami , které
obsahují kladná čísla menší nebo rovna 64, a které lze tedy
reprezentovat jediným bezznaménkovým číslem z rozsahu
(prázdná množina) až (obsahuje všechna čísla ). Číslo pak reprezentuje množinu , číslo množinu
, číslo množinu 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 );
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
0
0
1
0
1
0
0
1
1
1
0
0
1
0
1
1
1
0
1
1
1
Snadno se nahlédne, že zafixujeme-li tuto tabulku a zadáme
hodnoty až , 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
předáme v nejnižším a 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 );
This is a straightforward math exercise. Implement Euler's [φ],
for instance using the product formula where
the product is over all distinct prime divisors of n. You may
need to take care to compute the result exactly.
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:
Blok je správně utvořený, platí-li tyto vztahy:
kde 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 (d₄ v nejnižším bitu). Jinak vrátí
false a hodnotu out nemění.
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:
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:
atd.
Výpočet kroku by mělo být možné provést na libovolně širokém
celočíselném typu.
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:
stats – záznamové typy, zjednodušený for cyklus
primes – vkládání prvků do hodnoty typu std::vector
iterate – vytvoření posloupnosti iterací funkce
dfs – dosažitelnost vrcholu v grafu
bfs † – nejkratší vzdálenost v neohodnoceném grafu
Elementary exercises:
fibonacci – stará posloupnost, nová signatura
reflexive – reflexivní uzávěr zadané relace
unique – odstraněni duplicit ve vektoru
Preparatory exercises:
minsum – dělení posloupnosti čísel podle součtu
connected – rozklad grafu na komponenty souvislosti
divisors – kontejner jako vstupně-výstupní parametr
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ýrazemexpr.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.11 *
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 );
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.
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.12 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;
}
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 };
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.
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 which appears
in the relation, the pair 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 it also contains the opposite,
.
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 .
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;
}
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 dosažitelný z vrcholu ?“. Budeme navíc
požadovat, aby byla příslušná cesta neprázdná (tzn. budeme
považovat za dosažitelné z 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 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:
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
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 );
}
† The goal of this demonstration will be to find the shortest
distance in an unweighted, directed graph:
starting from a fixed (given) vertex,
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::queue13
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 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.
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.
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).
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 );
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 , kde je velikost vstupní
matice.
V grafu je hrana přítomna právě tehdy, je-li na řádku
ve sloupci hodnota true.
using graph = std::vector< std::vector< bool > >;
using component = std::set< int >;
using components = std::set< component >;
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 .
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.
† 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 );
Rozhodněte, zda je zadaný neorientovaný graf bipartitní (tzn.
jeho vrcholy lze rozdělit do dvou množin tak, že každá
hrana má jeden vrchol v a jeden v ). Protože graf je
neorientovaný, seznamy sousedů na vstupu jsou symetrické.
using edges = std::vector< int >;
using graph = std::map< int, edges >;
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 );
Consider a single-player game that takes place on a 1D playing
field like this:
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.
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 );
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 is a part of the
graph, so is .
using graph = std::set< std::pair< int, int > >;
bool has_3colouring( const graph &g );
V tomto cvičení implementujeme tzv. semínkové vyplňování, obvykle
popsané algoritmem, který:
dostane na vstupu bitmapu (odélníkovou síť pixelů),
počátečnou pozici v síti,
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):
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 nulovou,
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 );
V této ukáce budeme počítat histogram (číselných) -gramů, tzn.
bloků po sobě jdoucích čísel v nějaké delší sekvenci.
Jednotlivé -gramy se mohou překrývat (-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
-gramů bude řádově méně než vstupních dat.
Budeme implementovat dvě metody:
count, která pro zadaný -gram vrátí počet jeho
dosavadních výskytů, a metodu
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 -gram), hodnotou pak počet výskytů
tohoto -gramu.
std::map< std::vector< int >, int > _counter;
Druhou složkou bude posuvné okno, ve kterém budeme
uchovávat poslední zpracovaný -gram. Je to proto, že
některé -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 -gramy, a realizovat
ho budeme jako instanci std::deque14.
std::deque< int > _window;
Nejprve implementujeme pomocnou metodu, která zpracuje jedno
číslo. Je-li okno již plné, odstraníme z něj nejstarší
hodnotu. Je-li po vložení čísla okno dostatečně plné,
výsledný -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 )
{
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ý -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;
}
};
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 .
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:
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.
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.
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
, but we currently do not have the mechanisms to do
that either. This will improve in the next chapter.
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 .
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
.
gf7 operator*( gf7 b ) const
{
return gf7{ value * b.value }.normalize();
}
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).
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 );
}
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.
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í. Jsou-li a zlomky v základním
tvaru, můžete se u libovolné operace spolehnout, že
žádný ze součinů , , a nepřeteče
rozsah int-u.
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 , nebo
zajistěte, aby platilo polar( 1, x ) == polar( 1, x + 2π ).
Navrhněte typ numset, kterého hodnoty budou reprezentovat
množiny čísel. Jsou-li ns₁, ns₂ hodnoty typu numset a dále
i, j jsou hodnota typu int, požadujeme následující operace:
ns₁.add( i ) – vloží do ns₁ číslo i,
ns₁.del( i ) – odstraní z ns₁ číslo i,
ns₁.del_range( i, j ) – odstraní z ns₁ všechna čísla,
která spadají do uzavřeného intervalu ,
ns₁.merge( ns₂ ) – přidá do ns₁ všechna čísla přítomná
v ns₂,
ns₁.has( i ) – rozhodne, zda je i přítomné v ns₁.
Složitost:
del_range a merge musí mít nejvýše lineární složitost,
Řetězový zlomek reprezentujej racionální číslo jako součet
celého čísla a převrácené hodnoty nějakého dalšího
racionálního čísla, , které je samo zapsáno pomocí řetězového
zlomku. Tedy
a tak dále, až než je nějaké celé číslo, kterým sekvence
končí (pro racionální číslo se to jistě stane po konečném počtu
kroků). Hodnotám ří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.
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 – tzn. součet nezáporných
celočíseslných mocnin proměnné , 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 a tedy
jejich koeficienty musí být sečteny.
Pro každý polynom existuje nějaké takové, že všechny mocniny
větší než 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 a koeficient, obojí celá čísla.
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ů 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:
má-li pole žádné nebo 1 prvek, je již seřazené: konec;
jinak jeden z prvků vybereme jako pivot,
přeuspořádáme pole na dvě menší partice (viz popis metody
partition výše),
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.
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 udávají vždy
souřadnice spodní a horní meze příslušného sloupce – pole –
v rozsahu souřadnice . Například:
Loď lze ovládat nastavením stoupání (pro každý posun o
jednotek doprava se loď zároveň posune jednotek nahoru;
je-li záporné, posouvá se dolů). Hra má tyto 4 metody:
append( y₁, y₂ ) 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
a počáteční výška i stoupání lodě jsou 0.
Uvažme těleso a číslo takové, že (tedy zejména
). Těleso můžeme rozšířit na tzv. (algebraické)
číselné těleso (v tomto případě konkrétně kvadratické těleso)
, kterého prvky jsou tvaru , kde .
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í .
struct qf;
Protože k dispozici máme pouze celá čísla, k zápisu jednoho prvku
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 );
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 .
using cell = std::pair< int, int >;
using grid = std::set< cell >;
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 ); }
};
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.
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).
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:
first should work correctly when we call it on a constant,
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 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; }
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.
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.
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.
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 );
}
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:
pervasive use of automatic variables, through value
semantics – local variables and arguments are automatically
destroyed when they go out of scope,
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.
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.
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.
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;
}
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 );
}
}
};
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.
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.
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,15 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ý:
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;
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.
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).
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é
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 >;
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).
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 :
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
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.
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.
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).
a_queens – problém osmi dam,
b_city – panorama města,
c_magic – doplnění magického čtverce,
d_reversi – třírozměrná verze hry Reversi,
e_cellular – celulární automat na kružnici,
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.
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 64² operací – totiž O(n²) kde
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.
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 , -ová
souřadnice roste směrem na východ, -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:
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 -ovou souřadnicí.
každé políčko obsahuje jedno z čísel až (a to tak,
že se žádné z nich neopakuje), a
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 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
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ší).
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 , a ). 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ě spojeno16 č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 );
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.
Uvažujme dvojicí polí (krychlí) , a úsečku , která spojuje jejich středy, a která prochází středem stěny, hrany nebo vrcholem pole . Nepřerušeným spojením myslíme všechna pole, které úsečka protíná, vyjma a samotných. Dvojici polí, pro které potřebná úsečka neexistuje, nelze nepřerušeně spojit.
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:
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 až
(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 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.
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 čistou 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 (kde a jsou počty cifer operandů).
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:
queue – a queue with stable references
finexp – like regexps but finite
expr – expressions with operators and shared pointers
family – genealogy with weak pointers
Elementary exercises:
dynarray – a simple array with a dynamic size
list – a simple linked list with minimal interface
iota – an iterable integer range
Preparatory exercises:
unrolled – a linked list of arrays
bittrie – bitwise tries (radix trees)
solid – efficient storage of optional data
chartrie – binary tree for holding string keys
bdd – binary decision diagrams
rope – a string-like structure with cheap concatenation
Regular exercises:
circular – a singly-linked circular list
zipper – implementing zipper as a linked list
segment – a binary tree of disjoint intervals
diff – automatic differentiation
critbit – more efficient version of binary tries
refcnt † – implement a simple reference-counted heap
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.
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.
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.
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.
And finally the dereference operator: this is what will be
called when *it is evaluated. Also notice that the operator
is const but the result is a non-const reference – this
is because the iterator generalizes a pointer – the
const-ness of the pointed-to object is unrelated to the
const-ness of the pointer/iterator.
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).
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 headnode and by
rewriting first, we lost all pointers to that node. This
means two things:
the old head's next pointer, now null, is no longer
accessible
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.
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.
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;
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.
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.
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;
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.
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.
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ů.
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.
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 , a ).
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.
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.
Můžete si představit takto reprezentovanou trie jako -ární,
které by bylo zcela jistě nepraktické přímo implementovat. Proto
reprezentujeme virtuální uzly pomyslného -á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é.
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ď:
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á;
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.
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.17
Naprogramujte:
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žování.
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.
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).
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.
In this exercise, we will implement automatic differentiation of
simple expressions. You will need the following rules:
linearity:
the Leibniz rule:
chain rule:
derivative of exponential:
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).
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.
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:
account – a simple inheritance example
shapes – polymorphism and late dispatch
expr – dynamic and static types, more polymorphism
destroy – virtual destructors
factory – polymorphic return values
Elementary exercises:
resistance – compute resistance of a simple circuit
perimeter – shapes and their perimeter length
fight – rock, paper and scissors
Preparatory exercises:
prisoner – the famous dilemma
bexpr – boolean expressions with variables
sexpr – a tree made of lists (lisp style)
network – a network of counters
filter – filter items from a data source
geometry – shapes and visitors
Regular exercises:
bom – polymorphism and collections
circuit – calling virtual methods within the class
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.
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 )
: 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.
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.
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.
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.
A helper function (lambda) to decide whether a point is
inside (or on the 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.
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.
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.
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:
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.
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 always18 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:
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.
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.
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(); }
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 and therefore sum is a
subtype of node, too.
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.
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.
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.
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.
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 ) {}
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.
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.
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:
The resistance that we are interested in is between the points A
and B. Given and connected in series, the total
resistance is . For the same resistors connected in
parallel, the resistance is given by .
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.
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 floating-point numbers.
class shape;
class circle;
class rectangle;
bool check_shape( const shape &s, double p )
{
return std::fabs( s.perimeter() - p ) < 1e-8;
}
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.
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:
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:
falsefalse → bit 0 of table
falsetrue → bit 1 of table
truefalse → bit 2 of table
truetrue → 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.
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.
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íží.
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á.
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. 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.
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.
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.
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_ptrby 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.
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 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.
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:
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ů:
zvýšení proměnné o jedničku, a++,
cyklus while tvaru while (a != b) stmt, a konečně
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
Metoda eval dostane na vstupu jednak iniciální nastavení
proměnných (hodnotu typu state definovaného níže), jednak
limit na délku výpočtu – tento limit udává kolik může
program jako celek vykonat srovnání. Po provedení 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;
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.
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.
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):
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):
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.
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).
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
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.
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.
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.
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.
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.
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:
it will be useful in both the move assignment operator and
in the destructor,
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.
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).
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 respectively.
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:
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,
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.
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ěť.
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.
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.
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:
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.
† 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.19
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 to the caller,
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é pole20 – 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;
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.
† 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:
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
reinterpret_cast.
class token
{
int _value;
bool _robbed = false;
public:
static int _count;
token( int i ) : _value( i ) { ++ _count; }
~token() { -- _count; }
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.
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:
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,
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 );
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.
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.
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
{
_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.
Napište funkci kernel, která spočítá rozklad21 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 daného
obrazu . Formálněji .
Můžete předpokládat, že návratový typ funkce f je int.
Časová složitost nesmí být horší, než O(n⋅logn).
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, 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.
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 ),
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 . 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 .
Zde by f mohl být například tenzorový součin.
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 reference-like 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.22
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_ptrpast 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 */
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.
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 c₀ … cₙ prvky c, výsledek funkce fold pak
odpovídá f( … f( f( i, c₀ ), c₁ ), … cₙ ). 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.
† 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 neumí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.
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.
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 );
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 );
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ů.
a_natural – rozšíření s1/f_natural o dělení,
b_treap – jednoduchý vyhledávací strom,
c_robots – programujeme roboty na mapě,
d_network – simulátor počítačové sítě s přepínači,
e_tree – práce s heterogenním stromem,
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.
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 a d = m.to_double(), musí platit m - 2ˡ ≤ natural( d )
a zároveň natural( d ) ≤ m + 2ˡ; je-li 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).
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:
na základě klíče vložíme uzel na vhodné místo tak, aby nebyla
porušena vlastnost vyhledávacího stromu,
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).23
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).
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.
V této úloze budete programovat jednoduchou hru, ve které se ve
volném třírozměrném prostoru pohybují robotické entity tří barev:
č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),
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ě.
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 menší než jeden
metr, 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:
metoda tick posune čas o 1/60 sekundy, a provede tyto akce:
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,
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ě),
Poznámka ke srovnávání vzdáleností: při použití aritmetiky
s plovoucí desetinnou čárkou vznikají nepřesnosti a tyto se mohou
do jisté míry kumulovat. Proto je-li nějaký robot o nejvýše metrů vzdálenější než ten nejbližší, budeme je považovat za
stejně vzdálené. Za nejbližší ze všech těchto „stejně vzdálených“
pak vybereme ten, který byl do hry přidán jako první.
Z podobného důvodu volíme v testech startovní pozice tak, aby na
očekávané trajektorii nedošlo k situaci, kdy bude vzdálenost
relevantních robotů v intervalu .
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 a dosud mezi nimi přímé
spojení nebylo).
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;
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 v 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::domain_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;
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é.
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í 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.
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),
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.
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 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í orientace:
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í. Je-li 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 & );
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).
Implementujte množinu libovolných celých čísel, s těmito
operacemi:
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.
Napište čistou, generickou funkci distinct( s ), která 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 , kde je
počet různých prvků (výsledek volání distinct) a je délka
vstupní posloupnosti.
Napište podprogram reorder( s, weight ), který pro zadanou
posloupnost s a funkci weight na místě přeuspořádá s tak,
že pro budou prvky s váhou předcházet prvkům s váhou
. 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 kde je počet
různých vah, které se objeví na vstupu, a je délka
posloupnosti s. Je také povoleno využít lineární množství
dodatečné paměti.
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(), kde 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 , platí buď nebo
naopak . Pro zcela seřazený vstup je .
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á
výrazně efektivnější než obecné řazení).
Naprogramujte proceduru intervals, která z posloupnosti
dvojic (zleva uzavřených, zprava otevřených intervalů) vytvoří
vzestupně seřazenou posloupnost 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 .
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 nemodifikujte.
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.
Napište čistou funkci sorted_ranges, která na vstupu dostane
kontejner (nebo rozsah ve smyslu std::range) a kladné číslo
n. 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
kde je délka vstupní posloupnosti.
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
kde je délka vstupní posloupnosti.
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.
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 = [&]
{
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.
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" } ));
† 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
delimiter-separated 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 >;
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.
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.
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.
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 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.
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.
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.
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 tak, aby:
bylo zachováno rozdělení na odstavce,
jednotlivý řádek textu nepřesáhl cols,
zachová celistvost slov (tzn. smí měnit pouze mezery a znaky
nového řádku),
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.
Regulární gramatika má pravidla tvaru nebo kde
, jsou neterminály a 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:
jeden znak, který reprezentuje levou stranu (velké písmeno
anglické abecedy) a
jeden terminál (malé písmeno), nebo jeden terminál a jeden
neterminá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í.
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.
Napište čistou funkci, která spočítá naivní rozdělení textu na
jednotlivá slova.26 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):
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.
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ů).
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,
Č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 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.
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.
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;
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: { "key₁": 7, "key₂": [ 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).
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.
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).
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.
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.
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.
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;
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.
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).
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.
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.
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.
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.
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.
We will implement a simple wrapper around std::fstream 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.
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 };
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.
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.
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.
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 >;
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.
c_real – reálná čísla (dále rozšiřuje s2/a_natural),
d_json – reprezentace JSON-u použitím std::variant,
e_robots – rozšíření s2/c_robots o programovatelné roboty,
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.
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.
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.
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();
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 );
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 ),
a.log1p( p ) – přirozený logaritmus , kde
.
Přesností se myslí absolutní hodnota rozdílu skutečné (přesné) a
reprezentované hodnoty. Pro aproximaci odmocnin je vhodné použít
Newtonovu-Raphsonovu 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.
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.
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 , vrátí -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).
Č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ěť).
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í:
deklarace, které popisují jak roboty ve hře a jejich
startovní pozice, tak případné pomocné proměnné,
příkazy, které se provedou jednou na začátku hry,
příkazy, které se provedou každý tik, dokud hra neskončí.
Program by mohl vypadat například takto:
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.
S hodnotami (a proměnnými, které hodnoty daných typů aktuálně
obsahují), lze provádět tyto operace:
čísla lze sčítat, odečítat, násobit a srovnávat (neterminály
expr a cond),
trojice lze sčítat, odečítat a srovnat (ale pouze rovností),
roboty lze posílat za jiným robotem nebo na zadané
souřadnice (příkaz let),
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 ([ p₁ - p₂ ] tak spočítá
vzdálenost bodů p₁ a p₂.
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ě.
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 nesmí záležet na pořadí přidávání a propojování
(případně rozpojování) uzlů,27
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):
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.
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.
Máte-li dotazy k úlohám, organizaci, atp., využijte k jejich
položení prosím vždy přednostně diskusní fórum.28 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í.
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.
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.
Vypracované příklady můžete odevzdat do odevzdá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).
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).29
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)
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).
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“.
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.
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.
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
WSL30 – Windows Subsystem for Linux). Konkrétní příkaz (za xlogin
doplňte ten svůj):
$ ssh xlogin@aisa.fi.muni.cz
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).
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).
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.
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.
Ř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.
Příkaz pb161 update krom zdrojového balíku stahuje také:
zdrojové kódy, které máte možnost recenzovat, a to do složky
~/pb161/to_review,
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:
přesuňte se do složky ~/pb161/to_review a
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ář).
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ě:
přeloží Vaše řešení překladačem gcc,
spustí přiložené testy,
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).
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ý 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í.
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 Code).
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ě.
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á:
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.
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ě.
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í.
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.
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í.
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?
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.
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 F), 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.
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í.
Všechny entity ve zdrojovém kódu nesou anglická jména.
Angličtina je univerzální jazyk programátorů.
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.31
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).
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).
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.
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.
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).
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.
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_remove).
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).
Dávejte si pozor na překlepy a pravopisné chyby. Zbytečně
znesnadň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ě.
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í.
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í.
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.
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.
Podobně jako jména entit, komentáře které jsou součástí kódu
píšeme anglicky.32
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).
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.
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.
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á.