{{page>courses:b6b36pjc:styles#common&noheader&nofooter}} {{page>courses:b6b36pjc:styles#cviceni&noheader&nofooter}} ===== Cvičení 1: Základy kompilace, CMake a vstup/výstup ===== Účelem prvního cvičení je naučit se jak funguje kompilace C%%++%% kódu a jak použít CMake aby za nás tuto práci automatizoval. Předpokládáme, že již máte funkční prostředí, buďto protože používáte školní stroje, nebo jste si dle návodu zprovoznili CMake a git na svém stroji. ==== Kompilace ==== Začneme tím, že zkompilujeme "Hello world" v C%%++%%: #include int main() { std::cout << "Hello world\n"; } K tomu, aby kompilátor udělal z tohoto zdrojového kódu spustitelný program, ho musel zkompilovat. My budeme pracovat se zjednodušeným modelem kompilace, který se zkládá ze 3 fází(([[https://en.cppreference.com/w/cpp/language/translation_phases|Striktně řečeno jich je 9]])). - Preproces - Kompilace - Linkování které jsou prováděny preprocessorem, kompilátorem a linkerem respektive. Jak jsme viděli, pro základní použití je nemusíme rozlišovat, postará se o to tzv. "driver" kompilátoru, ale stejně se nám hodí o nich vědět. === Preproces(sor) === Když kompilujeme ''.cpp'' soubor, preprocessor je první nástroj, který ho přečte a musí nějak zpracovat. Je zodpovědný za vyřešení tzv. "preprocessor directives", například ''#include'', ''#define'', ''#ifdef''. Je též zodpovědný za expanzi maker, kdy se token v programu nahrazuje za jiný, dle předchozích ''#define'' direktiv. **Příklad** //Originální kód// #define KONSTANTA 123 int main() { return KONSTANTA; } //Možný výstup z preprocessoru// #line 1 "test.cpp" int main() { return 123; } Jak vidíme, token ''KONSTANTA'' byl definován jako ''123'', a jeho pozdější výskyt tedy byl nahrazen přímo za ''123''. Preprocessor se většinou volá přes tzv. "driver" kompilátoru, tj přímo spustitelná binárka ''gcc'', ''clang'', ''cl.exe'', etc. s přepínačem ''-E'' (nebo ''/E''): gcc -E test.cpp clang -E test.cpp cl /E test.cpp Kvůli ''#include'' direktivám preproces obvykle soubory značně zvětší. **Zjistěte, kolik řádků má náš "Hello world" program poté, co proběhne preprocessing.** === Kompilace === Kompilace samotná probíhá na výstupu z preprocesu a převádí C%%++%% kód, již bez direktiv pro preprocessor, do podoby které již rozumí procesor. Výsledkům kompilace se obvykle říká "object file", my jim budeme říkat objektové soubory. Objektové soubory nejsou spustitelné, protože jejich obsah sice je ve formátu kterému procesor rozumí, ale mohou obsahovat reference k funkcím (symbolům), které nejsou definovány v daném objektovém souboru. Kompilace bez linkování se provede předáním ''-c'' (nebo ''/c'') driveru kompilátoru. **Příklad** g++ -c hello.cpp clang++ -c hello.cpp cl /c hello.cpp V případě GCC a Clangu bude objektový souboru uložen jako ''hello.o'', v případe MSVC bude uložen jako ''hello.obj''. Během této fáze překladu dochází k najítí a hlášení většiny chyb v kódu. **V čem může být výhoda toho když se jednotlivé ''.cpp'' soubory nejdříve zkompilují do objektových, a pak se z nich teprve vytváří spustitelný program?** Do objektových souborů se dá různými způsoby podívat, my si ukážeme jak zjistit které symboly v daném souboru jsou, a které používá, ale v daném souboru nejsou. Na Linux/OS X k tomu slouží program s kryptickým jménem ''nm'', na Windows se za tímto účelem používá ''dumpbin''. **Použití** $ nm hello.o U __cxa_atexit 0000000000000000 t __cxx_global_var_init U __dso_handle 0000000000000050 t _GLOBAL__sub_I_test_io.cpp 0000000000000000 T main U _ZNSt8ios_base4InitC1Ev U _ZNSt8ios_base4InitD1Ev U _ZSt4cout 0000000000000000 b _ZStL8__ioinit U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc Stav každého symbolu se dá poznat jeho prefix, například ''U'' znamená, že daný symbol je potřeba, ale není v objektovém souboru definován. Seznam všech prefixů získáte pomocí ''man nm''. Výstup z ''dumpbin'' je mnohem ukecanější, takže ho rovnou necháme zapsat do souboru: ''dumpbin hello.obj /symbols /out:hello.txt''. Když se pak podíváme do souboru "hello.txt", najdeme tam například tyto řádky: 161 00000000 UNDEF notype () External | _atexit 165 00000000 UNDEF notype () External | _memcpy 166 00000000 UNDEF notype () External | _memmove 167 00000000 UNDEF notype () External | _strlen které nám říkají, že symboly ''_atexit'', ''_memcpy'', atd jsou žádané, ale nejsou definované. === Linkování === Linkování je krok, během kterého se spojí více objektových souborů, případně knihoven, do jedné spustitelné binárky. To znamená primárně nahrazení referencí k nedefinovaným symbolům správnou adresou daného symbolu, ale pokročilejší použití linkování umožňuje například z binárky vyhodit funkce, které se nikdy nemohou volat. Pokud by nějaký symbol zůstal nedefinovaný, jedné se o chybu a linker nahlásí "missing symbol" chybu. **Co by se mělo stát v případě, že nějaký symbol je naopak definovaný vícekrát?** //Příklad// g++ hello.o cl hello.obj ==== CMake ==== Protože pro netriviální programy je výhodné rozlišovat mezi kompilací a linkováním, ale je otravné se o to starat manuálně, používáme v praxi tzv. "build" systémy. Jejich účelem je starat se o kompilaci místo programátora, včetně kontroly které části programu je potřeba překompilovat po změnách, a které není. My z nich budeme pracovat s tzv "meta build" systémem, který se jmenuje CMake. Meta je pro to, že v něm popíšeme jak se nějaký program staví, a CMake následně vygeneruje soubory, které ten proces popisují pro build systém, který danou kompilaci doopravdy provede. Díky tomuto funguje CMake na většině platforem které člověk může potkat (nejenom Linux, OS X a Windows, ale i různé HPC systémy), a podporuje spoustu různých IDE. === Základní šablona === CMake vždy pracuje se souborem pojmenovaným ''CMakeLists.txt'' a my si ukážeme jak takový ''CMakeLists.txt'' má vypadat na jednoduchém příkladu, který si můžete stáhnout {{:courses:b6b36pjc:cviceni:cviceni-01_cmake-example.zip|zde}}. Mějme 5 souborů: * ''main.cpp'' * ''hello.h'' * ''hello.cpp'' * ''world.h'' * ''world.cpp'' Jejich odpovídající CMakeLists.txt pak je cmake_minimum_required(VERSION 3.5) project(cmake-example LANGUAGES CXX) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_executable(hello-world # implementacni soubory jsou dulezite hello.cpp world.cpp main.cpp # hlavickove soubory jsou pro IDE hello.h world.h ) # Zapneme rozumnou sadu varování if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU" ) target_compile_options( hello-world PRIVATE -Wall -Wextra -Wunreachable-code -Wpedantic) endif() if ( CMAKE_CXX_COMPILER_ID MATCHES "MSVC" ) target_compile_options( hello-world PRIVATE /W4 ) endif() První řádek říká, že musí být použit CMake ve verzi 3.5 nebo novější. Druhý řádek říká, že se tento projekt jmenuje ''cmake-example'' a jako jazyk používá pouze C%%++%%. Příkaz ''set'' nastavuje proměnou, v našem případě nastavujeme speciální proměnné na hodnoty které říkají, že chceme kompilovat se standardem C%%++%%14, nechceme používat rozšíření jednotlivých kompilátorů a že pokud nemůžeme kompilovat C%%++%%14 (třeba kvůli starému kompilátoru), tak nechceme používat starší standard. ''add_executable'' pak, jak název napovídá, definuje spustitelný program, který se bude jmenovat ''hello-world'' a skládá se z našich 5 souborů((ve skutečnosti by stačilo zde napsat pouze implementační soubory, ale když se zadají i hlavičky, tak lépe fungují generátory projektů pro IDE)). Poslední část naší ukázky pak zapne rozumnou sadu varování, pro specifický "target", v našem případě binárku ''hello-world''. === CMake CLI === Kompilace pomocí CMaku probíhá ve dvou fázích: - Konfigurace - Kompilace Konfigurace slouží k tomu, aby CMake vygeneroval soubory, dle kterých pak jiný build systém bude umět projekt postavit. Během kompilace pak dojde ke stavbě projektu samotného. My si teď ukážeme jak použít CMake CLI ke konfiguraci projektu. Základní invokace CMake je triviální: ''cmake -B -H''. Kde '''' je cesta ve které chcete aby probíhal build, a ''source-dir'' je cesta ze které se má přečíst ''CMakeLists.txt''. V případě, že chcete nastavit nějakou proměnnou uvnitř CMake, používá se na to ''-D'' prefix. Například při přípravě svého prostředí jste takto nastavili ''CMAKE_BUILD_TYPE'', což je proměnná která u některých generátorů nastavuje v jakém módu (Debug, Release, atd) má kompilace probíhat. Mezi další užitečné přepínátka pak patří ''-G'', které mění generátor použit během konfigurace, takže například ''cmake -Bbuild -H. -G "CodeBlocks - Unix Makefiles"'' vygeneruje projektové soubory pro CodeBlocks, které pak používají Make pro samotné stavění projektu, a ''-A'', které mění tzv. platformu pro konfiguraci, takže například ''cmake -Bbuild -H. -A Win64'' na Windows vytvoří projekt který staví 64 bitové aplikace místo 32 bitových. Ke stavbě projektu pak buďto můžeme použít cmake samotný, přes ''cmake %%--%%build '', jako se dělalo během přípravy prostředí, nebo přímo přes nástroj pro který jsme vybudovali projektové soubory. Například pro Make((Build systém, který se na Linuxu vybere pokud nespecifikujeme jinak)), to znamená ''make -C '', pro MSBuild((Build systém, který je používán Visual Studiem)) to znamená otevřít ''.sln'' soubor ve Visual Studiu. ------------------------------ ===== Ukázky ===== ==== Vstup a Výstup ==== Základní výstup (tisk do konzole) funguje v C%%++%% přes tzv. "streamy" (proudy). Konzolový výstup (stdout) je reprezentován objektem ''std::cout'', vstup (stdin) je pak reprezentován objektem ''std%%::%%cin''. === Hello World === Tato ukázka reprezentuje minimální program "Hello world". Po jeho vykonání bude na konzoli napsáno //Hello World//. #include int main(){ std::cout << "Hello world\n"; } ''#include '' umožní programu používat třídy pro standardní vstup. Operátor %%<<%% slouží k zápisu do proudu. === Jednoduchý vstup === Tento program ukazuje načítání vstupu. #include int main(){ std::cout << "Dej mi cislo\n"; int num; std::cin >> num; std::cout << "Cislo bylo: " << num << '\n'; } %%>>%% je operátor čtení z proudu. std::cin >> num; Program přečte ze standardního vstupu jedno číslo a uloží jeho hodnotu do num. Následně jeho hodnotu vypíše na výstup (spolu s trochou textu). Všimněte si, že se operátor výstupu dá řetězit. Stejným způsobem se dá řetězit i čtení: int num1, num2; std::cin >> num1 >> num2 Tyto dva řádky zadeklarují dvě čísla (proměnné typu int) a následně načtou dvě čísla ze stdin a uloží je do zadeklarovaných proměnných. ==== Kousek standardní knihovny ==== Nebudeme zatím řešit standardní knihovnu nijak do hloubky, ale krom standardního vstupu a výstupu se ještě podíváme na dva kousky standardní knihovny. Konkrétně na třídu pro řetězce, ''std::string'', která zjednodušuje práci s řetězci, a ''std::vector'', což je třída podobná javovské třídě ''ArrayList''. === std::string === ''std::string'' se dá načítat přes ''std::cin'' a vypisovat přes ''std::cout''. Pozor, načítání funguje po "slovech", tj. načtou se všechny znaky mezi dvěma prázdnými místy (mezery, tabulátory, konce řádků...). Co to znamená? Mějme tento kousek kódu: #include #include int main() { std::cout << "Napis sve jmeno:\n"; std::string jmeno; std::cin >> jmeno; std::cout << "Ahoj " << jmeno << '\n'; } Pokud mu jako vstup dáme ''Jan'', pak výstup bude Napis sve jmeno: Jan Ahoj Jan Pokud mu ale jako vstup dáme celé jméno, třeba ''Jan Novak'', pak výstup bude stále pouze samotné jméno. Napis sve jmeno: Jan Novak Ahoj Jan === std::vector === ''std::vector'' je rostoucí pole. Není potřeba předem určit velikost, stačí vytvořit vektor a přidávat/odebírat prvky dle libosti. Jedná se o šablonovou třídu, což znamená, že je potřeba deklarovat, jaký typ chceme do vektoru ukládat. Do vektoru můžeme ukládat primitivní i uživatelem definované typy. #include #include int main() { std::vector numbers; std::cout << "Napis 5 cisel.\n"; for (int i = 0; i < 5; ++i){ int temp; std::cin >> temp; numbers.push_back(temp); } std::cout << "Cisla byla:\n"; for (int i = 0; i < 5; ++i){ std::cout << numbers[i] << '\n'; } } === Iterace === Předchozí ukázka nejdříve načetla 5 čísel a pak vypsala prvních 5 prvků z vektoru. Co by se ale stalo, pokud bychom změnili načítací smyčku, aby načítala pouze 4 prvky? Není to jisté, protože to je tzv. nedefinované chování((UB bude vysvětleno na konci cvičení)), ale důležité je, že bychom smyčky, kde chceme přejít přes celý kontejner, měli psát trochu jinak. #include #include #include int main() { std::vector vec; std::cout << "Zadej 5 jmen.\n"; for (int i = 0; i < 5; ++i){ std::string temp; std::cin >> temp; vec.push_back(temp); } std::cout << "Jmena byla:\n"; for (std::string s : vec){ std::cout << s << '\n'; } } Někteří z vás určitě poznávají tzv. "for-each" smyčku, stejnou jako v Javě. Určitě si též umíte představit, že pokud by daný vector byl šablonován na složitém typu, nebylo by pohodlné jej použít. Naštěstí existuje i mnohem jednodušší forma: ... std::cout << "Jmena byla:\n"; for (auto s : vec) { std::cout << s << '\n'; } ''auto'' jako typ říká kompilátoru, ať zjistí typ "na pravé straně" výrazu a doplní ho. Více o použití ''auto'' v deklaracích proměnných si povíme později. ==== Koutek nedefinovaného chování ==== Pojem nedefinovaného chování (UB -- Undefined Behaviour) v C%%++%% vyjadřuje situaci, ke které dle standardu jazyka "nemůže dojít". Pozor, to neznamená, že jazyk brání tomu, aby k nim došlo, ale kompilátor při optimalizacích může nedefinované chování zanedbat a předpokládat, že si programátor "nějak" zajistil, aby k němu nedošlo. Co to znamená v praxi, si ukážeme na několika příkladech. === Příklad 1 === Zkuste si zkompilovat a spustit tento program, nejdříve bez optimalizací: #include int main(){ int i = 1; while (i > 0){ std::cout << "Ahoj\n"; i *= 2; } } Měl by vám vytisknout 31 řádků s "Ahoj". Nyní si ho zkuste zkompilovat s optimalizacemi (''-O3'' pro GCC, ''Release'' konfigurace ve Visual Studiu a XCode). Co vám vytiskne teď? === Příklad 2 === Zkuste si zkompilovat a spustit tento program. Nezapomeňte zapnout C%%++%%14. #include int test(int x) { if (x > 0) { return 1; } x -= 1'000'000'000; if (x > 0) { return 2; } return 1; } int main(){ std::cout << test(1) << std::endl; std::cout << test(-2'000'000'000) << std::endl; } Co je na výstupu, pokud spustíte program zkompilovaný s optimalizacemi? A co pokud spustíte program zkompilovaný bez nich? === Příklad 3 === Možná jste slyšeli o [[https://en.wikipedia.org/wiki/Fermat%27s_Last_Theorem|Velké Fermatově větě]]. Dlouho se nevědělo, jestli je opravdu pravdivá, nebo ne, ale v roce 1995 se ji po dlouhé době podařilo prokázat. Představte si, že tomu důkazu nevěřite a chcete si ji ověřit. Tento kousek kódu((Přejato z [[http://blog.regehr.org/archives/161|]])) ji vyzkouší pro ''n == 3'' a ''a, b, c %%<=%% 1000.'' Zkuste si ho zkompilovat s optimalizacemi. #include bool fermat() { const int MAX = 1000; int a = 1, b = 1, c = 1; while (true) { if ((a*a*a) == ((b*b*b) + (c*c*c))) { return true; } a++; if (a > MAX) { a = 1; b++; } if (b > MAX) { b = 1; c++; } if (c > MAX) { c = 1; } } return false; } int main() { if (fermat()) { std::cout << "Fermat's Last Theorem has been disproved.\n"; } else { std::cout << "Fermat's Last Theorem has not been disproved.\n"; } } Pokud vám program vypsal, že Velká Fermatova věta byla vyvrácena, nechte si vypsat hodnoty //a, b, c//, pro které je vyvrácena. Co vám program vypsal teď?