Pokročilé programování v C++ (část B) David Bednárek www.ksi.mff.cuni.cz
C++11
C++11 Jazyk Knihovny && (rvalue reference) Lambda auto Variadic templates Inicializátory Knihovny
Policy class, traits
Šablony tříd - definice Šablona je generická třída parametrizovaná libovolným počtem formálních parametrů těchto druhů: celé číslo – uvnitř šablony se chová jako konstanta, použitelná i jako meze polí ukazatel libovolného typu libovolný typ – deklarováno zápisem class T nebo typename T, identifikátor formálního parametru se chová jako identifikátor typu, použitelný uvnitř šablony v libovolné deklaraci Prefix definice šablony template< formální-parametry> lze použít před několika formami deklarací; oblastí platnosti formálních parametrů je celá prefixovaná deklarace
Šablony funkcí lze volat dvěma způsoby Šablona funkce je generická funkce (globální nebo metoda) prefixovaná konstrukcí template se stejnými druhy formálních parametrů šablony jako u šablon tříd template< typename T, int k> // parametry šablony int f( T * p, int q); // parametry funkce template< typename T, typename U> // parametry šablony int g( T * p, vector< U> q); // parametry funkce Šablony funkcí lze volat dvěma způsoby Explicitně f< int, 729>( a, b) Automaticky g( a, b) Překladač dopočte parametry šablony z typů parametrů funkce Všechny formální argumenty šablony by měly být užity v typech formálních parametrů funkce
Šablony s mnoha parametry vs. policy class template< typename T, bool binary, bool cached> class File { // ... T get() { if ( binary ) } }; File< char, false, true> my_file; template< typename P> class File { // ... typename P::T get() { if ( P::binary ) } }; struct my_policy { typedef char T; static const bool binary = false; static const bool cached = true; File< my_policy> my_file;
Policy class Policy class template< typename P> class File { // ... typename P::T get() { if ( P::binary ) } }; struct my_policy { typedef char T; static const bool binary = false; static const bool cached = true; File< my_policy> my_file; Policy class Třída (class/struct) použitá pouze jako parametr šablony Neexistuje objekt tohoto typu Obsahuje pouze typy (typedef/vnořené třídy) statické konstanty statické funkce Reference na typy z policy class jsou závislá jména a musejí mít prefix typename
Šablony tříd – závislé typy Šablony tříd (včetně těl metod) se při deklaraci kontrolují pouze částečně Překladač může kontrolovat jen to, co nezávisí na parametru Některé překladače nedělají ani to Překladač potřebuje odlišit jména typů od ostatních jmen U jmen ve tvaru A::B to překladač někdy nedokáže Programátor musí pomoci klíčovým slovem typename template< typename T> class X { typedef typename T::B U; // T::B je typ typename U::D p; // T::B::D je typ typename Y<T>::C q; // Y<T>::C je typ void f() { T::D(); } // T::D není typ } typename je nutné uvést před jmény typu ve tvaru A::B, kde A je závislé jméno Závislé jméno je jméno obsahující přímo či nepřímo parametr šablony
Pokud je předkem třídy závislé jméno Šablony tříd - this Pokud je předkem třídy závislé jméno překladač pak neví, které identifikátory jsou zděděny nedokáže realizovat přednost děděných před vnějšími uživatel musí pomoci konstrukcí this-> nebo kvalifikovaným jménem Pozor: Kvalifikované jméno vypíná virtuálnost funkce template< typename T> class X : public T { void f() { return m(); } // globální funkce m() void g() { return this->m(); } // metoda třídy T (nebo předka) void h() { return T::m(); } // metoda třídy T (nebo předka) }
Ekvivalence typů Při ekvivalenci typů rozhodují jména tříd/struktur template< typename P> class File { // ... typename P::T get() { if ( P::binary ) } }; struct my_policy { typedef char T; static const bool binary = false; static const bool cached = true; File< my_policy> my_file; struct my_policy2 { typedef char T; static const bool binary = false; static const bool cached = true; }; void f( File< my_policy2> & p); f( my_file); // error Při ekvivalenci typů rozhodují jména tříd/struktur obsah typových konstrukcí včetně typedef my_policy a my_policy2 jsou různé typy, tudíž se liší i File< my_policy> a File< my_policy2>
Využití neekvivalence typů template< typename P> class Value { typename P::T v; // ... }; struct mass { typedef double T; struct energy { Value< mass> m; Value< energy> e; e = m; // error Silná typová kontrola Odlišně pojmenované typy mají stejný obsah nejsou kompatibilní
Využití neekvivalence typů template< typename P> class Value { double v; // ... }; struct mass {}; struct energy {}; Value< mass> m; Value< energy> e; e = m; // error Silná typová kontrola Odlišně pojmenované typy mají stejný obsah nejsou kompatibilní Lze použít i typy bez obsahu
Využití ekvivalence typů template< int kg, int m, int s> class Value { double v; // ... }; template< int kg1, int m1, int s1, int kg2, int m2, int s2> Value< kg1+kg2, m1+m2, s1+s2> operator*( const Value< kg1, m1, s1> & a, const Value< kg2, m2, s2> & b); typedef Value< 1, 0, 0> Mass; typedef Value< 0, 1, -1> Velocity; typedef Value< 1, 2, -2> Energy; Mass m; Velocity c; Energy e; e = m * c * c; // OK Instance šablony Value se stejnými hodnotami číselných parametrů jsou ekvivalentní Konstrukce typedef je transparentní
Explicitní specializace template< int kg, int m, int s> struct Unit { static void print( ostream & os) { os << ”kg^” << kg << ”.m^” << m << ”.s^” << s; } }; template<> struct Unit< 1, 2, -2> { os << ”J”; Generickou definici šablony lze překrýt jinou definicí pro speciální případ Jiná šablona pak může mírně měnit své chování template< int kg, int m, int s> ostream & operator<<( ostream & os, const Value< kg, m, s> & x) { os << x << “ “; Unit< kg, m, s>::print( os); return os; }
Parciální specializace template< class X, class Y> class C { /* základní definice */ }; Parciální specializace Deklarovanou šablonu lze pro určité kombinace parametrů předefinovat jinak, než určuje její základní definice Parciální specializace může mít stejný, menší i větší počet formálních parametrů než základní definice, jejich hodnoty se odvozují ze skutečných parametrů šablony (kterých je vždy tolik, kolik určuje základní definice) template< class T, class U, int n> class C< T[n], U[n]> { /* specializace pro dvě pole stejné velikosti */ }; Explicitní specializace template<> class C< char, int[ 8]> { /* ... */ }; Explicitní specializace šablony není šablona Podléhá trochu jiným (jednodušším) pravidlům Překlad se neodkládá Těla metod se nemusí psát do hlavičkových souborů
Parciální specializace Typická použití parciální a explicitní specializace Výhodnější implementace ve speciálních případech Programátor - uživatel šablony o specializaci nemusí vědět Příklad: Implementace vector<char> může být jednodušší Mírná změna rozhraní ve speciálních případech Uživatel by měl být o specializaci informován Příklad: vector< bool> nedovoluje vytvořit ukazatel na jeden prvek Modifikace chování jiné šablony - traits Specializovaná šablona je volána z jiného šablonovaného kódu Autor volající šablony předpokládá, že ke specializaci dojde Někdy dokonce ani nedefinuje základní obsah volané šablony Autor specializace tak upravuje chování volající šablony Příklad: šablona basic_string<T> volá šablonu char_traits<T>, ve které je např. definována porovnávací funkce Generické programování Výpočty (s celými čísly a typovými konstrukcemi) při překladu
Parciální specializace - traits Modifikace chování jiné šablony - traits Specializovaná šablona je volána z jiného šablonovaného kódu Autor volající šablony předpokládá, že ke specializaci dojde Někdy dokonce ani nedefinuje základní obsah volané šablony Autor specializace tak upravuje chování volající šablony Příklad: šablona basic_string<T> volá šablonu char_traits<T>, ve které je např. definována porovnávací funkce template< class T> struct char_traits; template< class T> class basic_string { /* ... */ int compare( const basic_string< T> & b) const { /*...*/ char_traits< T>::compare( /* ... */) /*...*/ } }; template<> struct char_traits< char> { /* ... */ static int compare(const char* s1, const char* s2, size_t n) { return memcmp( s1, s2, n); }
Parciální specializace Modifikace chování jiné šablony Specializovaná šablona je volána z jiného šablonovaného kódu Autor volající šablony předpokládá, že ke specializaci dojde Někdy dokonce ani nedefinuje základní obsah volané šablony Autor specializace tak upravuje chování volající šablony Traits Šablony, ze kterých nejsou vytvářeny objekty Obsahují pouze: Definice typů Konstanty Statické funkce Určeny k doplnění informací o nějakém typu Příklad: char_traits<T> doplňuje informace o typu T, např. porovnávací funkci
Šablony, ze kterých nejsou vytvářeny objekty Obsahují pouze: Traits & policies Traits Šablony, ze kterých nejsou vytvářeny objekty Obsahují pouze: Definice typů Statické funkce Určeny k doplnění informací o nějakém typu Příklad: char_traits<T> doplňuje informace o typu T, např. porovnávací funkci Policy classes Třídy, ze kterých obvykle nejsou vytvářeny objekty Předávány jako parametr šablonám Defaultní hodnotou parametru často bývá šablona traits Určeny k definování určitého chování Příklad: Alokační strategie
Policy class vs. traits template< typename P, typename K> void for_each( K & data) { for( ... it = ... ) P::f( * it); } struct policy_add { static void f(...) ... }; my_k data; for_each< policy_add>( data); template< typename K> struct for_each_traits; void for_each( K & data) { for( ... it = ... ) for_each_traits< K>::f( * it); } template<> struct for_each_traits< my_k> { static void f(...) ... }; my_k data; for_each( data);
Polymorfismus Kompilační a běhový
Stejný zdrojový kód v různých situacích dělá různé věci Polymorfismus Polymorfismus Stejný zdrojový kód v různých situacích dělá různé věci Volání stejně pojmenované funkce (operátoru) volá různá těla Šetří práci programátora (zejména na údržbě) Kód není třeba kopírovat Nahrazuje konstrukce if/switch Kód je přehlednější a obsahuje méně chyb
Kompilační polymorfismus Šablony, zejména: Funktory apod. Policy classes, traits Běhový polymorfismus C: ukazatele na funkce C++: dědičnost a virtuální funkce Běhový polymorfismus zdržuje Používat pouze v nutném případě Datové struktury obsahující objekty s různým chováním ("OOP") Komunikace s neznámými partnery (komponentové systémy) Vše ostatní lze řešit kompilačním polymorfismem
Polymorfismus běhový a kompilační class functor { public: virtual void f( ...) = 0; }; void for_each( functor & x) { for( ... it = ... ) x.f( * it); // run-time binding } class functor_add : public functor { virtual void f(...) ... for_each( functor_add()); template< typename P> void for_each( P & x) { for( ... it = ... ) x.f( * it); // compile-time binding } struct functor_add { void f(...) ... }; for_each( functor_add());
Kompilační polymorfismus s objektem a bez template< typename P> void for_each( P & x) { for( ... it = ... ) x.f( it); // metoda } struct functor_add { void f(...) ... }; for_each( functor_add()); template< typename P> void for_each() { for( ... it = ... ) P::f( it); // statická funkce } struct policy_add { static void f(...) ... }; for_each< policy_add>();
Generické programování Výpočty při překladu
Teoretický pohled na šablony Překladač dokáže vyhodnotit celočíselnou aritmetiku I s rekurzivními funkcemi template< int N> struct Fib { static const int value = Fib< N-1>::value + Fib< N-2>::value; }; template<> struct Fib< 0> { static const int value = 1; template<> struct Fib< 1> { Kontrolní otázka: Jak dlouho trvá výpočet (tj. kompilace) Fib< 1000>::value
Teoretický pohled na šablony Překladač dokáže vyhodnotit celočíselnou aritmetiku I s rekurzivními funkcemi template< int N> struct Fib { static const int value = Fib< N-1>::value + Fib< N-2>::value; }; template<> struct Fib< 0> { static const int value = 1; template<> struct Fib< 1> { Kontrolní otázka: Jak dlouho trvá výpočet (tj. kompilace) Fib< 1000>::value MS Visual C++ 7.1: Build Time 0:00 Kompilátory ukládají již vytvořené instanciace a nepočítají je znovu
C++11: <type_traits> Triky s šablonami Podmíněný výraz nad typy template< bool C, typename A, typename B> struct conditional { typedef A type; }; template< typename A, typename B> struct conditional< false, A, B> { typedef B type; conditional< C, A, B>::type je typ Použití template< bool wide> class File { typename conditional< wide, wchar_t, char>::type get(); /* ... */ C++11: <type_traits>
C++11: <type_traits> Triky s šablonami Porovnání typů s booleovským výstupem template< class A, class B> struct is_same { static const bool value = false; }; template< class A> struct is_same< A, A> { static const bool value = true; is_same< X, Y>::value je konstantní výraz Použití template< class T1> class Test { static const bool very_long = is_same< long long, T1>::value; typedef conditional< is_long, unsigned long long, unsigned long> T; /* ... */ C++11: <type_traits>
C++11: vestavěno v jazyce Triky s šablonami Kompilační ověření invariantu template< bool x> struct static_assert { struct type {}; }; template<> struct static_assert< false> { template< int x> struct Assert { typename static_assert< (x > 0)>::type ignore_me(); static_assert( x > 0); C++11: vestavěno v jazyce
Variadic templates
Šablony s proměnlivým počtem parametrů Hlavička šablony s proměnlivým počtem typových argumentů template< typename ... TList> class C { /* ... */ }; pojmenovaný parametr zastupující seznam typů lze i kombinovat s pevnými parametry template< typename T1, int c2, typename ... TList> class D { /* ... */ }; platí i pro hlavičky parciálních specializací template< typename T1, typename ... TList> class C< T1, TList ...> { /* ... */ }; C++11
Šablony s proměnlivým počtem parametrů template< typename ... TList> pojmenovaný parametr - seznam typů lze uvnitř šablony použít v těchto konstrukcích: vždy se suffixem ... typové argumenty v použití (jiné) šablony X< TList ...> Y< int, TList ..., double> seznam předků třídy class E : public TList ... deklarace parametrů funkce/metody/konstruktoru void f( TList ... plist); double g( int a, TList ... b, double c); tím vzniká pojmenovaný parametr zastupující seznam hodnot několik dalších okrajových případů C++11
Šablony s proměnlivým počtem parametrů template< typename ... TList> void f( TList ... plist); pojmenovaný parametr - seznam hodnot lze uvnitř funkce použít v těchto konstrukcích: vždy se suffixem ... hodnotové argumenty ve volání (jiné) funkce/konstruktoru g( plist ...) new T( a, plist ..., 7) T v( b, plist ..., 8); inicializační sekce konstruktoru E( TList ... plist) : TList( plist) ... { } několik dalších případů C++11
Šablony s proměnlivým počtem parametrů template< typename ... TList> void f( TList ... plist); při použití je možno prvky seznamu obalit suffix ... slouží jako kompilační for_each (každý) výskyt názvu seznamu je nahrazen jeho i-tým prvkem výsledkem je seznam typů X< std::pair< int, TList *> ...> class E : public U< TList> ... void f( const TList & ... plist); seznam výrazů ve volání funkce/metody/konstruktoru g( make_pair( 1, & plist) ...); h( static_cast< TList *>( plist) ...); i( sizeof( TList) ...); seznam inicializátorů v konstruktoru další okrajové případy C++11
Generická N-tice použití template <class ... Types> class tuple { public: tuple( const Types & ...); /* ... */ }; template < size_t I, class T> class tuple_element { typedef /* ... */ type; template < size_t I, class ... Types> typename tuple_element< I, tuple< Types ...> >::type & get( tuple< Types ...> & t); použití tuple< int, double, int> t1( 1, 2.3, 4); double v = get< 1>( t1); C++11: <utility>
Rvalue reference Francisco de Goya. Sv. František Borgiáš u lože umírajícího (detail). 1788.
Doplnění rvalue referencí do standardních knihoven Rvalue reference Rvalue reference Nová typová konstrukce T && Lvalue reference Nový název pro starou typovou konstrukci T & Doplnění rvalue referencí do standardních knihoven Cíl: Zrychlit běh existujících zdrojových kódů Existující zdrojové kódy musí zůstat použitelné bez úprav Přidány nové funkce umožňující další zrychlení Rvalue reference mění styl programování v C++ Konstrukce dříve neefektivní se stávají použitelnými Použití nových knihovních funkcí Přímé použití rvalue referencí Move semantika - umožňuje nové druhy ukazatelů
Srozumitelný způsob použití Rvalue reference Rvalue reference Jasná motivace Odstranění nadbytečných kopírování Srozumitelný způsob použití Kanonické tvary tříd Nové knihovní funkce Složitá definice lvalue, xvalue, prvalue, glvalue, rvalue reference collapsing rules Nedozírné následky Princip navržen v r. 2002 Ještě v r. 2010 odstraňována nevhodná vylepšení z návrhu normy Byla to všechna?
Třídy obsahující velká data Motivace pro rvalue reference
Matrix - Řešení bez rvalue referencí class Matrix { public: Matrix(); Matrix( const Matrix & b); const Matrix & operator=( const Matrix & b); ~Matrix(); Matrix & operator+=( const Matrix & b) const; /* ... */ private: double * data_; std::size_t vsize_, hsize_; }; inline Matrix operator+( const Matrix & a, const Matrix & b) { Matrix t( a); t += b; return t; }
Poučení - konstruktory Ve třídě Matrix jsou odkazy na data uložená jinde Chování kompilátorem vytvořených metod nevyhovuje: Matrix(); Nedělá nic - je třeba alespoň vynulovat nebezpečné položky Matrix( const Matrix &); Kopíruje ukazatel - je třeba kopírovat data, na která ukazuje Matrix & operator=( const Matrix &); Totéž, navíc je nutné před přiřazením uklidit ~Matrix(); Nedělá nic - je třeba uklidit Tyto metody je tedy třeba napsat vlastní
Operátor přiřazení by měl udělat toto: Poučení - operator= Operátor přiřazení by měl udělat toto: Zrušit starý obsah levé strany Totéž co destruktor Okopírovat pravou stranu Totéž co konstruktor Vrátit novou hodnotu levé strany To lze vrátit odkazem Pozor - v některých případech to takto fungovat nebude: Matrix a; std::vector< Matrix> b; /* ... */ a = a; b[ i] = b[ j]; Při kopírovaní pravá strana už nebude existovat
Amatérské řešení operátoru přiřazení: Poučení - operator= Amatérské řešení operátoru přiřazení: Matrix & Matrix::operator=( const Matrix & b) { if ( this != & b ) { clean(); fill( b); } return * this; Konstruktory nelze přímo volat, u destruktoru to není rozumné Matrix::Matrix( const Matrix & b) Matrix::~Matrix() clean();
Lepší řešení operátoru přiřazení: Poučení - operator= Lepší řešení operátoru přiřazení: #include <algorithm> void Matrix::swap_with( Matrix & b) { swap( data_, b.data_); swap( xsize_, b.ysize_); swap( ysize_, b.ysize_); } Matrix & Matrix::operator=( const Matrix & b) Matrix tmp( b); swap_with( tmp); return * this; Toto řešení je navíc exception-safe (později...) Metodu swap je vhodné publikovat takto void swap( Matrix & a, Matrix & b) { a.swap_with( b); } Algoritmy (např. sort) používají swap a tento je rychlejší než generický
před C++11 Poučení - operator+ operator + vrací novou hodnotu v okamžiku návratu nikde jinde není je nutné vracet hodnotou Matrix operator+( const Matrix & a, const Matrix & b) vracení hodnotou znamená volání copy-constructoru data se kopírují, mnohdy zbytečně Matrix a, b, c, d; a = b + c + d; před C++11
před C++11 Poučení - operator+ Vracení hodnotou konstruuje pomocný objekt Matrix operator+( const Matrix & x, const Matrix & y); Matrix a, b, c, d; a = b + c + d; implementováno předáním ukazatele na volné místo void operator+( Matrix * rp, const Matrix * xp, const Matrix * yp); Matrix t1, t2; // bez volání konstruktoru operator+( & t1, & b, & c); operator+( & t2, & t1, & d); Matrix::operator=( & a, & t2); Matrix::~Matrix( & t2); Matrix::~Matrix( & t1); před C++11
před C++11 Poučení - operator+ implementováno předáním ukazatele Matrix operator+( const Matrix & x, const Matrix & y) { Matrix t( x); t += y; return t; } implementováno předáním ukazatele void operator+( Matrix * rp, const Matrix * xp, const Matrix * yp) Matrix t; // bez volání konstruktoru Matrix::Matrix( & t, xp); Matrix::operator+=( & t, yp); Matrix::Matrix( rp, & t); Matrix::~Matrix( & t); před C++11
před C++11 Poučení - operator+ implementováno takto: a = b + c + d; Matrix t1, t2, t3, t4, t5; // bez volání konstruktoru // operator+( & t1, & b, & c); Matrix::Matrix( & t3, & b); // kopie #1 Matrix::operator+=( & t3, & c); Matrix::Matrix( & t1, & t3); // kopie #2 Matrix::~Matrix( & t3); // operator+( & t2, & t1, & d); Matrix::Matrix( & t4, & t1); // kopie #3 Matrix::operator+=( & t4, & d); Matrix::Matrix( & t2, & t4); // kopie #4 Matrix::~Matrix( & t4); // Matrix::operator=( & a, & t2); Matrix::Matrix( & t5, & t2); // kopie #5 Matrix::swap( & a, & t5); Matrix::~Matrix( & t5); Matrix::~Matrix( & t2); Matrix::~Matrix( & t1); před C++11
před C++11 Poučení - operator+ šikovný překladač eliminuje proměnnou t Matrix operator+( const Matrix & x, const Matrix & y) { Matrix t( x); t += y; return t; } šikovný překladač eliminuje proměnnou t pozorovatelná změna chování programu, povoleno normou void operator+( Matrix * rp, const Matrix * xp, const Matrix * yp) Matrix::Matrix( rp, xp); Matrix::operator+=( & t, yp); před C++11
před C++11 Poučení - operator+ po eliminaci t3, t4: a = b + c + d; Matrix t1, t2, t5; // bez volání konstruktoru // operator+( & t1, & b, & c); Matrix::Matrix( & t1, & b); // kopie #1 Matrix::operator+=( & t1, & c); // operator+( & t2, & t1, & d); Matrix::Matrix( & t2, & t1); // kopie #2 Matrix::operator+=( & t2, & d); // Matrix::operator=( & a, & t2); Matrix::Matrix( & t5, & t2); // kopie #3 Matrix::swap( & a, & t5); Matrix::~Matrix( & t5); Matrix::~Matrix( & t2); Matrix::~Matrix( & t1); před C++11
před C++11 Poučení - operator+ Matrix & Matrix::operator=( Matrix b) { swap_with( b); return * this; } chytrý programátor a chytrý překladač dokážou eliminovat t5: Matrix t1, t2; // bez volání konstruktoru // operator+( & t1, & b, & c); Matrix::Matrix( & t1, & b); // kopie #1 Matrix::operator+=( & t1, & c); // operator+( & t2, & t1, & d); Matrix::Matrix( & t2, & t1); // kopie #2 Matrix::operator+=( & t2, & d); // Matrix::operator=( & a, & t2); Matrix::swap( & a, & t2); Matrix::~Matrix( & t2); Matrix::~Matrix( & t1); kopie t1 -> t2 je stále zbytečná před C++11
před C++11 Poučení - operator+ Matrix operator+( Matrix x, const Matrix & y) { x += y; return x; } nepomůže, potřebuje t3 pro x z prvního volání Matrix t1, t2, t3; // bez volání konstruktoru Matrix::Matrix( & t3, & b); // kopie #1 // operator+( & t1, & t3, & c); Matrix::operator+=( & t3, & c); Matrix::Matrix( & t1, & t3); // kopie #2 // operator+( & t2, & t1, & d); Matrix::operator+=( & t1, & d); Matrix::Matrix( & t2, & t1); // kopie #3 // Matrix::operator=( & a, & t2); Matrix::swap( & a, & t2); Matrix::~Matrix( & t2); Matrix::~Matrix( & t3); Matrix::~Matrix( & t1); před C++11
Řešení pomocí rvalue referencí Zbytečná kopírování Řešení pomocí rvalue referencí
C++11 Řešení – operator= Princip řešení - operator= Dvě různé situace a = b; zde se hodnota b okopírovat musí a = c + d; výsledek operátoru + se stane novou hodnotou a kopie principiálně není zapotřebí C++11
C++11 Řešení – operator= Princip řešení - operator= Dvě různé situace Potřebujeme dvě implementace operatoru = a = b; zde se hodnota b okopírovat musí T & T::operator=( const T & x); copy-assignment objekt b bude pouze čten a = c + d; výsledek operátoru + se stane novou hodnotou a T & T::operator=( T && x); move-assignment hodnota vrácená operátorem + bude přesunuta do a zdrojový pomocný objekt bude přesunem modifikován C++11
C++11 Řešení – inicializace Totéž platí pro inicializaci Dvě různé situace Potřebujeme dvě implementace konstruktoru T a = b; zde se hodnota b okopírovat musí T::T( const T & x); copy-constructor objekt b bude pouze čten T a = c + d; výsledek operátoru + se stane hodnotou a T::T( T && x); move-constructor hodnota vrácená operátorem + bude přesunuta do a zdrojový pomocný objekt bude přesunem modifikován C++11
Speciální metody tříd – C++11 copy/move Speciální metody tříd – C++11
C++11 copy/move Speciální metody tříd Copy constructor T( const T & x); Move constructor T( T && x); Copy assignment operator T & operator=( const T & x); Move assignment operator T & operator=( T && x); C++11
C++11 copy/move Překladačem definované chování (default) Copy constructor T( const T & x) = default; aplikuje copy constructor na složky Move constructor T( T && x) = default; aplikuje move constructor na složky Copy assignment operator T & operator=( const T & x) = default; aplikuje copy assignment operator na složky Move assignment operator T & operator=( T && x) = default; aplikuje move assignment operator na složky default umožňuje vynutit defaultní chování C++11
C++11 copy/move Podmínky automatického defaultu Copy constructor/assignment operator pokud není explicitně deklarován move constructor ani assignment operator budoucí normy pravděpodobně zakážou automatický default i v případě přítomnosti druhé copy metody nebo destruktoru Move constructor/assignment operator pokud není deklarována žádná ze 4 copy/move metod ani destruktor C++11
C++11 copy/move Nejběžnější kombinace Neškodná třída Nedeklaruje žádnou copy/move metodu ani destruktor Neobsahuje složky vyžadující zvláštní péči (ukazatele) Složky vyžadující zvláštní péči Překladačem generované chování (default) nevyhovuje Bez podpory move (před C++11) T( const T & x); T & operator=( const T & x); ~T(); Plná podpora copy/move T( T && x); T & operator=( T && x); C++11
C++11 copy/move Další kombinace Nekopírovatelná třída Např. dynamicky alokované živé objekty v simulacích T( const T & x) = delete; T & operator=( const T & x) = delete; delete zakazuje generování překladačem Destruktor může ale nemusí být nutný Přesouvatelná nekopírovatelná třída Např. unikátní vlastník jiného objektu (viz std::unique_ptr< U>) T( T && x); T & operator=( T && x); ~T(); Pravidla jazyka zakazují generování copy metod překladačem Destruktor typicky bývá nutný C++11
Třídy obsahující velká data Řešení podle C++11
Matrix - Řešení s podporou move class Matrix { public: Matrix(); Matrix( const Matrix & x); Matrix( Matrix && x); Matrix & operator=( const Matrix & x); Matrix & operator=( Matrix && x); ~Matrix(); /* ... */ private: double * data_; std::size_t vsize_, hsize_; }; Default nevyhovuje Copy metody musejí alokovat nová data_ Destruktor musí dealokovat data_ Move metody musejí vynulovat položku data_ ve zdroji rvalue reference neznamená, že zdroj nebude destruován Assignment operátory musejí nějak uklidit starý obsah C++11
Matrix – copy metody a destruktor Matrix::Matrix( const Matrix & x) : data_( new double[ x.vsize_ * x.hsize_]), vsize_( x.vsize_), hsize_( x.hsize_) { std::copy( x.data_, x.data_ + vsize_ * hsize_, data_); } Matrix & Matrix::operator=( const Matrix & x) Matrix t( x); t.swap_with( * this); return * this; Trik s eliminací proměnné t v operátoru = nelze použít Hodnotou předávaný parametr by zastínil move assignment Matrix::~Matrix() delete data_; Operátor delete je odolný proti nulovému ukazateli C++11
C++11 Matrix – move metody Matrix::Matrix( Matrix && x) : data_( x.data_), vsize_( x.vsize_), hsize_( x.hsize_) { x.data_ = nullptr; } x.data_ je nutné vynulovat – jinak je destruktor x dealokuje nullptr je nový preferovaný způsob zápisu nulového ukazatele Matrix & Matrix::operator=( Matrix && x) x.swap_with( * this); return * this; Stará hodnota * this se dostane do x Destruktor x ji časem zneškodní C++11
Matrix – move metody - varianta Matrix::Matrix( Matrix && x) : data_( x.data_), vsize_( x.vsize_), hsize_( x.hsize_) { x.data_ = nullptr; } x.data_ je nutné vynulovat – jinak je destruktor x dealokuje nullptr je nový preferovaný způsob zápisu nulového ukazatele Matrix & Matrix::operator=( Matrix && x) Matrix t( std::move( x)); t.swap_with( * this); return * this; std::move vynutí move constructor pro inicializaci t Stará hodnota * this se dostane do t Destruktor t ji včas zneškodní C++11
lvalue/rvalue Pravidla
C++11 lvalue/rvalue U výrazu překladač zkoumá Lvalue typu T Typ (po odstranění vnějších referencí) Kategorie - lvalue/rvalue T x; T * p; T & lr; T && rr; T f(); T & lrf(); T && rrf(); Lvalue typu T K objektu je možné přistupovat opakovaně (má jméno) Pozor: pojmenovaná rvalue reference je lvalue! x, * p, lr, rr, lrf() Rvalue typu T Tento výraz je jediná možnost přístupu k objektu Následovat bude už jen volání destruktoru f(), rrf(), std::move( x), std::move( rr) Číselné a znakové konstanty jsou rvalue Toto je zjednodušená definice Norma definuje lvalue, xvalue, prvalue, glvalue a rvalue C++11
C++11 lvalue/rvalue Pravidla předávání parametrů Kategorie a typ výrazu lvalue rvalue T const T Typ parametru T, const T OK T & OK (+) --- const T & OK (-) T && const T && OK (+) má přednost před OK (-) Preferované případy, neobvyklé případy Platí i pro inicializaci proměnných a příkaz return Vše ostatní je jen volání funkcí a operátorů Zjednodušeno Skutečná pravidla jsou komplikována existencí dalších konverzí (předek-potomek, číselné konverze, uživatelské konverze, pole, funkce, ...)
Třídy obsahující velká data Operátory podle C++11
Matrix - Řešení s podporou move class Matrix { public: Matrix(); Matrix( const Matrix & x); Matrix( Matrix && x); Matrix & operator=( const Matrix & x); Matrix & operator=( Matrix && x); ~Matrix(); Matrix & operator+=( const Matrix & b) const; /* ... */ private: double * data_; std::size_t vsize_, hsize_; }; Matrix operator+( const Matrix & a, const Matrix & b); Matrix && operator+( Matrix && a, const Matrix & b); Matrix && operator+( const Matrix & a, Matrix && b); Matrix && operator+( Matrix && a, Matrix && b); C++11
Matrix - Řešení s podporou move Matrix operator+( const Matrix & a, const Matrix & b) { return Matrix( a) += b; } Matrix && operator+( Matrix && a, const Matrix & b) return std::move( a += b); Matrix && operator+( const Matrix & a, Matrix && b) return std::move( b += a); Matrix && operator+( Matrix && a, Matrix && b) C++11
lvalue/rvalue Nové rozhraní STL
C++11 lvalue/rvalue - STL Nová implementace swap move void swap( T & a, T & b) { T tmp( std::move( a)); a = std::move( b); b = std::move( tmp); } Není nutné specializovat swap pro uživatelské typy Postačí implementace move-metod move std::move( b); Zkratka za: static_cast< T &&>( b); C++11
C++11 lvalue/rvalue - STL Copy/move insertion Emplace iterator insert( const_iterator p, const T & x); iterator insert( const_iterator p, T && x); Druhá verze umožňuje efektivní přesun hodnoty Týká se i push_back/push_front Emplace template< typename ... TList> iterator emplace( const_iterator p, TList && ... plist); Zkonstruuje objekt přímo na místě plist se předá jako parametry konstruktoru Existuje i emplace_back/emplace_front Další optimalizace uvnitř implementace kontejnerů Realokace vektoru používá move namísto copy C++11
lvalue/rvalue Perfect forwarding
C++11 Perfect forwarding new( q) je starý trik: placement new template< typename ... TList> iterator emplace( const_iterator p, TList && ... plist) { void * q = /* místo pro nový prvek */; pointer r = new( q) value_type( plist ...); /* ... */ } new( q) je starý trik: placement new využívá možnost napsat si vlastní alokátor s parametrem navíc void * operator new( std::size, void * q) { return q; } C++11
C++11 Perfect forwarding Jak dokáže emplace předat parametry? template< typename ... TList> iterator emplace( const_iterator p, TList && ... plist) { void * q = /* místo pro nový prvek */; pointer r = new( q) value_type( plist ...); /* ... */ } Jak dokáže emplace předat parametry? Co když je skutečným parametrem lvalue? Proč neexistuje verze s const TList & Bylo by jich exponenciálně mnoho! C++11
C++11 Perfect forwarding Trik: Skládání referencí Jazyk definuje tato pravidla Použijí se u šablon funkcí s parametrem typu T && template< typename T> void f( T && p); X lv; f( lv); Parametr typu T && lze navázat na lvalue typu X Dosadí se T = X & f( std::move( lv)); Je-li skutečným parametrem rvalue typu X Dosadí se T = X C++11 X & & X & X && & X & && X && && X &&
C++11 Perfect forwarding Forwarding template< typename T> void f( T && p) { g( p); } X lv; f( lv); Parametr typu T && lze navázat na lvalue typu X Dosadí se T = X & f( std::move( lv)); Je-li skutečným parametrem rvalue typu X Dosadí se T = X Parametr p je však lvalue Do funkce g by měl být předán pomocí std::move C++11
C++11 Perfect forwarding Perfect forwarding template< typename T> void f( T && p) { g( std::forward< T>( p)); } std::forward< T> vrací T && X lv; f( lv); T = X & skládání referencí zajistí, že std::forward< T> vrací X & f( std::move( lv)); T = X std::forward< T> vrací X && v tomto případě se std::forward< T> chová jako std::move C++11
C++11 Perfect forwarding Správná implementace emplace template< typename ... TList> iterator emplace( const_iterator p, TList && ... plist) { void * q = /* místo pro nový prvek */; pointer r = new( q) value_type( std::forward< TList>( plist) ...); /* ... */ } C++11
Smart pointers
C++11 Smart pointers Chytré ukazatele Výlučné vlastnictví Automatické uvolnění dynamicky alokovaného objektu Jinak se chovají jako T * Operátory *, -> Porovnání ukazatelů, nullptr Výlučné vlastnictví Vyžaduje move-semantiku std::unique_ptr< T> Varianta pro pole: Operátor [] namísto * a -> std::unique_ptr< T[]> Sdílené vlastnictví Založeno na počítání odkazů std::shared_ptr< T> Vedlejší odkaz, který se nepočítá (počítání zdržuje) std::weak_ptr< T> Lze povýšit na shared_ptr, jinak je ekvivalentní s T * C++11
C++11 Smart pointers Výlučné vlastnictví Sdílené vlastnictví { std::unique_ptr< T> q; // obsahuje nullptr std::unique_ptr< T> p = new T( /*...*/); q = std::move( p); } q->f(); h( * q); } // zde zaniká q i objekt T Sdílené vlastnictví std::shared_ptr< T> q; // obsahuje nullptr std::shared_ptr< T> p = std::make_shared< T>( /*...*/); q = p; // zde se zvětšuje čítač p->f(); } // zde se zmenšuje čítač std::weak_ptr< T> r = q; h( * r); C++11
Lambda
C++11 Lambda výrazy Motivace Řešení class ftor { public: ftor(int a, int b) : a_(a),b_(b) { } bool operator()(int x) const { return x*a_<b_; } private: int a_, b_; }; typedef std::vector<int> v_t; v_t v; v_t::iterator vi=remove_if(v.begin(), v.end(), ftor(m, n)); Řešení std::vector<int> v; auto vi=remove_if(v.begin(), v.end(), [=](int x){ return x*m<n; }); C++11
C++11 Lambda výrazy Lambda výraz Deklaruje třídu ve tvaru [ capture ]( params ) mutable -> rettype { body } Deklaruje třídu ve tvaru class ftor { public: ftor( TList ... plist) : vlist( plist) ... { } rettype operator()( params ) const { body } private: TList ... vlist; }; vlist je určen proměnnými použitými v body TList je určen jejich typy a upraven podle capture operator() je const pokud není uvedeno mutable Lambda výraz je nahrazen vytvořením objektu ftor( vlist ...) C++11
Lambda výrazy – návratový typ a typ funkce Návratový typ operátoru Explicitně definovaný návratový typ []() -> int { … } Automaticky určen pro tělo lambda funkce ve tvaru []() { return V; } Jinak void C++11
Lambda výrazy – capture [ capture ]( params ) mutable -> rettype { body } Způsob zpřístupnění vnějších entit Určuje typy datových položek a konstruktoru funktoru Explicitní capture Programátor vyjmenuje všechny vnější entity v capture [a,&b,c,&d] entity označené & předány odkazem, ostatní hodnotou Implicitní capture Překladač sám určí vnější entity, capture určuje způsob předání [=] [=,&b,&d] předání hodnotou, vyjmenované výjimky odkazem [&] [&,a,c] předání odkazem, vyjmenované výjimky hodnotou C++11
Lambda výrazy – příklad int a = 1, b = 1, c = 1; auto m1 = [a, &b, &c]() mutable { auto m2 = [a, b, &c]() mutable { std::cout << a << b << c; a = 4; b = 4; c = 4; }; a = 3; b = 3; c = 3; m2(); a = 2; b = 2; c = 2; m1(); Co to vypíše? C++11 123234
Pokročilý pohled na kontejnery
Kontejnery a iterátory Standardní knihovna definuje Mnoho druhů kontejnerů basic_string, vector, deque, list array, forward_list map, multimap, set, multiset unordered_map, unordered_multimap, unordered_set, unordered_multiset Odlišné přidávání/odebírání Shodný způsob procházení Každý kontejner definuje Typy iterator, const_iterator Metody begin(), end() Iterátory jsou inspirovány ukazatelovou aritmetikou Ukazatele do pole se chovají jako iterátory Algoritmy mohou pracovat s iterátory libovolného kontejneru s ukazateli do pole s čímkoliv, co má potřebné operátory void clr( int & x) { x = 0; } std::vector< int> v; for_each( v.begin(), v.end(), clr); std::array< int, N> a; for_each( a.begin(), a.end(), clr); int p[ N]; for_each( p, p + N, clr); C++11 C++11
Kategorie iterátorů Standardní knihovna definuje Algoritmy mohou pracovat s čímkoliv, co má potřebné operátory Různé algoritmy mají různé nároky Norma C++ definuje 5 kategorií iterátorů random_access bidirectional forward output input Kategorie určuje, které syntaktické konstrukce musí iterátor umožňovat všechny kategorie iterator(I) /* copy constructor */ ++I, I++ output *I = x, *I++ = x random_access, bidirectional, forward, input I1 = I2 I1 == I2, I1 != I2 I->m /* pokud existuje (*I).m */ input *I /* pouze pro čtení */ random_access, bidirectional, forward iterator() *I /* čtení i zápis */ random_access, bidirectional --I, I-- random_access I += n, I + n, n + I I -= n, I - n, I1 - I2 I[ n] I1 < I2, I1 > I2, I1 <= I2, I1 >= I2
Použití iterátorů Pravidla použití iterátorů Procházený rozsah bývá zadán intervalem [b,e) Dereferencovat e se nesmí Přebíhat za e/před b se nesmí template< iterator> void f( iterator b, iterator e) { ... = * b; // chyba ! for ( iterator i = e - 1; // chyba ! i >= b; -- i) // chyba ! ... }
Standardem definované iterátory Iterátory na kontejnerech random_access category iterátory k vector a deque forward category iterátory k forward_list bidirectional category iterátory ostatních kontejnerů reverse_iterator šablona pro otočení smyslu bidirectional/random_access iterátoru kontejnery mají rbegin()/rend() pozor: k.rbegin() != reverse_iterator( k.end()) Falešné iterátory output category back_inserter, front_inserter, inserter std::vector a; std::copy( x.begin(), x.end(), std::back_inserter( a)); ostream_iterator std::ostream & o = ...; std::ostream_iterator( o)); input category istream_iterator std::istream & i = ...; std::copy( std::istream_iterator( i), std::istream_iterator(),
iterator_traits Složitější algoritmy Potřebují znát typ, na který iterátor ukazuje, apod. Potřebují určit kategorii iterátoru Standard definuje šablonu iterator_traits parametrizovanou typem iterátoru Každý iterator ji musí definovat Obvykle nepřímo definováním těchto typů uvnitř třídy iterátoru Pokud to nejde, explicitní specializací šablony template<class Iterator> struct iterator_traits { typedef typename Iterator::difference_type difference_type; typedef typename Iterator::value_type value_type; typedef typename Iterator::pointer pointer; typedef typename Iterator::reference reference; typedef typename Iterator::iterator_category iterator_category; }; template<class T> struct iterator_traits<T*> { typedef ptrdiff_t difference_type; typedef T value_type; typedef T* pointer; typedef T& reference; typedef random_access_iterator_tag iterator_category;
Exception handling Mechanismus výjimek
Exception-safe programming Používat throw a catch je jednoduché Těžší je programovat běžný kód tak, aby se choval korektně i za přítomnosti výjimek Exception-safety Exception-safe programming void f() { int * a = new int[ 100]; int * b = new int[ 200]; g( a, b); delete[] b; delete[] a; } Pokud new int[ 200] způsobí výjimku, procedura zanechá naalokovaný nedostupný blok Pokud výjimku vyvolá procedura g, zůstanou dva nedostupné bloky
Exception-safe programming Používat throw a catch je jednoduché Těžší je programovat běžný kód tak, aby se choval korektně i za přítomnosti výjimek Exception-safety Exception-safe programming T & operator=( const T & b) { if ( this != & b ) delete body_; body_ = new TBody( b.length()); copy( body_, b.body_); } return * this; Pokud new TBody způsobí výjimku, operátor= zanechá v položce body_ původní ukazatel, který již míří na dealokovaný blok Pokud výjimku vyvolá procedura copy, operátor zanechá třídu v neúplném stavu
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Výjimka může být vyvolána uvnitř, ale musí být zachycena nejpozději uvnitř destruktoru Zdůvodnění: V rámci ošetření výjimek (ve fázi stack-unwinding) se volají destruktory lokálních proměnných Výjimku zde vyvolanou nelze z technických i logických důvodů ošetřit (ztratila by se původní výjimka) Nastane-li taková výjimka, volá se funkce terminate() a program končí
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Výjimka může být vyvolána uvnitř, ale musí být zachycena nejpozději uvnitř destruktoru Toto pravidlo jazyka sice platí pouze pro destruktory lokálních proměnných A z jiných důvodů též pro globální proměnné Je však vhodné je dodržovat vždy Bezpečnostní zdůvodnění: Destruktory lokálních proměnných často volají jiné destruktory Logické zdůvodnění: Nesmrtelné objekty nechceme
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Konstruktor globálního objektu nesmí skončit vyvoláním výjimky Zdůvodnění: Není místo, kde ji zachytit Stane-li se to, volá se terminate() a program končí Jiné konstruktory ale výjimky volat mohou (a bývá to vhodné)
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Konstruktor globálního objektu nesmí skončit vyvoláním výjimky Copy-constructor typu v hlavičce catch-bloku nesmí skončit vyvoláním výjimky Zdůvodnění: Catch blok by nebylo možné vyvolat Stane-li se to, volá se terminate() a program končí Jiné copy-constructory ale výjimky volat mohou (a bývá to vhodné)
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Konstruktor globálního objektu nesmí skončit vyvoláním výjimky Copy-constructor typu v hlavičce catch-bloku nesmí skončit vyvoláním výjimky
Exception-safe programming Poznámka: Výjimky při zpracování výjimky Výjimka při výpočtu výrazu v throw příkaze Tento throw příkaz nebude vyvolán Výjimka v destruktoru při stack-unwinding Povolena, pokud neopustí destruktor Po zachycení a normálním ukončení destruktoru se pokračuje v původní výjimce Výjimka uvnitř catch-bloku Pokud je zachycena uvnitř, ošetření původní výjimky může dále pokračovat (přikazem throw bez výrazu) Pokud není zachycena, namísto původní výjimky se pokračuje ošetřováním nové
Exception-safe programming Kompilátory samy ošetřují některé výjimky Dynamická alokace polí Dojde-li k výjimce v konstruktoru některého prvku, úspěšně zkonstruované prvky budou destruovány Ve zpracování výjimky se poté pokračuje
Exception-safe programming Kompilátory samy ošetřují některé výjimky Dynamická alokace polí Dojde-li k výjimce v konstruktoru některého prvku, úspěšně zkonstruované prvky budou destruovány Ve zpracování výjimky se poté pokračuje Výjimka v konstruktoru součásti (prvku nebo předka) třídy Sousední, již zkonstruované součásti, budou destruovány Uvnitř konstruktoru je možno výjimku zachytit speciálním try-blokem: X::X( /* formální parametry */) try : Y( /* parametry pro konstruktor součásti Y */) { /* vlastní tělo konstruktoru */ } catch ( /* parametr catch-bloku */ ) { /* ošetření výjimky v konstruktoru Y i ve vlastním těle */ } Konstrukci objektu nelze dokončit Opuštění speciálního catch bloku znamená throw;
Exception-safe programming Bezpečné programování s výjimkami
Exception-safe programming Definice (Weak) exception safety Funkce (operátor, konstruktor) je (slabě) bezpečná, pokud i v případě výjimky zanechá veškerá data v konzistentním stavu Konzistentní stav znamená zejména: Nedostupná data byla korektně destruována a odalokována Ukazatele nemíří na odalokovaná data Platí další invarianty dané logikou aplikace
Exception-safe programming Definice (Weak) exception safety Funkce (operátor, konstruktor) je (slabě) bezpečná, pokud i v případě výjimky zanechá veškerá data v konzistentním stavu Konzistentní stav znamená zejména: Nedostupná data byla korektně destruována a odalokována Ukazatele nemíří na odalokovaná data Platí další invarianty dané logikou aplikace Strong exception safety Funkce je silně bezpečná, pokud v případě, že skončí vyvoláním výjimky, zanechá data ve stejném (pozorovatelném) stavu, ve kterém byla při jejím vyvolání Observable state - chování veřejných metod Nazýváno též "Commit-or-rollback semantics"
Exception-safe programming Poznámky (Weak) exception safety Tohoto stupně bezpečnosti lze většinou dosáhnout Stačí vhodně definovat nějaký konzistentní stav, kterého lze vždy dosáhnout, a ošetřit pomocí něj všechny výjimky Konzistentním stavem může být třeba nulovost všech položek Je nutné upravit všechny funkce tak, aby je tento konzistentní stav nepřekvapil (mohou na něj ale reagovat výjimkou) Strong exception safety Silné bezpečnosti nemusí jít vůbec dosáhnout, pokud je rozhraní funkce navrženo špatně Obvykle jsou problémy s funkcemi s dvojím efektem Příklad: funkce pop vracející odebranou hodnotu
Exception-safe programming Konstruktory a operator=
Exception-safe programming copy-constructor Silně bezpečné řešení Pokud tělo dorazí na konec, budou datové položky korektně vyplněny Tělo může vyvolávat výjimky V takovém případě není třeba datové položky vyplňovat Objekt nebude považován za platný a nebude používán ani destruován Obecně je však třeba ošetřit try-blokem situace, kdy je v objektu více dynamicky alokovaných ukazatelů Vyplatí se uzavírat ukazatele do tříd po jednom Chytré ukazatele class String { /*...*/ char * str_; }; String( const String & b) { if ( b.str_ ) str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } else throw InvalidString();
Exception-safe programming copy-constructor Silně bezpečné řešení Chytré ukazatele std::unique_ptr< T> #include <memory> class String { /*...*/ std::unique_ptr< char[]> str_; }; String( const String & b) { if ( b.str_ ) str_.reset( new char[ strlen( b.str_) + 1]); strcpy( str_.get(), b.str_.get()); } else throw InvalidString(); C++11
Exception-safe programming operator= Silně bezpečné řešení Pokud je copy-constructor silně bezpečný Copy-constructor naplní lokální proměnnou c kopií parametru b Zde může dojít k výjimce Metoda swap_with vyměňuje obsah this a proměnné c Knihovní funkce swap je rychlá a (na ukazatelích) nevyvolává výjimky Před návratem z operatoru se volá destruktor c Tím zaniká původní obsah this #include <algorithm> void String::swap_with( String & x) { swap( str_, x.str_); } String & String::operator=( const String & b) String c( b); swap_with( c); return * this;
Exception-safe programming operator= Silně bezpečné řešení Metodu swap_with je vhodné publikovat ve formě globální funkce se standardním jménem swap Některé algoritmy nad kontejnery obsahujícími String se tak zrychlí a stanou se bezpečnými vůči výjimkám #include <algorithm> void String::swap_with( String & x) { swap( str_, x.str_); } String & String::operator=( const String & b) String c( b); swap_with( c); return * this; void swap( String & x, String & y) x.swap_with( y); před C++11
Exception-safe programming Move metody Obvykle negenerují výjimky Některé algoritmy nad kontejnery obsahujícími String se tak zrychlí a stanou se bezpečnými vůči výjimkám Vlastní implementace globální funkce swap není zapotřebí Knihovní implementace swap volá move metody #include <algorithm> String::String( String && b) : str_( std::move( b.str_)) { } String & String::operator=( str_ = std::move( b.str_); return * this; C++11
Exception-safe programming Funkce s vedlejšími efekty
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Slabě bezpečná implementace: Při výjimce v konstruktoru proměnné s se nestane nic operator delete nezpůsobuje výjimky struct Box { String v; Box * next; }; class StringStack { public: // ... private: Box * top_; }; String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); Box * p = top_; String s = p->v; top_ = p->next; delete p; return s; }
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Slabě bezpečná implementace Není silně bezpečná: Funkce vrací hodnotou Pokud při vracení dojde k výjimce v copy-constructoru, zásobník již bude zkrácen struct Box { String v; Box * next; }; class StringStack { public: // ... private: Box * top_; }; String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); Box * p = top_; String s = p->v; top_ = p->next; delete p; return s; }
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Silně bezpečná implementace Jak zrušíme proměnnou p, když k výjimce nedojde? std::unique_ptr< T> #include <memory> String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); std::unique_ptr< Box> p = top_; top_ = p->next; try { return p->v; } catch ( ...) top_ = std::move( p); // toto přiřazení nuluje p throw; // při návratu se automaticky zruší p // pokud je p nenulové
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Silně bezpečná implementace Uživatel ji nedokáže použít tak, aby to bylo silně bezpečné Vracenou hodnotu je nutné okopírovat Nedá se poznat, zda výjimku vyvolala metoda pop nebo operator= V prvním případě je zásobník nedotčen, ale ve druhém je již zkrácen StringStack stk; String a; /* ... */ try { a = stk.pop(); } catch (...) { /* ??? */
Exception-safe programming Poučení Funkce, která má vedlejší efekty, smí vracet hodnotou pouze typy, jejichž kopírování nevyvolává výjimky a = stk.pop(); Pokud je třeba nějakou "nebezpečnou" hodnotu vracet, musí se předávat jako výstupní parametr stk.pop( a); Funkcí bez vedlejších efektů se problém netýká a = b + c; StringStack stk; String a; /* ... */ try { a = stk.pop(); } catch (...) { /* ??? */ Většiny operátorů se problém netýká: buď nemají vedlejší efekty, nebo vracejí trvale existující objekt odkazem Problematické jsou postfixové ++, --
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Řešení A Jako v STL Rozdělit pop na dvě funkce top vrací vrchol zásobníku může jej vracet odkazem nemodifikuje data pop pouze zkracuje je silně bezpečná StringStack stk; String a; /* ... */ try { a = stk.top(); } catch (...) { /* chyba kopírování nebo prázdný zásobník, proměnná a nezměněna, zásobník nedotčen */ stk.pop(); /* chyba zkracování, proměnná a změněna,
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Řešení B Namísto vracení hodnoty funkce pop vyplňuje parametr předávaný odkazem tím se vyloučí nutnost kombinovat volání pop s dalším kopírováním Pro uživatele jednodušší, implementace pop je však těžší StringStack stk; String a; /* ... */ try { stk.pop( a); } catch (...) { /* chyba zkracování nebo kopírování, proměnná a nezměněna, zásobník nedotčen */
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Řešení B Lze implementovat nad řešením A #include <memory> class StringStack { public: /* A */ String & top(); void pop(); /* B */ void pop( String & out) { String & t = top(); swap( out, t); try { pop(); } catch (...) { throw; };
Exception specifications
Exception specifications U každé funkce (operátoru, metody) je možno určit seznam výjimek, kterými smí být ukončena Na výjimky ošetřené uvnitř funkce se specifikace nevztahuje Pokud není specifikace uvedena, povoleny jsou všechny výjimky Specifikace respektuje dědičnost, to jest automaticky povoluje i všechny potomky uvedené třídy void a() { /* tahle smí všechno */ } void b() throw () /* tahle nesmí nic */ void c() throw ( std::bad_alloc) /* tahle smí std::bad_alloc */ void d() throw ( std::exception, MyExc) /* tahle smí potomky std::exception a MyExc */ před C++11
Exception-safe programming Exception specifications U každé funkce (operátoru, metody) je možno určit, že nevyvolává výjimky void a() { /* tahle smí všechno */ } void b() noexcept /* tahle nesmí nic */ C++11
Exception-safe programming Exception specifications U každé funkce (operátoru, metody) je možno určit, že nevyvolává výjimky noexcept může být podmíněno konstantním booleovským výrazem vhodné pro šablony void a() { /* tahle smí všechno */ } void b() noexcept /* tahle nesmí nic */ template< typename T> void swap( T & a, T & b) noexcept( is_nothrow_move_constructible<T>::value && is_nothrow_move_assignable<T>::value) T c( std::move( a)); a = std::move( b); b = std::move( c); C++11
Koenig lookup
iostream Problém: namespace namespace prostor { class Souradnice { public: int x, y; }; std::ostream & operator<<( std::ostream & s, const Souradnice & a) { return s << '[' << a.x << ',' << a.y << ']'; } }; prostor::Souradnice p; std::cout << p; // správný operator<< je v namespace prostor, // který není přímo vidět
iostream Problém: namespace namespace prostor { class Souradnice { public: int x, y; }; std::ostream & operator<<( std::ostream & s, const Souradnice & a) { return s << '[' << a.x << ',' << a.y << ']'; } }; prostor::Souradnice p; std::cout << p; // správný operator<< je v namespace prostor, // který není přímo vidět std::cout << std::endl; // tentýž problém je ale už tady: // tento operator<< je v namespace std
Oba případy jsou překládány správně Koenig lookup prostor::Souradnice p; std::cout << p; // správný operator<< je v namespace prostor, // který není přímo vidět std::cout << std::endl; // tentýž problém je ale už tady: // tento operator<< je v namespace std Oba případy jsou překládány správně Je k tomu nutná složitá definice vyhledávání identifikátoru tzv. Koenigovo vyhledávání používá se, je-li význam identifikátoru závislý na parametrech volání funkce použití operátoru
Koenigovo vyhledávání (zjednodušeno) Koenig lookup Koenigovo vyhledávání (zjednodušeno) Argument-dependent name lookup (ISO C++) Pro každý skutečný parametr se z jeho typu T určí množina asociovaných namespace Je-li T číselný, tyto množiny jsou prázdné Je-li T union nebo enum, jeho asociovaným namespace je ten, ve kterém je definován Je-li T ukazatel na U nebo pole U, přejímá asociované namespace od typu U Je-li T funkce nebo ukazatel na funkci, přejímá (sjednocením) asociované namespace všech parametrů a návratového typu Je-li T třída, asociovanými namespace jsou ty, v nichž jsou definovány tato třída a všichni její přímí i nepřímí předkové Je-li T instancí šablony, přejímá kromě asociovaných tříd a namespace definovaných pro třídu také asociované třídy a namespace všech typových argumentů šablony
Koenigovo vyhledávání (zjednodušeno) Koenig lookup Koenigovo vyhledávání (zjednodušeno) Argument-dependent name lookup (ISO C++) Pro každý skutečný parametr se z jeho typu T určí množina asociovaných namespace Identifikátor funkce se pak vyhledává v těchto prostorech Globální prostor a aktuální namespace Všechny namespace přidané direktivami using Sjednocení asociovaných namespace všech parametrů funkce Všechny varianty funkce nalezené v těchto namespace jsou rovnocenné Mezi nimi se vybírá podle počtu a typu parametrů Pokud není jednoznačně určena nejlepší varianta, je to chyba Volání v kontextu třídy: Je-li identifikátor nalezen uvnitř této třídy nebo některého předka (jako metoda), má přednost před výše uvedenými variantami (globálními funkcemi)
-----------------------------------
Klíčové vlastnosti C++
Zpřístupníme téměř vše, co dokáže „hardware“ Koncepce jazyka C/C++ Zpřístupníme téměř vše, co dokáže „hardware“ Proto máme lokální/globální/dynamicky alokovaná data Ukazatelová aritmetika Nabízíme pouze to, co si programátor neudělá sám Proto nemáme referenční semantiku a garbage collector Nezdržujeme, pokud nemusíme Nevirtuální metody Minimální běhové kontroly Neinicializovaná data Nejsme mateřská školka Konstrukce jsou často nebezpečné
Real programmers can write assembly code in any language. Koncepce jazyka C/C++ The committee shall make no rule that prevents C++ programmers from shooting themselves in the foot. First Amendment to the C++ Standard (urban legend) In C++ it's harder to shoot yourself in the foot, but when you do, you blow off your whole leg. Bjarne Stroustrup Within C++, there is a much smaller and cleaner language struggling to get out. Real programmers can write assembly code in any language. Larry Wall
Důsledky koncepce C/C++ Existuje mnoho způsobů, jak udělat totéž Špatně čitelné programy Existuje mnoho způsobů, jak udělat chybu Řešení: Dobrovolné omezení síly jazyka Některé možnosti se nevyužívají Chystají se pasti na úmyslná i neúmyslná vybočení Dodržování konvencí a vzorů Využití moudrosti věků Srozumitelnost rozhraní i kódu
Abstraktní pohled na typy
Číselné typy (char, int, double,...) jsou dobře zapouzdřené Dobře zapouzdřený typ Dobře zapouzdřený typ Nemůže „onemocnět“ při nevhodném používání Zejména z hlediska inicializace, kopírování a úklidu Tím není vyloučena chyba jednotlivých operací dělení nulou, přístup mimo meze pole... Číselné typy (char, int, double,...) jsou dobře zapouzdřené Až na absenci inicializace Ukazatele v C/C++ nejsou dobře zapouzdřené Chybějící inicializací vznikne nedetekovatelný vadný obsah Chybějící úklid může způsobit nedosažitelnost alokované paměti Duplikovaná dealokace je katastrofální chyba Kopírování ukazatelů vede k neřešitelné odpovědnosti za úklid Ukazatele na lokální data mohou ztratit použitelnost ... a mnoho dalších potenciálních problémů
Dobře zapouzdřené typy Číselné typy (char, int, double,...) jsou dobře zapouzdřené Ukazatele v C/C++ nejsou dobře zapouzdřené Reference v C++ nejsou dobře zapouzdřené Odpadá problém kopírování a dealokace Zůstává problém odkazu na zaniklá data Kontejnery jsou tak dobře zapouzdřené, jako jejich prvky
Dobře zapouzdřené typy Třída obsahující pouze dobře zapouzdřené prvky Nemusí mít copy-constructor, operator= ani destruktor Sama bude dobře zapouzdřená Konstruktor může být nutný kvůli rozhraní a/nebo logice třídy Třída obsahující prvky, které nejsou dobře zapouzdřené Pokud má být dobře zapouzdřena, musí obsahovat: Konstruktor Copy-constructor operator= Destruktor Prvky musí být privátní
Třídy z pohledu chování Hodnotové typy Nemají vlastní identitu (3+5 je totéž jako 2+6) Kopírování je bez problémů Jsou to programátorské proměnné, nikoliv matematické hodnoty Dědičnost nemá smysl „Živé“ objekty Mají vlastní identitu (dvě černé vrány nejsou jedna vrána) Mění stav v průběhu života, kopírování nemívá smysl Bývají polymorfní – dědičnost Rozhraní Vnější pohled na plnohodnotný objekt Odkazy Na rozhraní nebo plnohodnotné objekty Nemají vlastní identitu Mohou dovolovat kopírování – musí řešit odpovědnost za úklid Další (funktory, singletony,...)
Třídy z pohledu instanciace Neinstanciované třídy Policy classes Traits templates Třídy s jednou instancí Singletony Třídy instanciované jako proměnné Hodnotové typy, chytré ukazatele, ... Jednorázové třídy - Funktory, visitory apod. Třídy reprezentující výjimky Třídy instanciované dynamicky Plain Old Data Velká a sdílená data Živé objekty - třídy s hierarchií dědičnosti Poznámka: Toto třídění nemůže být ani úplné ani jednoznačné
Kanonické tvary tříd
Kanonické tvary tříd Singletony
Kanonické tvary tříd Singletony Veřejná statická funkce zpřístupňující instanci Lokální statická proměnná obsahuje jedinou instanci Privátní konstruktor a destruktor Znemožní vytváření a likvidaci mimo předepsaná místa Privátní hlavička copy-constructoru a operatoru= Tělo se neimplementuje Znemožní kopírování Většinou nevyužívají dědičnost ani virtuální funkce /* syslog.h */ class SysLog { public: static SysLog & instance() { static SysLog inst; return inst; } void write( const std::string & s) { os_ << s; } private: std::ofstream os_; SysLog() : os_("syslog") {} ~SysLog() {} SysLog( const SysLog &); void operator=( const SysLog &); }; /* pouziti */ Syslog::instance().write( "Hi");
Kanonické tvary tříd Hodnotové typy
Kanonické tvary tříd Hodnotové typy – jednoduché případy Obsahují pouze dobře zapouzdřené prvky Číselné typy Jiné dobře zapouzdřené třídy Prvky mohou být veřejné Pokud nemusí dodržovat nějaký invariant class Complex { public: double re, im; Complex(); Complex( double r, double i = 0); Complex operator-() const; Complex & operator+=( const Complex &); }; Complex operator+( const Complex &, const Complex &);
Kanonické tvary tříd Hodnotové typy – jednoduché případy Obsahují pouze dobře zapouzdřené prvky Číselné typy Jiné dobře zapouzdřené třídy Privátní prvky mívají zpřístupňující metody set/get konvence class Complex { public: Complex(); Complex( double r, double i = 0); double get_re() const; void set_re( double x); double get_im() const; void set_im( double x); Complex operator-() const; Complex & operator+=( const Complex &); private: double re_, im_; }; Complex operator+( const Complex &, const Complex &);
Kanonické tvary tříd Hodnotové typy – jednoduché případy Obsahují pouze dobře zapouzdřené prvky Privátní prvky mívají zpřístupňující metody Konstruktory s parametry (někdy) Konverze, inicializace Mohou nahradit set-metody Konstruktor bez parametrů Pokud jsou konstruktory s parametry Pokud třída obsahuje atomické číselné typy class Complex { public: Complex(); Complex( double r, double i = 0); double get_re() const; void set_re( double x); double get_im() const; void set_im( double x); Complex operator-() const; Complex & operator+=( const Complex &); private: double re_, im_; }; Complex operator+( const Complex &, const Complex &);
Kanonické tvary tříd Hodnotové typy – jednoduché případy Obsahují pouze dobře zapouzdřené prvky Konstruktory s parametry (někdy) Konstruktor bez parametrů Copy-constructor, operator=, destruktor nejsou zapotřebí Vyhovuje chování generované překladačem class Complex { public: Complex(); Complex( double r, double i = 0); double get_re() const; void set_re( double x); double get_im() const; void set_im( double x); Complex operator-() const; Complex & operator+=( const Complex &); private: double re_, im_; }; Complex operator+( const Complex &, const Complex &);
Kanonické tvary tříd Hodnotové typy Unární a modifikující binární operátory jako metody unární operátory (včetně postfixového ++) vrací hodnotou modifikující operátory (vyjma postfixového ++) vrací odkazem * this Nemodifikující binární operátory jako globální funkce Vrací hodnotou Většinou lze implementovat pomocí modifikujících binárních operátorů class Complex { public: double re, im; Complex(); Complex operator-() const; Complex & operator+=( const Complex &); }; Complex operator+( const Complex & a, const Complex & b) { Complex tmp( a); tmp += b; return tmp; }
Kanonické tvary tříd Hodnotové typy Dědičnost nemá smysl Virtuální funkce nemají smysl class Complex { public: double re, im; Complex(); Complex operator-() const; Complex & operator+=( const Complex &); }; Complex operator+( const Complex & a, const Complex & b) { Complex tmp( a); tmp += b; return tmp; }
Kanonické tvary tříd Hodnotové typy - složitější případy Obsahují nedostatečně zapouzdřené prvky Ukazatele apod. Konstruktor bez parametrů Copy-constructor operator= Destruktor Data a pomocné funkce privátní class Matrix { public: Matrix(); Matrix( const Matrix &); Matrix & operator=( const Matrix &); ~Matrix(); Matrix operator-() const; Matrix & operator+=( const Matrix &); friend Matrix operator+( const Matrix &, const Matrix &); private: int m_, n_; double * d_; }; Matrix operator+(
Kanonické tvary tříd Hodnotové typy - složitější případy Obsahují nedostatečně zapouzdřené prvky Ukazatele apod. Více než jeden dynamicky alokovaný blok Problémy s exception-safety Vyplatí se samostatně zapouzdřit ukazatele do pomocných tříd class Matrix { public: Matrix(); Matrix( const Matrix &); Matrix & operator=( const Matrix &); ~Matrix(); Matrix operator-() const; Matrix & operator+=( const Matrix &); friend Matrix operator+( const Matrix &, const Matrix &); private: int m_, n_; double * d_; }; Matrix operator+(
Kanonické tvary tříd Funktory, visitory
Kanonické tvary tříd Třídy instanciované jako proměnné Funktory Třídy určené k předávání šablonovaným funkcím Obvykle používány pouze jednou Co nejjednodušší konstrukce Konstruktor S požadovanými parametry Data Metoda implementující požadovanou operaci Typicky operator() Dědičnost nemá smysl struct Printer { public: Printer( std::ostream & o) : out_( o) {} void operator()( int x) { o << x << std::endl; } private: std::ostream & out_; }; std::vector< int> v; for_each( v.begin(), v.end(), Printer( std::cout));
Kanonické tvary tříd Třídy instanciované jako proměnné Abstraktní visitor Sada čistě virtuálních funkcí Virtuální destruktor Prázdné tělo Vše veřejné Konkrétní visitor Obvykle používán pouze jednou Co nejjednodušší konstrukce Kontrola přístupu není nutná Potomek abstraktní třídy Privátní nebo veřejná data Konstruktor (jsou-li data) Virtuální metody implementující požadované operace class Visitor { public: virtual void visitEllipse( Ellipse *)=0; virtual void visitRectangle( Rectangle *)=0; virtual void visitLine( Line *)=0; virtual ~Visitor() {} }; class PrintVisitor : public Visitor { PrintVisitor( Printer * p) : p_( p) {} virtual void visitEllipse( Ellipse *); virtual void visitRectangle( Rectangle *); virtual void visitLine( Line *); private: Printer * p_;
Dynamicky alokovaná data Kanonické tvary tříd Dynamicky alokovaná data
Kanonické tvary tříd Třídy instanciované dynamicky Plain Old Data Neobsahují dynamicky alokované součásti Datové položky obvykle veřejné Většinou bez konstruktoru Nedefinuje se copy-constructor, operator= ani destruktor Bez virtuálních funkcí a dědičnosti Někdy s obyčejnými metodami class Configuration { public: bool show_toolbar; bool show_status_bar; int max_windows; }; Dynamická alokace pouze kvůli požadované životnosti Pro srovnání Pojem "Plain Old Data" (POD) je definován jazykem C++ takto: Třída nemá žádný konstruktor ani destruktor Třída nemá virtuální funkce ani virtuální dědičnost Všichni předkové a datové položky jsou POD POD-třída má zjednodušená pravidla: Může být součástí unie Může být staticky inicializována
Kanonické tvary tříd Třídy instanciované dynamicky Velká a sdílená data Často obsahuje definice typů Konstruktor nebo několik konstruktorů, často s parametry Destruktor Privátní datové položky Manipulace prostřednictvím metod udržujících konzistenci Obvykle privátní neimplementovaný copy-constructor a operator= Většinou bez virtuálních funkcí a dědičnosti Dynamická alokace kvůli velikosti a životnosti class ColoredGraph { public: typedef int node_id; typedef int edge_id; typedef int color; ColoredGraph(); ColoredGraph( istream &); ~ColoredGraph(); node_id add_node(); edge_id add_edge( node_id, node_id, color); /* ... */ private: vector< node_id> nodes_; multimap< node_id, edge_id> edges_; map< edge_id, color> colors_; ColoredGraph(const ColoredGraph &); void operator=(const ColoredGraph &); };
Kanonické tvary tříd Třídy instanciované dynamicky Prvky datových struktur Obsahují ukazatele Konstruktor (inicializace ukazatelů) Další konstruktory, jsou-li využitelné Destruktor jen tehdy, pokud může mít dobře definované chování Datové položky někdy veřejné Nepolymorfní struktury: bez virtuálních funkcí a dědičnosti Konzistentní manipulace zajištěna zapouzdřením celé struktury do jiné třídy Dynamická alokace kvůli životnosti a složitosti struktury class TreeNode { public: TreeNode() : left( 0), right( 0) {} TreeNode( TreeNode * l, TreeNode * r) : left( l), right( r) {} ~TreeNode() { /* ??? */ } TreeNode * left, * right; }; class Tree { Tree() : root( 0) {} ~Tree() { /* ... */ } void insert_node( /* ??? */ ); private: TreeNode * root;
Kanonické tvary tříd Třídy instanciované dynamicky Prvky datových struktur Datové položky někdy veřejné Konzistentní manipulace zajištěna zapouzdřením celé struktury do jiné třídy Problém: zpřístupnění prvků vnějšímu světu class Tree { public: Tree() : root( 0) {} ~Tree() { /* ... */ } void insert_node( /* ??? */ ); /* ??? */ find_node( /* ... */ ); private: class TreeNode { TreeNode() : left( 0), right( 0) {} ~TreeNode() { /* ??? */ } TreeNode * left, * right; }; TreeNode * root;
Kanonické tvary tříd Třídy instanciované dynamicky Prvky datových struktur Datové položky někdy veřejné Konzistentní manipulace zajištěna zapouzdřením celé struktury do jiné třídy Problém: zpřístupnění prvků vnějšímu světu Lze řešit zapouzdřením ukazatelů do tříd – viz iterátory Shrnuto: NodeRef - technicky hodnotová třída s přiznanou referenční semantikou Tree – obvykle nekopírovatelná třída TreeNode - PlainOldData class Tree { private: class TreeNode { public: /* ... */ TreeNode * left, * right; }; class NodeRef { TreeNode * p; Tree() : root( 0) {} ~Tree() { /* ... */ } NodeRef create_node( /* ... */); void insert_node( NodeRef p); NodeRef find_node( /* ... */ ); TreeNode * root; Tree( const Tree &); /* ... */
Kanonické tvary tříd Třídy s dědičností
Kanonické tvary tříd Třídy instanciované dynamicky Třídy s hierarchií dědičnosti "Objektové programování" Abstraktní třída Sada veřejných (čistě) virtuálních funkcí Veřejný virtuální destruktor Prázdné tělo Konstruktor obvykle protected Chrání proti instanciaci, pokud nejsou čistě virtuální funkce Pro jistotu: privátní neimplementovaný copy-constructor a operator= Kopírování metodou clone Žádné datové položky class AbstractObject { public: virtual void doit() {} virtual void showit( Where *) const = 0; virtual AbstractObject * clone() const = 0; virtual ~AbstractObject() {} protected: AbstractObject() {} private: AbstractObject( const AbstractObject&); void operator=( };
Kanonické tvary tříd Třídy instanciované dynamicky Třídy s hierarchií dědičnosti "Objektové programování" Konkrétní třída Potomek abstraktní třídy Veřejný konstruktor Virtuální funkce obvykle protected Privátní data class ConcreteObject : public AbstractObject { public: ConcreteObject( /*...*/); protected: virtual void doit(); virtual void showit( Where *) const; virtual AbstractObject * clone() const; virtual ~ConcreteObject(); private: /* data */; };
Neinstanciované třídy Kanonické tvary tříd Neinstanciované třídy
Kanonické tvary tříd Neinstanciované třídy Policy classes Traits templates Obsahují pouze Definice typů (typedef, případně vnořené třídy) Definice výčtových konstant (a typů) Statické funkce Statická data Obvykle vše veřejné (struct) Nemají konstruktory ani destruktory Obvykle nevyužívají dědičnost struct allocation_policy { static void * alloc( size_t); static void free( void *); }; template< typename T> struct type_traits; template<> struct type_traits< char> { typedef char param_type; enum { min = 0, max = 255 }; static bool less( char, char);
Kanonické tvary tříd Policy class – použití Univerzální šablona template< class policy> class BigTree { /* ... */ Node * new_node() { return policy::alloc(sizeof(Node)); } }; Specifická policy class struct my_policy { static void * alloc( size_t); static void free( void *); Použití BigTree< my_policy> my_tree; Traits template – použití Deklarace traits template< class T> struct type_traits; Univerzální šablona template< class T> class Stack { /* ... */ void push( typename type_traits::param_t x); }; Univerzální definice traits struct type_traits { typedef const T & param_t; Explicitní specializace traits template<> struct type_traits< char> { typedef char param_t;
Traits a kontejnery template< typename K> struct for_each_traits; void for_each( K & data) { typedef typename K::value_type T; for( ... it = ... ) for_each_traits< T>::f( it); } template< typename U> class vector { public: typedef U value_type; ... }; template<> struct for_each_traits< int> { static void f(...) { } typedef vector< int> my_k; my_k data; for_each( data);
Traits a iterátory template< typename K> struct for_each_traits; template< typename IT> void for_each( IT b, IT e) { typedef typename iterator_traits< IT>::value_type T; for( ... it = ... ) for_each_traits< T>::f( it); } template<> struct for_each_traits< int> { static void f(...) { ... } }; typedef int my_k[ N]; my_k data; for_each( data, data + N); // IT = int*
Lambda (C++0x) template< typename P> void modify( const P & x) { for( ... it = ... ) * it = x.operator()( * it); } struct functor_twice { int operator()( int x) const return 2 * x; }; modify( functor_twice()); for_each( []( int x){ return 2 * x });
Lambda (C++0x vs. boost) for_each( []( int x){ return 2 * x }); for_each( constant( 2) * _1);
Lambda (boost) struct functor_1 { template< typename T> T operator()( T x) const { return x; } }; extern functor_1 _1; struct functor_const { functor_const( T c) : c_( c) {} return c_; T c_; functor_const< T> constant( T c) { return functor_const< T>( c); } template< typename F1, typename F2> struct functor_mul { functor_mul( F1 f1, F2 f2) : f1_( f1), f2_( f2) {} T operator()( T x) const { return c_; } F1 f1_; F2 f2_; }; functor_mul< F1, F2> operator+ ( F1 f1, F1 f2) return functor_const< F1, F2> ( f1, f2); for_each( constant( 2) * _1);
Lambda (boost) - problémy for_each( std::cout << "x=" << _1); for_each( constant( 2) * _1);
Šablony Templates
Šablony tříd - instanciace Instanciace šablony: Šablonu lze použít jako typ pouze s explicitním uvedením skutečných parametrů odpovídajících druhů: celé číslo: celočíselný konstantní výraz ukazatel: adresa globální nebo statické proměnné či funkce kompatibilního typu libovolný typ – jméno typu či typová konstrukce (včetně jiné instanciované šablony) Užití instanciované šablony: Instanciované šablony jsou stejného typu, pokud jsou stejného jména a jejich skutečné parametry obsahují stejné hodnoty konstantních výrazů, adresy stejných proměnných či funkcí a stejné typy
Šablony tříd – pravidla použití Uvnitř těla šablony (nebo jako její předky) je možno užívat libovolné typy včetně: Instancí jiných šablon Téže šablony s jinými argumenty Téže šablony se stejnými argumenty V tomto případě se argumenty mohou, ale nemusí opisovat Ekvivalentní varianty šablony s copy-constructorem: template< typename T> class X { X( const X< T> &); }; X( const X &); Některé překladače připouštějí i tuto variantu X< T>( const X< T> &);
Šablony tříd – pravidla použití Metody šablon mohou mít těla uvnitř třídy nebo vně Vně uvedená těla metod musejí být připojena k šabloně takto: template< typename T> void X< T>::f( int a, int b) { /* ... */ } V kvalifikovaném jméně metody je nutné uvést patřičný seznam argumentů, tj. X< T>::f a nikoliv X::f Těla metod musejí být viditelná z každého místa, kde jsou pro nějakou instanci šablony volána Musejí tedy typicky být v témže hlavičkovém souboru jako sama šablona. Uvedení těla metody vně třídy tedy u šablon typicky nic nepřináší, může být však vynuceno rekurzivními odkazy mezi šablonami apod.
Šablony tříd – pravidla použití Šablona třídy se překládá až v okamžiku instanciace, tj. použití s konkrétními parametry Překladač instanciuje (tj. překládá) pouze ty metody, které jsou zapotřebí (tj. jsou volány nebo jsou virtuální) Některá těla metod tedy nemusí být pro některé případy parametrů přeložitelná template< typename Container> class Proxy { public: void pop_front() { c->pop_front(); } // jen pro list/deque /* ... */ private: Container * c; }; Explicitní instanciace Překladač je možné donutit ke kompletní instanciaci šablony template class Array< 128, char>;
Dopředná deklarace šablony Šablony tříd – triky Dopředná deklarace šablony template< typename T> class X; /* ... zde může být použito X<U> s jakýmikoliv argumenty U... ... pouze v kontextech, kde kompilátor nepotřebuje znát tělo šablony ... */ template< typename T> class X { /* ... */ };
Šablony funkcí Pod stejným identifikátorem může být deklarováno několik různých šablon funkce a navíc několik obyčejných funkcí. Obyčejné funkce mají přednost před generickými template< class T> T max( T a, T b) { return a < b ? b : a; }; char * max( char * a, char * b) { return strcmp( a, b) < 0 ? b : a; }; template< int n, class T> T max( Array< n, T> a) { /* ... */ } Příklad ze standardních knihoven: template< class T> void swap( T & a, T & b) { T tmp(a); a = b; b = tmp; }; K tomu řada chytřejších implementací swap pro některé třídy
Šablony – pokročilé konstrukce jazyka Parciální specializace Deklarovanou šablonu lze pro určité kombinace parametrů předefinovat jinak, než určuje její základní definice template< int n> class Array< n, bool> { /* specializace pro pole typu bool */ }; Krajním případem parciální specializace je explicitní specializace Explicitní specializace template<> class Array< 32, bool> { /* ... */ }; U šablon funkcí nahrazena obyčejnou funkcí Explicitní instanciace Překladač je možné donutit ke kompletní instanciaci šablony template class Array< 128, char>;
Parciální specializace Deklarovanou šablonu lze pro určité kombinace parametrů předefinovat jinak, než určuje její základní definice Parciálně specializovat lze šablony funkcí, celé šablony tříd i jednotlivě těla jejich metod Obsah specializace šablony třídy (teoreticky) nemusí nijak souviset se základní definicí - může mít zcela jiné položky, předky apod. Základní definice dokonce nemusí vůbec existovat (ale musí být deklarována) template< class X, class Y> class C; // základní deklarace template< class P, class Q> class C< P *, Q *> { // specializace bool cmp( P *, Q *); }; template< class Z> class C< Z, Z> : public Z { // jiná specializace bool set( Z &); template< class X, class Y> class C { // základní definice X add( Y);
Parciální specializace Deklarovanou šablonu lze pro určité kombinace parametrů předefinovat jinak, než určuje její základní definice Parciální specializace může mít stejný, menší i větší počet formálních parametrů než základní definice, jejich hodnoty se odvozují ze skutečných parametrů šablony (kterých je vždy tolik, kolik určuje základní definice) template< class T, class U, int n> class C< T[n], U[n]> { /* specializace pro dvě pole stejné velikosti */ };
Teoretický pohled na šablony Příklad template< int N> struct Fact { enum { value = Fact< N-1>::value * N }; }; template<> struct Fact< 0> { enum { value = 1 };
Teoretický pohled na šablony Příklad template< int N> struct Fact { enum { value = Fact< N-1>::value * N }; }; template<> struct Fact< 0> { enum { value = 1 }; Použití enum { N = 10 }; int permutations[ Fact< N>::value];
Teoretický pohled na šablony Příklad template< int N> struct Fact { enum { value = Fact< N-1>::value * N }; }; template<> struct Fact< 0> { enum { value = 1 }; Kontrolní otázka: Kolik je Fact< -1>::value
Teoretický pohled na šablony Příklad template< int N> struct Fact { enum { value = Fact< N-1>::value * N }; }; template<> struct Fact< 0> { enum { value = 1 }; Kontrolní otázka: Kolik je Fact< -1>::value MS Visual C++ 7.1: fatal error C1202: recursive type or function dependency context too complex Řetěz instanciací Fact< -1>, Fact< -2>, Fact< -3>, ... způsobí přetečení tabulek kompilátoru
Šablony tříd – explicitní instanciace Je-li předem známa množina typů (formálních parametrů), pro něž se bude šablona instanciovat, není nutno publikovat těla metod ve zdrojové formě a je možné je předkompilovat do knihovny Veřejný hlavičkový soubor X.h – hlavička třídy template< class T> class X { /* ... */ void f(/*...*/); }; Nepublikovaný hlavičkový soubor XBody.h Generická těla metod #include "X.h" template< class T> void X< T>::f(/*...*/) { /*...*/ } Knihovní modul XBodyInt.cpp Instanciace pro typ int #include "XBody.h" template X< int>;
Teoretický pohled na šablony Šablona třídy je kompilátorem vyhodnocovaná funkce f : Ti T T je množina všech typů zkonstruovatelných v jazyce C
Teoretický pohled na šablony Šablona třídy je kompilátorem vyhodnocovaná funkce f : Ti T T je množina všech typů zkonstruovatelných v jazyce C f : Ti × Kj T šablona s celočíselnými parametry K je množina všech celočíselných konstant
Teoretický pohled na šablony Šablona třídy je kompilátorem vyhodnocovaná funkce f : Ti T T je množina všech typů zkonstruovatelných v jazyce C f : Ti × Kj T šablona s celočíselnými parametry K je množina všech celočíselných konstant f : Ti × Kj Tm × Kn výsledná třída může obsahovat typy a konstanty
Teoretický pohled na šablony Šablona třídy je kompilátorem vyhodnocovaná funkce f : Ti × Kj Tm × Kn Taková funkce může být definována Jedním předpisem Základní šablonou Po částech Parciálními specializacemi šablony V jednotlivých bodech Explicitními specializacemi šablony
Exception handling Mechanismus výjimek
Exception-safe programming Používat throw a catch je jednoduché Těžší je programovat běžný kód tak, aby se choval korektně i za přítomnosti výjimek Exception-safety Exception-safe programming void f() { int * a = new int[ 100]; int * b = new int[ 200]; g( a, b); delete[] b; delete[] a; } Pokud new int[ 200] způsobí výjimku, procedura zanechá naalokovaný nedostupný blok Pokud výjimku vyvolá procedura g, zůstanou dva nedostupné bloky
Exception-safe programming Používat throw a catch je jednoduché Těžší je programovat běžný kód tak, aby se choval korektně i za přítomnosti výjimek Exception-safety Exception-safe programming T & operator=( const T & b) { if ( this != & b ) delete body_; body_ = new TBody( b.length()); copy( body_, b.body_); } return * this; Pokud new TBody způsobí výjimku, operátor= zanechá v položce body_ původní ukazatel, který již míří na dealokovaný blok Pokud výjimku vyvolá procedura copy, operátor zanechá třídu v neúplném stavu
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Výjimka může být vyvolána uvnitř, ale musí být zachycena nejpozději uvnitř destruktoru Zdůvodnění: V rámci ošetření výjimek (ve fázi stack-unwinding) se volají destruktory lokálních proměnných Výjimku zde vyvolanou nelze z technických i logických důvodů ošetřit (ztratila by se původní výjimka) Nastane-li taková výjimka, volá se funkce terminate() a program končí
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Výjimka může být vyvolána uvnitř, ale musí být zachycena nejpozději uvnitř destruktoru Toto pravidlo jazyka sice platí pouze pro destruktory lokálních proměnných A z jiných důvodů též pro globální proměnné Je však vhodné je dodržovat vždy Bezpečnostní zdůvodnění: Destruktory lokálních proměnných často volají jiné destruktory Logické zdůvodnění: Nesmrtelné objekty nechceme
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Konstruktor globálního objektu nesmí skončit vyvoláním výjimky Zdůvodnění: Není místo, kde ji zachytit Stane-li se to, volá se terminate() a program končí Jiné konstruktory ale výjimky volat mohou (a bývá to vhodné)
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Konstruktor globálního objektu nesmí skončit vyvoláním výjimky Copy-constructor typu v hlavičce catch-bloku nesmí skončit vyvoláním výjimky Zdůvodnění: Catch blok by nebylo možné vyvolat Stane-li se to, volá se terminate() a program končí Jiné copy-constructory ale výjimky volat mohou (a bývá to vhodné)
Exception-safe programming Pravidla vynucená jazykem Destruktor nesmí skončit vyvoláním výjimky Konstruktor globálního objektu nesmí skončit vyvoláním výjimky Copy-constructor typu v hlavičce catch-bloku nesmí skončit vyvoláním výjimky
Exception-safe programming Poznámka: Výjimky při zpracování výjimky Výjimka při výpočtu výrazu v throw příkaze Tento throw příkaz nebude vyvolán Výjimka v destruktoru při stack-unwinding Povolena, pokud neopustí destruktor Po zachycení a normálním ukončení destruktoru se pokračuje v původní výjimce Výjimka uvnitř catch-bloku Pokud je zachycena uvnitř, ošetření původní výjimky může dále pokračovat (přikazem throw bez výrazu) Pokud není zachycena, namísto původní výjimky se pokračuje ošetřováním nové
Exception-safe programming Kompilátory samy ošetřují některé výjimky Dynamická alokace polí Dojde-li k výjimce v konstruktoru některého prvku, úspěšně zkonstruované prvky budou destruovány Ve zpracování výjimky se poté pokračuje
Exception-safe programming Kompilátory samy ošetřují některé výjimky Dynamická alokace polí Dojde-li k výjimce v konstruktoru některého prvku, úspěšně zkonstruované prvky budou destruovány Ve zpracování výjimky se poté pokračuje Výjimka v konstruktoru součásti (prvku nebo předka) třídy Sousední, již zkonstruované součásti, budou destruovány Uvnitř konstruktoru je možno výjimku zachytit speciálním try-blokem: X::X( /* formální parametry */) try : Y( /* parametry pro konstruktor součásti Y */) { /* vlastní tělo konstruktoru */ } catch ( /* parametr catch-bloku */ ) { /* ošetření výjimky v konstruktoru Y i ve vlastním těle */ } Konstrukci objektu nelze dokončit Opuštění speciálního catch bloku znamená throw;
Exception-safe programming Bezpečné programování s výjimkami
Exception-safe programming Definice (Weak) exception safety Funkce (operátor, konstruktor) je (slabě) bezpečná, pokud i v případě výjimky zanechá veškerá data v konzistentním stavu Konzistentní stav znamená zejména: Nedostupná data byla korektně destruována a odalokována Ukazatele nemíří na odalokovaná data Platí další invarianty dané logikou aplikace
Exception-safe programming Definice (Weak) exception safety Funkce (operátor, konstruktor) je (slabě) bezpečná, pokud i v případě výjimky zanechá veškerá data v konzistentním stavu Konzistentní stav znamená zejména: Nedostupná data byla korektně destruována a odalokována Ukazatele nemíří na odalokovaná data Platí další invarianty dané logikou aplikace Strong exception safety Funkce je silně bezpečná, pokud v případě, že skončí vyvoláním výjimky, zanechá data ve stejném (pozorovatelném) stavu, ve kterém byla při jejím vyvolání Observable state - chování veřejných metod Nazýváno též "Commit-or-rollback semantics"
Exception-safe programming Poznámky (Weak) exception safety Tohoto stupně bezpečnosti lze většinou dosáhnout Stačí vhodně definovat nějaký konzistentní stav, kterého lze vždy dosáhnout, a ošetřit pomocí něj všechny výjimky Konzistentním stavem může být třeba nulovost všech položek Je nutné upravit všechny funkce tak, aby je tento konzistentní stav nepřekvapil (mohou na něj ale reagovat výjimkou) Strong exception safety Silné bezpečnosti nemusí jít vůbec dosáhnout, pokud je rozhraní funkce navrženo špatně Obvykle jsou problémy s funkcemi s dvojím efektem Příklad: funkce pop vracející odebranou hodnotu
Exception-safe programming Příklad: String č. 2 operator= Nebezpečná implementace: Pokud new char způsobí výjimku, operátor= zanechá v položce str_ původní ukazatel, který již míří na dealokovaný blok class String { public: // ... private: char * str_; }; String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } return * this;
Exception-safe programming Příklad: String č. 2 operator= Nebezpečná implementace: Pokud new char způsobí výjimku, operátor= zanechá v položce str_ původní ukazatel, který již míří na dealokovaný blok K jiné výjimce zde dojít nemůže: std::operator delete výjimky nikdy nevyvolává char je vestavěný typ a nemá tedy konstruktory které by mohly výjimku vyvolávat strlen a strcpy jsou C-funkce Parametry a návratová hodnota se předávají odkazem class String { public: // ... private: char * str_; }; String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } return * this;
Exception-safe programming Příklad: String č. 2 operator= Naivní pokus o opravu: Pokud new char způsobí výjimku, ošetří se Objekt se uvede do konzistentního stavu Výjimka se propaguje dál - ven z funkce Problém: V catch bloku teoreticky může vzniknout nová výjimka String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; try { str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } catch ( ... ) str_ = new char[ 1]; * str_ = 0; throw; return * this;
Exception-safe programming Příklad: String č. 2 operator= Lepší pokus o opravu: Pokud new char způsobí výjimku, ošetří se Je nutné pozměnit invariant třídy String: Položka str_ nyní smí obsahovat nulový ukazatel String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; try { str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } catch ( ... ) str_ = 0; throw; return * this;
Exception-safe programming Příklad: String č. 2 operator= Lepší pokus o opravu: Pokud new char způsobí výjimku, ošetří se Je nutné pozměnit invariant třídy String: Položka str_ nyní smí obsahovat nulový ukazatel Takový exemplář String je považován za konzistentní Konzistentnost nemusí znamenat, že to je z uživatelského pohledu platná hodnota Může být považována i za chybovou a každá operace s takovou hodnotou může vyvolávat výjimku String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; try { str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } catch ( ... ) str_ = 0; throw; return * this;
Exception-safe programming Příklad: String č. 2 operator= Ekvivalentní řešení: Nulovat str_ po delete Pokud new způsobí výjimku, v str_ zůstane nulový ukazatel String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; str_ = 0; str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } return * this;
Exception-safe programming Příklad: String č. 2 operator= Chyba: změnili jsme invariant str_ nyní může být nulové delete _str je v pořádku operator delete je vždy proti nulovému ukazateli ošetřen (nedělá nic) strlen a strcpy ale fungovat nebudou String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; str_ = 0; str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } return * this;
Exception-safe programming Příklad: String č. 2 operator= Opraveno String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; str_ = 0; if ( b.str_ ) str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } return * this;
Exception-safe programming Příklad: String č. 2 operator= Vylepšení: operator= může vyvolávat výjimku, pokud se přiřazuje neplatná hodnota Tato výjimka může být definována např. takto: #include <exception> class InvalidString : public std::exception { virtual const char * what() const { return "Invalid string"; } String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; str_ = 0; if ( b.str_ ) str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } else throw InvalidString(); return * this;
Exception-safe programming Příklad: String č. 2 operator= Toto řešení je slabě bezpečné Silně bezpečné ale není: Pokud dojde k výjimce, nezachovává se původní stav dat To bude pro uživatele nepříjemné: String x, y; /* ... */ try { x = y + x; } catch (...) { /* ... */ Uživatel nedokáže rozlišit mezi výjimkami v operátorech + a = Náš operator= ale v případě výjimky ztratí hodnotu x String & String::operator=( const String & b) { if ( this != & b ) delete[] str_; str_ = 0; if ( b.str_ ) str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } else throw InvalidString(); return * this;
Exception-safe programming Příklad: String č. 2 operator= Silně bezpečné řešení Pokud dojde k výjimce v new, nestane se nic Ani před throw nenastane žádná změna String & String::operator=( const String & b) { if ( this != & b ) if ( b.str_ ) char * aux = new char[ strlen( b.str_) + 1]; strcpy( aux, b.str_); delete[] str_; str_ = aux; } else throw InvalidString(); return * this;
Exception-safe programming Příklad: String č. 2 operator= Silně bezpečné řešení Pozorování: Toto řešení je "shodou okolností" imunní proti this == & b String & String::operator=( const String & b) { if ( this != & b ) if ( b.str_ ) char * aux = new char[ strlen( b.str_) + 1]; strcpy( aux, b.str_); delete[] str_; str_ = aux; } else throw InvalidString(); return * this;
Exception-safe programming Příklad: String č. 2 operator= Silně bezpečné řešení Pozorování: Toto řešení je "shodou okolností" imunní proti this == & b Test je možno zrušit String & String::operator=( const String & b) { if ( b.str_ ) char * aux = new char[ strlen( b.str_) + 1]; strcpy( aux, b.str_); delete[] str_; str_ = aux; } else throw InvalidString(); return * this;
Exception-safe programming Příklad: String č. 2 operator= Silně bezpečné řešení Pokud je copy-constructor silně bezpečný Standardní řešení: Copy-constructor naplní lokální proměnnou c kopií parametru b Zde může dojít k výjimce Metoda swap vyměňuje obsah this a proměnné c Metoda swap je rychlá a nevyvolává výjimky Před návratem z operatoru se volá destruktor c Tím zaniká původní obsah this void String::swap( String & x) { char * aux = str_; str_ = x.str_; x.str_ = aux; } String & String::operator=( const String & b) String c( b); swap( c); return * this;
Exception-safe programming Příklad: String č. 2 operator= Silně bezpečné řešení Metodu swap je vhodné publikovat ve formě globální funkce Některé algoritmy nad kontejnery obsahujícími String se tak zrychlí a stanou se bezpečnými vůči výjimkám void String::swap( String & x) { char * aux = str_; str_ = x.str_; x.str_ = aux; } String & String::operator=( const String & b) String c( b); swap( c); return * this; void swap( String & x, String & y) x.swap( y);
Exception-safe programming Příklad: String č. 2 operator= Silně bezpečné řešení Metodu swap je vhodné publikovat ve formě globální funkce Některé algoritmy nad kontejnery obsahujícími String se tak zrychlí a stanou se bezpečnými vůči výjimkám Sama metoda swap může využívat šablonu swap pro typ char * #include <algorithm> void String::swap( String & x) { swap( str_, x.str_); } String & String::operator=( const String & b) String c( b); swap( c); return * this; void swap( String & x, String & y) x.swap( y);
Exception-safe programming Příklad: String č. 2 copy-constructor Silně bezpečné řešení Pokud tělo dorazí na konec, budou datové položky korektně vyplněny Tělo může vyvolávat výjimky V takovém případě není třeba datové položky vyplňovat Objekt nebude považován za platný a nebude používán ani destruován Obecně je však třeba ošetřit try-blokem situace, kdy je v objektu více dynamicky alokovaných ukazatelů String( const String & b) { if ( b.str_ ) str_ = new char[ strlen( b.str_) + 1]; strcpy( str_, b.str_); } else throw InvalidString();
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Slabě bezpečná implementace: Při výjimce v konstruktoru proměnné s se nestane nic operator delete nezpůsobuje výjimky struct Box { String v; Box * next; }; class StringStack { public: // ... private: Box * top_; }; String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); Box * p = top_; String s = p->v; top_ = p->next; delete p; return s; }
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Slabě bezpečná implementace Není silně bezpečná: Funkce vrací hodnotou Pokud při vracení dojde k výjimce v copy-constructoru, zásobník již bude zkrácen struct Box { String v; Box * next; }; class StringStack { public: // ... private: Box * top_; }; String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); Box * p = top_; String s = p->v; top_ = p->next; delete p; return s; }
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Slabě bezpečná implementace Není silně bezpečná: Funkce vrací hodnotou Pokud při vracení dojde k výjimce v copy-constructoru, zásobník již bude zkrácen Tuto výjimku lze ošetřit try-blokem okolo příkazu return Uvést zásobník do původního stavu Ale: co když se uvedení do původního stavu nezdaří? String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); Box * p = top_; String s = p->v; top_ = p->next; delete p; try { return s; } catch ( ...) p = new Box; p->v = s; p->next = top_; top_ = p; throw;
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Nefunkční implementace Není silně bezpečná: Funkce vrací hodnotou Pokud při vracení dojde k výjimce v copy-constructoru, zásobník již bude zkrácen Tuto výjimku lze ošetřit try-blokem okolo příkazu return Dokážeme udělat obnovení původního stavu bez nebezpečí výjimky Ale: jak zrušíme proměnnou p, když k výjimce nedojde? String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); Box * p = top_; String s = p->v; top_ = p->next; // tady bylo delete p; try { return s; // tady by delete p; nepomohlo } catch ( ...) top_ = p; throw;
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Silně bezpečná implementace Jak zrušíme proměnnou p, když k výjimce nedojde? std::auto_ptr< T> "chytrý" ukazatel na T, který se chová jako "jediný vlastník objektu": po zkopírování se vynuluje při zániku volá delete Pozor: auto_ptr má nestandardní copy-constructor a operator= modifikují svůj parametr pro auto_ptr nefungují kontejnery apod. #include <memory> String StringStack::pop() { if ( ! top_ ) throw StackEmpty(); std::auto_ptr< Box> p = top_; top_ = p->next; try { return p->v; } catch ( ...) top_ = p; // toto přiřazení nuluje p throw; // při návratu se automaticky zruší * p // pokud je p nenulové
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Silně bezpečná implementace Uživatel ji nedokáže použít tak, aby to bylo silně bezpečné Vracenou hodnotu je nutné okopírovat Nedá se poznat, zda výjimku vyvolala metoda pop nebo operator= V prvním případě je zásobník nedotčen, ale ve druhém je již zkrácen StringStack stk; String a; /* ... */ try { a = stk.pop(); } catch (...) { /* ??? */
Exception-safe programming Poučení Funkce, která má vedlejší efekty, smí vracet hodnotou pouze typy, jejichž kopírování nevyvolává výjimky a = stk.pop(); Pokud je třeba nějakou "nebezpečnou" hodnotu vracet, musí se předávat jako výstupní parametr stk.pop( a); Funkcí bez vedlejších efektů se problém netýká a = b + c; StringStack stk; String a; /* ... */ try { a = stk.pop(); } catch (...) { /* ??? */ Většiny operátorů se problém netýká: buď nemají vedlejší efekty, nebo vracejí trvale existující objekt odkazem Problematické jsou postfixové ++, --
Exception-safe programming Použití Nebezpečí nastává všude, kde je změna pozorovatelného stavu následovaná voláním funkce (operátoru), které může vyvolat výjimku data_ = ...; f(...); Problém lze někdy odstranit obrácením pořadí Pomocná proměnná není součástí pozorovatelného stavu T tmp = ...; Pokud pořadí nelze vyměnit, je v případě výjimky nutný rollback T old = data_; data_ = ...; try { f(...); } catch(...) { data_ = old; Problém 1: Některé operace rollbackovat nelze Problém 2: Rollback nemusí být spolehlivý
Exception-safe programming Typické použití Nebezpečný kód change_1(...); change_2(...); change_3(...); change_4(...); change_1(...); try { change_2(...); change_3(...); change_4(...); } catch(...) { rollback_change_3(...); rollback_change_2(...); rollback_change_1(...);
Exception-safe programming Použití Operace, která rollbackovat nejde, musí být zařazena jako poslední Pokud to není možné, je třeba rollback simulovat dodatečnými daty f1_.get( tmp); } try { f2_.put( tmp); catch(...) { f1_.unget( tmp); // ??? if ( additional_data_valid_ ) { tmp = additional_data_; additional_data_valid_ = false; } else f1_.get( tmp); try { f2_.put( tmp); catch(...) { additional_data_ = tmp; additional_data_valid_ = true;
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Řešení A Jako v STL Rozdělit pop na dvě funkce top vrací vrchol zásobníku může jej vracet odkazem nemodifikuje data pop pouze zkracuje je silně bezpečná StringStack stk; String a; /* ... */ try { a = stk.top(); } catch (...) { /* chyba kopírování nebo prázdný zásobník, proměnná a nezměněna, zásobník nedotčen */ stk.pop(); /* chyba zkracování, proměnná a změněna,
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Řešení B Namísto vracení hodnoty funkce pop vyplňuje parametr předávaný odkazem tím se vyloučí nutnost kombinovat volání pop s dalším kopírováním Pro uživatele jednodušší, implementace pop je však těžší StringStack stk; String a; /* ... */ try { stk.pop( a); } catch (...) { /* chyba zkracování nebo kopírování, proměnná a nezměněna, zásobník nedotčen */
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován seznamem Řešení B Lze implementovat nad řešením A #include <memory> class StringStack { public: /* A */ String & top(); void pop(); /* B */ void pop( String & out) { String & t = top(); swap( out, t); try { pop(); } catch (...) { throw; };
Exception specifications U každé funkce (operátoru, metody) je možno určit seznam výjimek, kterými smí být ukončena Na výjimky ošetřené uvnitř funkce se specifikace nevztahuje Pokud není specifikace uvedena, povoleny jsou všechny výjimky Specifikace respektuje dědičnost, to jest automaticky povoluje i všechny potomky uvedené třídy void a() { /* tahle smí všechno */ } void b() throw () /* tahle nesmí nic */ void c() throw ( std::bad_alloc) /* tahle smí std::bad_alloc */ void d() throw ( std::exception, MyExc) /* tahle smí potomky std::exception a MyExc */
Exception specifications Kompilátor zajistí, že nepovolená výjimka neopustí funkci: Pokud by se tak mělo stát, volá se unexpected() unexpected() smí vyvolat "náhradní" výjimku Pokud ani náhradní výjimka není povolena, zkusí se vyvolat std::bad_exception Pokud ani std::bad_exception není povoleno, volá se terminate() a program končí
Exception specifications Kompilátor zajistí, že nepovolená výjimka neopustí funkci Toto je běhová kontrola Kompilátor smí vydávat nejvýše varování Funkce smí volat jinou, která by mohla vyvolat nepovolenou výjimku (ale nemusí) void f() throw ( std::exception) { } void g() throw () f(); /* tohle se smí */
Exception specifications Kompilátor (a runtime) zajistí, že nepovolená výjimka neopustí funkci Kompilátor to může využít Speciálně při volání funkce s prázdným throw () se nemusí generovat ošetřující kód Program se zmenší a možná i zrychlí Užitek pro programátory: Komentář Ladicí prostředek
Reference vs. ukazatel
Pravidla pro začátečníky Kdy použít referenci: T & Reference a ukazatelé Pravidla pro začátečníky Kdy použít referenci: T & Výstupní parametr Návratová hodnota funkce zpřístupňující objekt T & vector<T>::at(size_t i) Kdy použít konstantní referenci: const T & Obvykle pouze kvůli rychlosti Parametr typu struktura/třída Návratová hodnota funkce zpřístupňující objekt ke čtení const T & vector<T>::at(size_t i) const Kdy použít ukazatel (T *) Je-li objekt dynamicky alokován Je-li nutná schopnost přesměrování, null, nebo aritmetika Nelze-li referenci správně namířit v okamžiku inicializace Kdy použít konstantní ukazatel (const T *) Sekundární odkaz na objekt, schopný pouze čtení
Pravidla pro pokročilejší Vlastník dynamicky alokovaného objektu Reference a ukazatelé Pravidla pro pokročilejší Vlastník dynamicky alokovaného objektu je zodpovědný za jeho zrušení - musí použít ukazatel “T *” nelze-li jednoznačně určit vlastníka, použijte “shared_ptr<T>” Uživatel objektu Pokud je životnost pozorovatele kratší než životnost objektu lze použít referenci – “T &” nebo “const T &” Pokud je životnost delší než životnost objektu nebo jinak komplikovaná je nutné použít ukazatel – “T *” nebo “const T *”
Pravidla pro vracení hodnot odkazem Reference a ukazatelé Pravidla pro vracení hodnot odkazem Pokud hodnota, kterou funkce vrací, existuje v nějakém objektu i po návratu z funkce, lze vrátit odkaz na tento objekt (konstantní) referencí T & vector<T>::back(); const T & vector<T>::back() const; T & T::operator+=(const T & b); T & T::operator++();// prefixová verze ++ vrací novou hodnotu Pokud se hodnota, kterou funkce vrací, nově spočítala a není nikde uložena, funkce musí vracet hodnotou T operator+( const T & a, const T & b); T T::operator++(int);// postfixová verze ++ vrací starou hodnotu
Třída jako datový typ
Konstruktor bez parametrů (default constructor) Speciální metody tříd Konstruktor bez parametrů (default constructor) XXX(); Používán u proměnných bez inicializace Kompilátor se jej pokusí vygenerovat, je-li to třeba a pokud třída nemá vůbec žádný konstruktor: Položky, které nejsou třídami, nejsou generovaným konstruktorem inicializovány Generovaný konstruktor volá konstruktor bez parametrů na všechny předky a položky To nemusí jít např. pro neexistenci takového konstruktoru Kopírovací konstruktor (copy constructor) XXX( const XXX &); Používán pro předávání parametrů a návratových hodnot Kompilátor se jej pokusí vygenerovat, je-li to třeba a pokud třída kopírovací konstruktor nemá: Položky, které nejsou třídami, jsou kopírovány Na předky a položky se volá kopírovací konstruktor To nemusí jít kvůli ochraně přístupu
Operátor přiřazení (assignment operator) Speciální metody tříd Operátor přiřazení (assignment operator) const XXX & operator=( const XXX &); Implementace operátoru = pro typ XXX na levé straně Kompilátor se jej pokusí vygenerovat, je-li to třeba a třída jej nemá: Položky, které nejsou třídami, jsou kopírovány Na předky a položky se volá operátor přiřazení To nemusí jít kvůli ochraně přístupu Destruktor ~XXX(); Používán při zániku objektu Kompilátor se jej pokusí vygenerovat, je-li to třeba a třída jej nemá Pokud je objekt destruován operátorem delete aplikovaným na ukazatel na předka, musí být destruktor v tomto předku deklarován jako virtuální virtual ~XXX();
Konverzní konstruktory Speciální metody tříd Konverzní konstruktory class XXX { XXX( YYY); }; Zobecnění kopírovacího konstruktoru Definuje uživatelskou konverzi typu YYY na XXX Je-li tento speciální efekt nežádoucí, lze jej zrušit: explicit XXX( YYY); Konverzní operátory operator YYY() const; Definuje uživatelskou konverzi typu XXX na YYY Vrací typ YYY hodnotou (tedy s použitím kopírovacího konstruktoru YYY, pokud je YYY třída) Kompilátor vždy použije nejvýše jednu uživatelskou konverzi
V jiné situaci není virtuálnost funkcí užitečná Virtuální funkce class Base { virtual void f() { /* ... */ } }; class Derived : public Base { Mechanismus virtuálních funkcí se uplatní pouze v přítomnosti ukazatelů nebo referencí Base * p = new Derived; p->f(); // volá Derived::f V jiné situaci není virtuálnost funkcí užitečná Derived d; d.f(); // volá Derived::f i kdyby nebyla virtuální Base b = d; // slicing = kopie části objektu b.f(); // volá Base::f ikdyž je virtuální Slicing je specifikum jazyka C++
Názvosloví Abstraktní třída Definice v C++: Třída obsahující alespoň jednu čistě virtuální funkci Běžná definice: Třída, která sama nebude instanciována Představuje rozhraní, které mají z ní odvozené třídy (potomci) implementovat Konkrétní třída Třída, určená k samostatné instanciaci Implementuje rozhraní, předepsané abstraktní třídou, ze které je odvozena
Dědičnost a destruktor class Base { public: virtual ~Base() {} }; class Derived : public Base { virtual ~Derived() { /* ... */ } Base * p = new Derived; delete p; Pokud je objekt destruován operátorem delete aplikovaným na ukazatel na předka, musí být destruktor v tomto předku deklarován jako virtuální Odvozené pravidlo: Každá abstraktní třída má mít virtuální destruktor Je to zadarmo Může se to hodit
Mechanismus dědičnosti v C++ je velmi silný Bývá používán i pro nevhodné účely Ideální použití dědičnosti je pouze toto ISA hierarchie pro plnohodnotné objekty (first-class objects) Živočich-Obratlovec-Savec-Pes-Jezevčík Objekt-Viditelný-Editovatelný-Polygon-Čtverec Vztah interface-implementace Readable-InputFile Writable-OutputFile (Readable+Writable)-IOFile Jiná použití dědičnosti obvykle signalizují chybu v návrhu Výjimky samozřejmě existují (traits...)
Dědičnost ISA hierarchie pro plnohodnotné objekty (first-class objects) C++: Jednoduchá nevirtuální veřejná dědičnost class Derived : public Base Abstraktní třídy někdy obsahují datové položky Vztah interface-implementace C++: Násobná virtuální veřejná dědičnost class Derived : virtual public Base1, virtual public Base2 Abstraktní třídy obvykle neobsahují datové položky Abstraktní třídy nebývají využívány k destrukci objektu Oba přístupy se často kombinují class Derived : public Base, virtual public Interface1, virtual public Interface2
Nesprávné užití dědičnosti class Real { public: double Re; }; class Complex : public Real { public: double Im; }; Porušuje pravidlo "každý potomek má všechny vlastnosti předka" např. pro vlastnost "má nulovou imaginární složku" Důsledek - slicing: double abs( const Real & p) { return p.Re > 0 ? p.Re : - p.Re; } Complex x; double a = abs( x); // tento kód LZE přeložit, a to je špatně Důvod: Referenci na potomka lze přiřadit do reference na předka Complex => Complex & => Real & => const Real &
Nesprávné užití dědičnosti class Real { public: double Re; }; class Complex : public Real { public: double Im; }; Slicing nastává i u předávání hodnotou double abs( Real p) { return p.Re > 0 ? p.Re : - p.Re; } Complex x; double a = abs( x); // tento kód LZE přeložit, a to je špatně Důvod: Předání hodnoty x do parametru p je provedeno implicitně vytvořeným konstruktorem: Real::Real( const Real & y) { Re = y.Re; } Parametr x typu Complex do tohoto konstruktoru lze předat Complex => Complex & => Real & => const Real &
Nesprávné užití dědičnosti class Complex { public: double Re, Im; }; class Real : public Complex { public: Real( double r); }; Vypadá jako korektní specializace: "každé reálné číslo má všechny vlastnosti komplexního čísla" Chyba: Objekty v C++ nejsou hodnoty v matematice Třída Complex má vlastnost "lze do mne přiřadit Complex" Tuto vlastnost třída Real logicky nemá mít, s touto dědičností ji mít bude
Nesprávné užití dědičnosti class Complex { public: double Re, Im; }; class Real : public Complex { public: Real( double r); }; Vypadá jako korektní specializace: "každé reálné číslo má všechny vlastnosti komplexního čísla" Chyba: Objekty v C++ nejsou hodnoty v matematice Třída Complex má vlastnost "lze do mne přiřadit Complex" Tuto vlastnost třída Real logicky nemá mít, s touto dědičností ji mít bude void set_to_i( Complex & p) { p.Re = 0; p.Im = 1; } Real x; set_to_i( x); // tento kód LZE přeložit, a to je špatně Důvod: Referenci na potomka lze přiřadit do reference na předka Real => Real & => Complex &
Nesprávné užití dědičnosti class Complex { public: double Re, Im; }; class Real : public Complex { public: Real( double r); }; Vypadá jako korektní specializace: "každé reálné číslo má všechny vlastnosti komplexního čísla" Chyba: Objekty v C++ nejsou hodnoty v matematice Třída Complex má vlastnost "lze do mne přiřadit Complex" Tuto vlastnost třída Real logicky nemá mít, s touto dědičností ji mít bude Poznámka: při přímem přiřazování tento problém nenastane Complex y; Real x; x = y; // tento kód NELZE přeložit Důvod: operátor = se nedědí Complex & Complex::operator=( const Complex &); // nezdědí se Real & Real::operator=( const Real &); // nesouhlasí typ argumentu
Ideální užití dědičnosti a virtuálních funkcí Abstraktní třída Definuje rozhraní objektu jako množinu předepsaných virtuálních funkcí class GraphicObject { public: virtual ~GraphicObject(); // každá abstraktní třída má mít v.d. virtual void paint() = 0; // čistě virtuální funkce virtual void move( int dx, int dy) = 0; // čistě virtuální funkce };
Příklad: dědičnost a virtuální funkce ISA hierarchie Osoba Student Zaměstnanec Učitel Matikář Fyzikář Chemikář Ředitel Ostatní Školník Kuchař Které abstraktní třídy potřebujeme? Ty, jejichž specifické rozhraní někdo potřebuje Třída „Ostatní“ není potřeba
Příklad: dědičnost a virtuální funkce ISA hierarchie Osoba Student Zaměstnanec Ředitel Školník Kuchař Potřebujeme třídu Osoba? Máme nějaký seznam osob? Další rozhraní Učitel Matikář Fyzikář Chemikář Má být Matikář odvozen z Učitele? Mají Matikář a Fyzikář něco společného?
Nepoužité slajdy PRG032
Šablony tříd - příklad Definice Použití template< int n, class T> class Array { T p[ n]; T dummy; public: T & operator[]( int x) { return x<n ? p[x] : dummy; } }; Použití Array< 5, int> a; Array< 7, int> b; Array< 5 + 2, int> c; Array< 3, Array< 7, int> > d; a[ 3] = b[ 3]; a = b; // chyba !!! b = c; // OK, implicitní copy-constructor d[ 2][ 3] = 1;
Poučení – konverzní operátor operator double() const; Implementuje konverzi Complex => double Není zde vhodný Konverze je ztrátová Může vést k nejednoznačnostem Complex a, b; double c; a = b + c; může znamenat volání Complex::Complex( double re, double im = 0.0); Complex operator+( const Complex & a, const Complex & b); Complex::operator double() const; double double::operator=( double b); // vestavěná operace ale také double operator+( double a, double b); // vestavěná operace Kompilátor si (z definice) neumí vybrat – hlásí chybu Výběr funkce/operátoru je řízen pouze typy argumentů
Poučení – konverzní operátor operator double() const; Implementuje konverzi Complex => double Není zde vhodný Konverze je ztrátová Může vést k nejednoznačnostem Může vést k chybám Complex a, b; a = sin( b); může znamenat volání Complex sin( const Complex & x); // <complexmath.h> ale také Complex::operator double() const; double sin( double x); // <math.h> Complex( double re, double im = 0.0); První varianta má přednost... ...ale když zapomenete #include <complexmath.h> ...
C++
Zobecnění pojmu struktura (struct) Třída a objekt Třída (class) Zobecnění pojmu struktura (struct) Rozdíl mezi class a struct v C++ je nepatrný Užívání class místo struct je pouze konvence Deklarace třídy obsahuje Deklarace datových položek (stejně jako v C) Funkce (metody), virtuální funkce a statické funkce Definice výčtových konstant a typů (včetně vnořených tříd) Objekt (instance třídy) Běhová reprezentace jednoho exempláře třídy Reprezentace objektu v paměti obsahuje Datové položky Skryté pomocné položky umožňující funkci virtuálních metod, výjimek a RTTI virtuální dědičnosti
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Triviální důsledek: class T { /*...*/ }; T * p; // zde nevzniká objekt typu T
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Triviální důsledek: class T { /*...*/ }; T * p; // zde nevzniká objekt typu T Netriviální důsledek: Proměnná typu T je vždy objekt typu T a žádného jiného, přestože do ní lze přiřadit i objekt odvozeného typu class U : public T { /*...*/ }; U y; T x = y; // toto je kopie části objektu y do vznikajícího objektu x
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Triviální důsledek: class T { /*...*/ }; T * p; // zde nevzniká objekt typu T Netriviální důsledek: Proměnná typu T je vždy objekt typu T a žádného jiného, přestože do ní lze přiřadit i objekt odvozeného typu class U : public T { /*...*/ }; U y; T x = y; // toto je kopie části objektu y do vznikajícího objektu x Poznámka pro znalce pravidel: K tomuto přiřazení může dojít díky existenci automaticky vytvořeného copy-constructoru T::T( const T &); a díky možnosti konvertovat odkaz na potomka na odkaz na předka: U => U & => T & => const T &
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Triviální důsledek: class T { /*...*/ }; T * p; // zde nevzniká objekt typu T Netriviální důsledek: Proměnná typu T je vždy objekt typu T a žádného jiného, přestože do ní lze přiřadit i objekt odvozeného typu class U : public T { /*...*/ }; U y; T x = y; // toto je kopie části objektu y do vznikajícího objektu x Poznámka pro znalce implementace: Zde (ani nikde jinde) se nekopírují odkazy na tabulky virtuálních funkcí Proměnná typu T tedy zůstane typem T včetně přiřazení těl virtuálních funkcí Jiné chování by nemělo smysl
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Triviální důsledek: class T { /*...*/ }; T * p; // zde nevzniká objekt typu T Netriviální důsledek: Proměnná typu T je vždy objekt typu T a žádného jiného, přestože do ní lze přiřadit i objekt odvozeného typu class U : public T { /*...*/ }; U y; T x = y; // toto je kopie části objektu y do vznikajícího objektu x V tomto odlišování se C++ liší od většiny jazyků s objekty (Java, JavaScript, PHP, VisualBasic, ...)
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Nefunguje naivní implementace polymorfního typu: class Variant { enum { REAL, COMPLEX } t; }; class Real : public Variant { public: double Re; }; class Complex : public Variant { public: double Re, Im; }; Variant max( Variant a, Variant b); Real x, y, z = max( x, y); // nelze přeložit Complex u, v, w = max( u, v); // nelze přeložit
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Nefunguje naivní implementace polymorfního typu: class Variant { enum { REAL, COMPLEX } t; }; class Real : public Variant { public: double Re; }; class Complex : public Variant { public: double Re, Im; }; Variant max( Variant a, Variant b); Real x, y, z = max( x, y); // nelze přeložit Complex u, v, w = max( u, v); // nelze přeložit Parametry a, b nedokážou přenést atributy Re, Im Návratovou hodnotu nelze (ani explicitně) přetypovat na potomka Real x, y, z = (Real)max( x, y); // nelze přeložit Complex u, v, w = (Complex)max( u, v); // nelze přeložit I kdyby to šlo, typ Variant vracený hodnotou nedokáže přenést atributy Re, Im
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Nefunguje naivní implementace polymorfního typu: class Variant { enum { REAL, COMPLEX } t; }; class Real : public Variant { public: double Re; }; class Complex : public Variant { public: double Re, Im; }; V tomto případě lze tento problém řešit referencemi: Variant & max( Variant & a, Variant & b); vyžaduje ovšem explicitní přetypování, které je nebezpečné Real x, y, z = (Real &)max( x, y); // funguje Complex u, v, w = (Complex &)max( u, v); // funguje Vracení referencí ovšem funguje pouze pro funkce max a min
Objekt a ukazatel na objekt C++ důsledně odlišuje objekt a ukazatel na něj Nefunguje naivní implementace polymorfního typu: class Variant { enum { REAL, COMPLEX } t; }; class Real : public Variant { public: double Re; }; class Complex : public Variant { public: double Re, Im; }; V tomto případě lze tento problém řešit referencemi: Variant & max( Variant & a, Variant & b); vyžaduje ovšem explicitní přetypování, které je nebezpečné Real x, y, z = (Real &)max( x, y); // funguje Complex u, v, w = (Complex &)max( u, v); // funguje Vracení referencí ovšem funguje pouze pro funkce max a min Tyto funkce mají speciální vlastnost: vrací jeden ze svých parametrů
Různé pohledy na třídy a různá pojmenování Třídy v C++ Konstrukce class, dědičnost a virtuální funkce jsou silný mechanismus, užívaný k různým účelům Různé pohledy na třídy a různá pojmenování Abstraktní a konkrétní třídy Třídy jako datové typy Kontejnery (třídy logicky obsahující jiné objekty) Singletony (jednou instanciované třídy) Traits (neinstanciované třídy) Různé účely dědičnosti Rozšíření požadovaného rozhraní Implementace požadovaného rozhraní Rozšíření implementované funkčnosti Využití k implementaci
Nesprávné užití dědičnosti class Real { public: double Re; }; class Complex : public Real { public: double Im; }; Vypadá jako reusabilita kódu
Nesprávné užití dědičnosti class Real { public: double Re; }; class Complex : public Real { public: double Im; }; Vypadá jako reusabilita kódu Porušuje pravidlo "každý potomek má všechny vlastnosti předka" např. pro vlastnost "má nulovou imaginární složku"
Nesprávné užití dědičnosti class Real { public: double Re; }; class Complex : public Real { public: double Im; }; Vypadá jako reusabilita kódu Porušuje pravidlo "každý potomek má všechny vlastnosti předka" např. pro vlastnost "má nulovou imaginární složku" Důsledek - slicing: double abs( const Real & p) { return p.Re > 0 ? p.Re : - p.Re; } Complex x; double a = abs( x); // tento kód LZE přeložit, a to je špatně Důvod: Referenci na potomka lze přiřadit do reference na předka Complex => Complex & => Real & => const Real &
Nesprávné užití dědičnosti class Real { public: double Re; }; class Complex : public Real { public: double Im; }; Slicing nastává i u předávání hodnotou double abs( Real p) { return p.Re > 0 ? p.Re : - p.Re; } Complex x; double a = abs( x); // tento kód LZE přeložit, a to je špatně Důvod: Předání hodnoty x do parametru p je provedeno implicitně vytvořeným konstruktorem: Real::Real( const Real & y) { Re = y.Re; } Parametr x typu Complex do tohoto konstruktoru lze předat Complex => Complex & => Real & => const Real &
Nesprávné užití dědičnosti class Complex { public: double Re, Im; }; class Real : public Complex { public: Real( double r); }; Vypadá jako korektní specializace: "každé reálné číslo má všechny vlastnosti komplexního čísla"
Nesprávné užití dědičnosti class Complex { public: double Re, Im; }; class Real : public Complex { public: Real( double r); }; Vypadá jako korektní specializace: "každé reálné číslo má všechny vlastnosti komplexního čísla" Chyba: Objekty v C++ nejsou hodnoty v matematice Třída Complex má vlastnost "lze do mne přiřadit Complex" Tuto vlastnost třída Real logicky nemá mít, s touto dědičností ji mít bude
Nesprávné užití dědičnosti class Complex { public: double Re, Im; }; class Real : public Complex { public: Real( double r); }; Vypadá jako korektní specializace: "každé reálné číslo má všechny vlastnosti komplexního čísla" Chyba: Objekty v C++ nejsou hodnoty v matematice Třída Complex má vlastnost "lze do mne přiřadit Complex" Tuto vlastnost třída Real logicky nemá mít, s touto dědičností ji mít bude void set_to_i( Complex & p) { p.Re = 0; p.Im = 1; } Real x; set_to_i( x); // tento kód LZE přeložit, a to je špatně Důvod: Referenci na potomka lze přiřadit do reference na předka Real => Real & => Complex &
Nesprávné užití dědičnosti class Complex { public: double Re, Im; }; class Real : public Complex { public: Real( double r); }; Vypadá jako korektní specializace: "každé reálné číslo má všechny vlastnosti komplexního čísla" Chyba: Objekty v C++ nejsou hodnoty v matematice Třída Complex má vlastnost "lze do mne přiřadit Complex" Tuto vlastnost třída Real logicky nemá mít, s touto dědičností ji mít bude Poznámka: při přímem přiřazování tento problém nenastane Complex y; Real x; x = y; // tento kód NELZE přeložit Důvod: operátor = se nedědí Complex & Complex::operator=( const Complex &); // nezdědí se Real & Real::operator=( const Real &); // nesouhlasí typ argumentu
Třídy sloužící jako datové typy Proměnné typu T Třídy v C++ Třídy sloužící jako datové typy Proměnné typu T Časté kopírování, vracení hodnotou Přiřazení bývá jediný způsob změny stavu objektu Dědičnost nemá smysl Bez virtuálních funkcí Třídy reprezentující „živé“ objekty Proměnné typu T *, případně T & Objekty alokovány dynamicky Kopírování nemívá smysl Metody měnící stav objektu Většinou s dědičností a virtuálními funkcemi
Ideální užití dědičnosti a virtuálních funkcí Abstraktní třída Definuje rozhraní objektu jako množinu předepsaných virtuálních funkcí Abstraktní třídy se mohou dědit Dědičnost jako rozšiřování předepsaného rozhraní class ClickableObject : public GraphicObject { public: virtual void click( int x, int y) = 0; // čistě virtuální funkce };
Ideální užití dědičnosti a virtuálních funkcí Abstraktní třída Definuje rozhraní objektu jako množinu předepsaných virtuálních funkcí Konkrétní třída Implementuje předepsané virtuální funkce Je potomkem abstraktní třídy Dědičnost jako vztah rozhraní-implementace
Ideální užití dědičnosti a virtuálních funkcí Abstraktní třída Definuje rozhraní objektu jako množinu předepsaných virtuálních funkcí Konkrétní třída Implementuje předepsané virtuální funkce class Button : public ClickableObject { public: Button( int x, int y, const char * text); protected: virtual void paint(); virtual void move( int dx, int dy); virtual void click( int x, int y); private: int x_, y_; char * text_; };
Ideální užití dědičnosti a virtuálních funkcí Abstraktní třída Definuje rozhraní objektu jako množinu předepsaných virtuálních funkcí Konkrétní třída Implementuje předepsané virtuální funkce Polotovar třídy Mezi abstraktní a konkrétní třídou - třída implementující část předepsaných virtuálních funkcí Jejím potomkem je konkrétní třída nebo jiný polotovar Dědičnost jako reusabilita kódu
Ideální užití dědičnosti a virtuálních funkcí Polotovar třídy Mezi abstraktní a konkrétní třídou - třída implementující část předepsaných virtuálních funkcí class PositionedObject : public ClickableObject { public: PositionedObject( int x, int y); protected: int get_x() const { return x_; } int get_y() const { return y_; } virtual void move( int dx, int dy); private: int x_, y_; };
Ideální užití dědičnosti a virtuálních funkcí Konkrétní třída Implementuje předepsané virtuální funkce Konkrétní třídy mohou mít potomky - jiné konkrétní třídy se změněnými vlastnostmi (redefinovanými virtuálními funkcemi) Dědičnost jako reusabilita kódu se změnou chování class IconButton : public Button { public: IconButton( int x, int y, const char * text, BitMap icon); protected: virtual void paint(); private: BitMap icon_; };
Ideální užití dědičnosti a virtuálních funkcí Různé významy dědičnosti Rozšiřování předepsaného rozhraní GraphicObject => ClickableObject Vztah rozhraní-implementace ClickableObject => PositionedObject, Button Reusabilita kódu PositionedObject => Button Reusabilita se změnou chování (overriding) Button => IconButton A to není zdaleka všechno... C++ pro odlišné účely využívá tytéž mechanismy Některé jazyky tyto účely rozlišují (Java)
Ideální užití dědičnosti a virtuálních funkcí Ideální abstraktní třída Pouze čistě virtuální funkce Žádná data, žádná těla funkcí Někdy (nesprávně) nazývána protokol Pojem Protokol většinou znamená seznam funkcí a pravidla pro pořadí jejich volání, což C++ nedovede vyjádřit
Ideální užití dědičnosti a virtuálních funkcí Ideální abstraktní třída Pouze čistě virtuální funkce Žádná data, žádná těla funkcí Někdy (nesprávně) nazývána protokol Pojem Protokol většinou znamená množinu funkcí a pravidla pro pořadí jejich volání, což C++ nedovede vyjádřit Takové množiny funkcí má smysl kombinovat Významem je sjednocení množin schopností Příklad: Fyzikář+Matikář
Ideální užití dědičnosti a virtuálních funkcí Ideální abstraktní třída Pouze čistě virtuální funkce Žádná data, žádná těla funkcí Někdy (nesprávně) nazývána protokol Pojem Protokol většinou znamená množinu funkcí a pravidla pro pořadí jejich volání, což C++ nedovede vyjádřit Takové množiny funkcí má smysl kombinovat Významem je sjednocení množin schopností Příklad: Fyzikář+Matikář V C++: násobná dědičnost Obvykle musí být virtuální, aby odpovídala sjednocení: Fyzikář = Pedagogika + Fyzika Matikář = Pedagogika + Matematika Fyzikář+Matikář nemá mít dvě rozhraní pro Pedagogiku
Ideální užití dědičnosti a virtuálních funkcí Podmínka užitečnosti virtuálních funkcí Funkce musí být volána na objektu, jehož skutečný typ není v době kompilace znám Nesmí to být proměnná typu třída Musí to být ukazatel nebo reference na třídu
Ideální užití dědičnosti a virtuálních funkcí Typické použití virtuálních funkcí Polymorfní datová struktura Datová struktura (pole, seznam, strom, ...) obsahující objekty různých typů Tyto objekty musí být vázány odkazem Typicky bývají samostatně dynamicky alokovány Na objektech se obvykle vykonávají hromadné abstraktní operace (např. vykreslení) Volání čistě virtuální funkce (paint) na každém objektu
Ideální užití dědičnosti a virtuálních funkcí Typické použití virtuálních funkcí Polymorfní datová struktura class Scene { public: void insert( GraphicObject * go); void paintAll() const; private: enum { MAX_ = 30 }; GraphicObject * objects_[ MAX_]; int n_; }; primitivní řešení: pole ukazatelů pole objektů by nefungovalo (ani nešlo přeložit) void Scene::paintAll() { for ( int i = 0; i < n_; i++) objects_[ i]->paint(); }
Ideální užití dědičnosti a virtuálních funkcí Typické použití virtuálních funkcí Polymorfní datová struktura struct Item { GraphicObject * go; Item * next; }; class Scene { public: void insert( GraphicObject * go); void paintAll() const; private: Item * first_; }; lepší řešení: spojový seznam ukazatelů Item nemůže přímo obsahovat GraphicObject void Scene::paintAll() { for ( Item * p = first_; p; p = p->next) p->go->paint(); }
Ideální užití dědičnosti a virtuálních funkcí Typické použití virtuálních funkcí Polymorfní datová struktura jiné lepší řešení: intrusivní spojový seznam samotný GraphicObject slouží jako prvek spojového seznamu nevýhoda: abstraktní rozhraní zároveň obsahuje implementaci class GraphicObject { /* ... čistě virtuální funkce ... */ private: friend class Scene; Item * next_; }; class Scene { public: void insert( GraphicObject * go); void paintAll() const; GraphicObject * first_;
Ideální užití dědičnosti a virtuálních funkcí Typické použití virtuálních funkcí Polymorfní datová struktura moderní řešení: STL kontejnery z STL vždy obsahují kopie vkládaných objektů std::list< GraphicObject> by nešlo ani přeložit kontejner je tedy třeba aplikovat na ukazatel, podobně jako pole class Scene { public: void insert( GraphicObject * go); void paintAll() const; private: std::list< GraphicObject *> objects_; };
Ukázková implementace č. 1 (nepoužitelná) String Ukázková implementace č. 1 (nepoužitelná)
String č. 1 - Nepoužitelné řešení class String { public: String() { _str[ 0] = 0; } String( const char * s) { strcpy( _str, s); } const char * c_str() const { return _str; } friend String operator+( const String & a, const String & b); private: enum { MAX = 256 }; char _str[ MAX]; }; String operator+( const String & a, const String & b) { String c; strcpy( c._str, a._str); strcat( c._str, b._str); return c; }
Poučení - Konverzní operátor Metoda c_str by mohla být nahrazena konverzním operátorem operator const char *() const; V tomto případě to není vhodné: String x; if ( x > "A" ) Může být převedeno na String > String (pokud je definován takový operátor) ale také na const char * > const char * Porovnává adresy ! printf( "%s", x); Zde k žádné konverzi nedojde ! Program bude pravděpodobně ukončen pro pokus o nedovolenou operaci
Ukázková implementace č. 2 (naivní, neefektivní) String Ukázková implementace č. 2 (naivní, neefektivní)
String č. 2 - Naivní řešení class String { public: String(); String( const String &); const String & operator=( const String &); ~String(); String( const char *); const char * c_str() const; String cat( const String &) const; private: char * _str; }; inline String operator+( const String & a, const String & b) { return a.cat( b); }
Poučení - konstruktory Ve třídě String jsou odkazy na data uložená jinde Chování kompilátorem vytvořených metod nevyhovuje: String(); Nedělá nic - je třeba naalokovat prázdný string String( const String &); Kopíruje ukazatel - je třeba kopírovat data, na která ukazuje String & operator=( const String &); Totéž, navíc je nutné před přiřazením uklidit ~String(); Nedělá nic - je třeba uklidit Tyto metody je tedy třeba napsat vlastní
Poučení - konstruktory Operátor přiřazení by měl udělat toto: Zrušit starý obsah levé strany Totéž co destruktor Okopírovat pravou stranu Totéž co konstruktor Vrátit novou hodnotu levé strany To lze vrátit odkazem Pozor - v některých případech to fungovat nebude: String a = "ha"; a = a; Při kopírovaní pravá strana už nebude existovat
Poučení - konstruktory Vzorový operátor přiřazení: String & String::operator=( const String & b) { if ( this != & b ) { clean(); fill( b); } return * this; Konstruktory nelze přímo volat, u destruktoru to není rozumné String::String( const String & b) String::~String() clean();
Poučení - konstruktory Lepší řešení operátoru přiřazení: #include <algorithm> void String::swap( String & b) { std::swap( _str, b._str); } String & String::operator=( const String & b) String tmp( b); swap( tmp); return * this; Toto řešení je navíc exception-safe (později...) Metodu swap je vhodné publikovat takto void swap( String & a, String & b) { a.swap( b); } Kdyby někdo udělal třídu obsahující náš String...
Ukázková implementace č. 3 (counted-pointers) String Ukázková implementace č. 3 (counted-pointers)
String č. 3 - Counted-pointers class StringBody; class String { public: String(); String( const String &); const String & operator=( const String &); ~String(); String( const char *); operator const char *() const; String cat( const String &) const; private: String( StringBody *); StringBody * _body; };
String č. 3 - Counted-pointers class StringBody { friend class String; private: StringBody(); StringBody( int); ~StringBody(); void inc() { _count++; }; void dec(); char * buffer() { return _str; }; char * _str; int _count; static StringBody empty; };
String č. 3 - Counted-pointers Kopie a destrukce String::String( const String & b) { (_body = b._body)->inc(); } const String & String::operator=( const String & b) if ( _body != b._body ) _body->dec(); return * this; String::~String()
String č. 3 - Counted-pointers Destrukce těla void StringBody::dec() { if ( ! --count ) delete this; }; StringBody::~StringBody() delete[] _str; }
String č. 3 - Counted-pointers Vytvoření neprázdného těla StringBody::StringBody( int l) { _count = 0; _str = new char[ l]; } String::String( StringBody * b) (_body = b)->inc(); String::String( const char * s) _body = new StringBody( strlen( s) + 1); _body->inc(); strcpy( _body->_str, s);
String č. 3 - Counted-pointers Speciální implementace prázdného řetězce StringBody::StringBody() { _str = ""; _count = 1; } StringBody StringBody::empty; String::String() (_body = &StringBody::empty)->inc();
String operator [ ]
String č. 4 - operator [ ] - čtení class String { public: char operator[]( int pos) const { if ( pos < 0 || pos >= _body->length() ) return 0; return _body->buffer()[ _pos]; }; /* ... */
String č. 4 - operator [ ] - zápis class String { public: char & operator[]( int pos) { if ( pos < 0 || pos >= _body->length() ) return _dummy; return _body->buffer()[ _pos]; }; private: static char _dummy; /* ... */ char String::_dummy = 0;
String č. 4 - operator [ ] - zápis class String { public: char & operator[]( int pos) { if ( pos < 0 || pos >= _body->length() ) return _dummy; return _body->buffer()[ _pos]; // chyba: _body je sdíleno ! }; private: static char _dummy; /* ... */ char String::_dummy = 0;
String č. 4 - operator [ ] - zápis do privátní kopie class String { public: char & operator[]( int pos) { /* ... */ make_private(); return _body->buffer()[ _pos]; }; private: void make_private() if ( _body->count > 1 ) StringBody * nb = new StringBody( * _body); // copy-constructor _body->dec(); _body = nb; _body->inc(); }
String č. 4 - operator [ ] - čtení a zápis class String { public: char operator[]( int pos) const { /* ... */ return _body->buffer()[ _pos]; }; char & operator[]( int pos) make_private();
String č. 4 - operator [ ] - čtení a zápis class String { public: char operator[]( int pos) const { /* ... */ return _body->buffer()[ _pos]; }; char & operator[]( int pos) make_private(); String a, b; char c; const String d; a[ 1] = c; // char & operator[]( int) c = d[ 1]; // char operator[]( int) const c = b[ 1]; // char & operator[]( int) - vytváří privátní kopii
String č. 4 - operator [ ] – rozlišení čtení a zápisu class StringPos; class String { public: /* ... */ char read_pos( int pos) const { /* ... */ } void write_pos( int pos, char b) { /* ... */ } char operator[]( int pos) const { return read_pos( pos); } StringPos operator[]( int pos) { return StringPos( this, pos); } }; class StringPos { StringPos( String * t, int p) : t_( t), p_( p) {} operator char() const { return t_->read_pos( p_); } const StringPos & operator =( char b) const { t_->write_pos( p_, b); return * this; } private: String * t_; int p_;
Standardní knihovny C++
namespace Konstrukce namespace umožňuje uzavřít několik deklarací typů, tříd, globálních proměnných a funkcí do zvláštního prostoru jmen Konstrukci namespace lze otevírat vícenásobně namespace X { typedef char * ptr; ptr f( ptr a, ptr b); }; ptr g(); Uzavřené identifikátory lze mimo tento prostor referencovat kvalifikovaným jménem X::ptr v = X::g(); Celý prostor jmen lze rozbalit do aktuálního bloku nebo modulu konstrukcí: using namespace X;
Standardní knihovny C++ V novějších implementacích má většina hlavičkových souborů dvě verze Stará konvence – v budoucnu nebude podporována soubor vector.h obsahuje šablonu vector Nová konvence soubor vector obsahuje šablonu vector uzavřenou do namespace std je tedy nutné používat identifikátor std::vector Standardní knihovny C++ mají tyto hlavní součásti Základní knihovny převzaté z C, podle nové konvence v přejmenovaných souborech Rozšířené C++ knihovny iostream: Systém znakového a formátovaného vstupu a výstupu STL: Standard Template Library
Základní knihovny C a C++ <assert.h> <cassert> - ladicí funkce (makro assert) <ctype.h> <cctype> - klasifikace znaků (isalpha, isspace, ...) <errno.h> <cerrno> - chybové kódy (ENOMEM, ...), proměnná errno <float.h> <cfloat> - vlastnosti a limity reálných typů (DBL_MAX, ...) <limits.h> <limits> <climits> - limity celočíselných typů (INT_MAX, ...) <locale.h> <locale> <clocale> - přizpůsobení národnímu prostředí <math.h> <cmath> - matematické funkce (sin, ...) <setjmp.h> <csetjmp> - meziprocedurální skoky (setjmp, longjmp) <signal.h> <csignal> - signály operačního systému <stdarg.h> <cstdarg> - makra pro funkce s proměnným počtem argumentů <stddef.h> <cstddef> - užitečné typy a konstanty (NULL) <stdio.h> <cstdio> - standardní a souborový vstup a výstup <stdlib.h> <cstdlib> - užitečné funkce (malloc, ...) <string.h> <cstring> - manipulace s řetězci (strcpy, ...) <time.h> <ctime> - konverze data a času <wchar.h> <cwchar> - 16-bitové řetězce (wchar_t) <wctype.h> <cwctype> - klasifikace 16-bitových znaků
Rozšířené knihovny pro C++ <bitset.h> <bitset> -- šablona pro bitové pole (bitset<N>) <complex.h> <complex> - komplexní čísla různé přesnosti (complex<double>,...) <exception.h> <exception> - nastavení zpracování výjimek (set_unexpected,...) <stdexcept.h> <stdexcept> - standardní výjimky (overflow_error,...) <valarray.h> <valarray> - šablony různě inteligentních polí (valarray,...), vektorové operace (podpora paralelních výpočtů matematických funkcí)
STL – Ostatní <algorithm.h> <algorithm> - užitečné algoritmy (for_each, sort, next_permutation, ...) <functional.h> <functional> - podpora funktorů <iterator.h> <iterator> - podpora iterátorů <memory.h> <memory> - alokátory pro kontejnery <numeric.h> <numeric> - jednoduchá matematika na prvcích kontejnerů <utility.h> <utility> - pomocné konstrukce (pair,...)
vstupní a výstupní proudy iostream vstupní a výstupní proudy
iostream Manipulátory hex - šestnáctkový výpis, setw - počet míst #include <iostream> #include <iomanip> using namespace std; f() { int X; double Y; cin >> X >> Y; cout << "X = " << hex << setw(4) << X << ", Y = " << Y << endl; } Manipulátory hex - šestnáctkový výpis, setw - počet míst platí pro daný stream (cout) trvale (do další změny) endl - vloží oddělovač řádek
<sstream> - *stringstream – spolupracuje se std::string #include <string> #include <sstream> #include <iomanip> using namespace std; string f( int a) { ostringstream x; x << "a = " << a; return x.str(); } <sstream> - *stringstream – spolupracuje se std::string <strstream> - *strstream – spolupracuje s char *
iostream Hlavičkové soubory <fstream.h> <fstream> - souborový vstup a výstup (ifstream, ofstream, fstream) <iomanip.h> <iomanip> - manipulátory pro nastavení parametrů formátovaného vstupu a výstupu (setw, setprecision, setfill, setbase, ...) <ios.h> <ios> - základní funkce abstraktního souboru, základní nastavení formátu (hex, left, ...) <iostream.h> <iostream> - standardní vstup a výstup (cin, cout, cerr) <istream.h> <istream> - abstraktní vstupní a kombinované médium (istream, iostream) <ostream.h> <ostream> - abstraktní výstupní médium (ostream) <sstream.h> <sstream> - vnitřní paměť jako médium (istringstream, ostringstream, stringstream)
basic_...<T> jsou šablony iostream Abstraktní rozhraní basic_...<T> jsou šablony T = char - 8-bitové znakové sady typedef: ios, istream, ostream, iostream, streambuf T = wchar_t - 16-bitové znakové sady typedef: wios, wistream, wostream, wiostream, wstreambuf dědičnost virtuální dědičnost nastavení formátovače ios_base ukazatel stav média basic_ios<T> basic_streambuf<T> virtuální funkce čtení a zápisu basic_istream<T> basic_ostream<T> přímé a formátované čtení přímý a formátovaný zápis basic_iostream<T>
iostream Abstraktní rozhraní funkce, které potřebují zapisovat, dostávají basic_ostream<T> & void zapis_neco( ostream & o) { o << neco; } dědičnost virtuální dědičnost nastavení formátovače ios_base stav média basic_ios<T> basic_ostream<T> přímý a formátovaný zápis
iostream Abstraktní rozhraní funkce, které potřebují číst, dostávají basic_istream<T> & pozor: čtení modifikuje stream (posunuje ukazovátko) void cti_neco( istream & i) { i >> neco; } dědičnost virtuální dědičnost nastavení formátovače ios_base ukazatel stav média basic_ios<T> basic_istream<T> přímé a formátované čtení
iostream Abstraktní rozhraní funkce, které potřebují číst i zapisovat totéž médium, dostávají basic_iostream<T> & ukazovátko čtení a zápisu NENÍ společné void zmen_neco( iostream & io) { io.read( neco, N); io.write( neco2, N); } dědičnost virtuální dědičnost nastavení formátovače ios_base ukazatel stav média basic_ios<T> basic_istream<T> basic_ostream<T> přímé a formátované čtení přímý a formátovaný zápis basic_iostream<T>
iostream Abstraktní rozhraní basic_iostream< T> obsahuje funkce čtení i zápisu nastavení formátu i stav média jsou společné pro čtení i zápis basic_ios< T> zde musí být pouze v jedné instanci dědění basic_ios< T> musí být virtuální template< class T> class basic_istream : virtual public basic_ios< T> dědičnost virtuální dědičnost nastavení formátovače ios_base stav média basic_ios<T> basic_istream<T> basic_ostream<T> přímé a formátované čtení přímý a formátovaný zápis basic_iostream<T>
iostream Médium Konkrétní médium je implementováno jako potomek třídy basic_streambuf<T> Standardní knihovna C++ nabízí: soubor (to, co umí OS, tedy včetně rour apod.) uložení v paměti Lze implementovat vlastní (např. výstup do okna) stav formátovače ios_base stav média basic_ios<T> basic_streambuf<T> virtuální funkce čtení a zápisu basic_istream<T> basic_ostream<T> přímé a formátované čtení přímý a formátovaný zápis basic_...buf<T> basic_iostream<T> médium, implementace čtení a zápisu
Paměťové médium <sstream> Obálkové třídy ...stringstream<T> zařídí vznik basic_stringbuf<T> Umožňují přístup k médiu jako basic_string<T> stav formátovače ios_base stav média basic_ios<T> basic_streambuf<T> virtuální funkce čtení a zápisu basic_istream<T> basic_ostream<T> přímé a formátované čtení přímý a formátovaný zápis basic_stringbuf<T> basic_iostream<T> médium, implementace čtení a zápisu basic_istringstream<T> basic_ostringstream<T> str: přístup k médiu str: přístup k médiu basic_stringstream<T> str: přístup k médiu
Souborové médium <fstream>, <iostream> (cin, cout, cerr) Obálkové třídy ...fstream<T> zařídí vznik basic_filebuf<T> Soubor se otevírá(zavírá) metodou open(close) těchto tříd stav formátovače ios_base stav média basic_ios<T> basic_streambuf<T> virtuální funkce čtení a zápisu basic_istream<T> basic_ostream<T> přímé a formátované čtení přímý a formátovaný zápis basic_filebuf<T> basic_iostream<T> médium, implementace čtení a zápisu basic_ifstream<T> basic_ofstream<T> open, close open, close Operační systém basic_fstream<T> open, close
istream/ostream - neformátované čtení/zápis iostream ios: stav média good, eof, fail, bad - metody indikující stavy istream/ostream - neformátované čtení/zápis read/write - čtení/zápis n znaků get/put - čtení/zápis jednotlivých znaků a čtení po řádkách seekg/seekp, tellg/tellp - posun ukazovátka, zjištění pozice funkce ...g manipulují s ukazovátkem pro čtení (get, istream) funkce ...p manipulují s ukazovátkem pro čtení (put, ostream) u některých médií nefunguje (roury)
Oba operátory vrací levý operand iostream formátované čtení basic_istream<T> & operator>>( basic_istream<T> & s, D & x) operátor přečte několik znaků ve tvaru určeném typem D naplní výstupní parametr x formátovaný zápis basic_ostream<T> & operator<<( basic_ostream<T> & s, D x) operátor vypíše x jako několik znaků ve tvaru určeném typem D Oba operátory vrací levý operand Tím je umožněno zřetězené použití s << x << y << z; je ekvivalentní s << x; s << y; s << z;
formátované čtení/zápis iostream formátované čtení/zápis basic_istream<T> & operator>>( basic_istream<T> & s, D & x) basic_ostream<T> & operator<<( basic_ostream<T> & s, D x) Knihovna istream/ostream implementuje operátory pro typy (unsigned) short, (unsigned) int, (unsigned) long - dec, hex, oct float, double, long double - desetinný a/nebo exponenciální tvar bool, void * - pro ladicí účely char/wchar_t - znak char * / wchar_t * - řetězec v C tvaru Další typy lze dodefinovat (jako globální operátory) Standardní knihovny C++ je definují pro string/wstring - řetězec complex - komplexní číslo
Definování vlastních formátovacích operátorů iostream Definování vlastních formátovacích operátorů class Souradnice { public: int x, y; }; std::ostream & operator<<( std::ostream & s, const Souradnice & a) { return s << '[' << a.x << ',' << a.y << ']'; } Použití Souradnice p; std::cout << "p = " << p << std::endl;
Jak fungují manipulátory iostream Jak fungují manipulátory cout << "X = " << hex << setw(4) << X << ", Y = " << Y << endl; Bez parametrů (hex, endl, ...) Definovány jako funkce manipulující s ios_base Proto ios_base musí být třída a nikoliv šablona jako basic_ios ios_base & hex( ios_base & s) { s.setf( ios_base::hex); return s; } Akceptovány jako ukazatel na funkci zvláštní verzí operátoru << basic_ostream<T> & operator<<( basic_ostream<T> & s, ios_base & (* f)( ios_base & s)) { f( s);
Jak fungují manipulátory iostream Jak fungují manipulátory cout << "X = " << hex << setw(4) << X << ", Y = " << Y << endl; S parametry (setw, setprecision, setfill, ...) Definovány jako funkce vracející speciální třídu struct setw_manip { int x; explicit setw_manip( int p) : x( p) {} }; setw_manip setw( int p) { return setw_manip( p); } Akceptovány zvláštními verzemi operátoru << basic_ostream<T> & operator<<( basic_ostream<T> & s, const setw_manip & p) { s.width( p.x); return s;
Vztah proudů a kontejnerů iostream Vztah proudů a kontejnerů stringstream umožňuje přístup k médiu jako string pro string jsou definovány operátory << a >> <iterator> definuje iterátory nad proudy: istream_iterator< U> vstupní iterátor, čte typ U pomocí operátoru >> parametrem konstruktoru je istream & ostream_iterator< U> výstupní iterátor, zapisuje typ U pomocí operátoru << parametrem konstruktoru je ostream &
Iterátory nad proudy - příklady naplnění kontejneru ze vstupu iostream Iterátory nad proudy - příklady naplnění kontejneru ze vstupu std::vector< double> a; std::copy( std::istream_iterator< double>( std::cin), // aktuální pozice std::istream_iterator< double>(), // "konec souboru" std::back_inserter( a)); // vkládací iterátor kontejneru vysypání kontejneru na výstup nevýhoda: neodděluje elementy výstupu std::vector< std::string> b; b.begin(), b.end(), std::ostream_iterator< std::string>( std::cout));
Typová informace za běhu RTTI Typová informace za běhu
RTTI Operátor typeid Identifikace typu Vrací identifikaci typu T typeid(T) Vrací identifikaci typu T typeid(e) Pokud výraz e je typu reference na třídu s alespoň jednou virtuální funkcí Vrací identifikaci typu objektu určeného výrazem e Pokud je reference nulová, vyvolává výjimku std::bad_typeid Jinak Vrací identifikaci statického typu výrazu e Identifikace typu const std::type_info & <typeinfo> Lze porovnávat na rovnost Má metodu name() vracející řetězec s nějakou formou jména typu
Alternativa místo dynamic_cast RTTI Typické použití Alternativa místo dynamic_cast #include <typeinfo> class Base { public: virtual ~Base(); /* alespoň jedna virtuální funkce */ }; class X : public Base { /* ... */ class Y : public Base { /* ... */ Base * p = /* ... */; if ( typeid( * p) == typeid( X) ) { X * xp = static_cast< X *>( p); /* ... */ } if ( typeid( * p) == typeid( Y) ) { Y * yp = static_cast< Y *>( p); /* ... */ }
Alternativa místo dynamic_cast RTTI Typické použití Alternativa místo dynamic_cast Pozor: rovnost typeid nereflektuje dědičnost Zatímco dynamic_cast ano #include <typeinfo> class Base { public: virtual ~Base(); /* alespoň jedna virtuální funkce */ }; class X : public Base { /* ... */ class Z : public X { /* ... */ Base * p = new Z; if ( typeid( * p) == typeid( X) ) // neplatí ! { X * xp = static_cast< X *>( p); /* ... */ } if ( typeid( * p) == typeid( Z) ) // platí { Z * zp = static_cast< Z *>( p); /* ... */ }
RTTI obvykle něco stojí Každá třída s virtuálními funkcemi někde musí mít své type_info a odkaz na něj ve své tabulce virtuálních funkcí To platí i pro instance šablon, jejichž jména bývají dlouhá RTTI se příliš nevyužívá Je slabší než dynamic_cast Je nové Mezitím se programátoři naučili dělat si RTTI vlastními prostředky Většina překladačů zapíná RTTI na vyžádání Někdy se takový přepínač vztahuje i na dynamic_cast U některých překladačů souvisí s RTTI i zpracování výjimek
Visitor Visitor Abstraktní třída Příklad Určena k předání jiné funkci, která vybere a zavolá vhodnou virtuální metodu podle skutečného typu nějakého objektu Často používána ve spojení s procházením polymorfní datovou strukturou Příklad Datová struktura Scene obsahuje objekty tří druhů (Ellipse, Rectangle, Line) Metoda doitforall projde všechny prvky scény a aplikuje na každý z nich vhodnou virtuální metodu zadaného Visitoru class Ellipse; class Rectangle; class Line; class Visitor { public: virtual void visitEllipse( Ellipse *)=0; virtual void visitRectangle( Rectangle *)=0; virtual void visitLine( Line *)=0; virtual ~Visitor() {} }; class Scene { void doitforall( Visitor &); /* ... */
Visitor Visitor - použití class Ellipse; class Rectangle; class Line; class PrintVisitor : public Visitor { public: PrintVisitor( Printer * p) : p_( p) {} virtual void visitEllipse( Ellipse *); virtual void visitRectangle( Rectangle *); virtual void visitLine( Line *); private: Printer * p_; }; Scene s; Printer * p; s.doitforall( PrintVisitor( p)); class Ellipse; class Rectangle; class Line; class Visitor { public: virtual void visitEllipse( Ellipse *)=0; virtual void visitRectangle( Rectangle *)=0; virtual void visitLine( Line *)=0; virtual ~Visitor() {} }; class Scene { void doitforall( Visitor &); /* ... */
Visitor Visitor Visitor umožňuje definovat tolik variant nějaké akce, kolik je druhů procházených objektů Pomocí visitoru je možné definovat další akce Příklad Vyskytne-li se potřeba výstupu na plotter, stačí definovat další potomek visitoru class Ellipse; class Rectangle; class Line; class Visitor { public: virtual void visitEllipse( Ellipse *)=0; virtual void visitRectangle( Rectangle *)=0; virtual void visitLine( Line *)=0; virtual ~Visitor() {} }; class Scene { void doitforall( Visitor &); /* ... */
Visitor Srovnání Virtuální funkce Podobného efektu jako u visitoru lze dosáhnout virtuální funkcí deklarovanou ve společném předku objektů s odlišnými těly pro každý druh objektu Sada virtuálních funkcí ve společném předku není rozšiřitelná class AbstractObject { public: virtual void print( Printer *)=0; virtual void display( Display *)=0; virtual ~AbstractObject() {} }; class Ellipse : public AbstractObject { /* ... */ }; class Ellipse; class Rectangle; class Line; class Visitor { public: virtual void visitEllipse( Ellipse *)=0; virtual void visitRectangle( Rectangle *)=0; virtual void visitLine( Line *)=0; virtual ~Visitor() {} }; class Scene { void doitforall( Visitor &); /* ... */
Visitor Visitor Výhoda: Přidávání další akce nevyžaduje změnu společného rozhraní Nevýhoda: Přidání dalšího druhu objektu si vynutí změnu visitoru (přidání virtuální funkce) Poučení V případě stabilní množiny akcí na nestabilní množině druhů objektů použijte virtuální funkce V případě nestabilní množiny akcí na stabilní množině druhů objektů použijte visitor V případě nestabilní množiny akcí i druhů ... ? class Ellipse; class Rectangle; class Line; class Visitor { public: virtual void visitEllipse( Ellipse *)=0; virtual void visitRectangle( Rectangle *)=0; virtual void visitLine( Line *)=0; virtual ~Visitor() {} }; class Scene { void doitforall( Visitor &); /* ... */
Visitor Visitor - implementace class Ellipse; class Rectangle; class AbstractObject { public: virtual void apply( Visitor &) = 0; virtual ~AbstractObject() {} }; class Ellipse { protected: virtual void apply( Visitor & v) { v.visitEllipse( v); } /* ... */ void Scene::doitforall( Visitor & v) { for ( my_vector_::iterator it = elements_.begin(); it != elements_.end(); ++it ) (*it)->apply( v); } class Ellipse; class Rectangle; class Line; class Visitor { public: virtual void visitEllipse( Ellipse *)=0; virtual void visitRectangle( Rectangle *)=0; virtual void visitLine( Line *)=0; virtual ~Visitor() {} }; class Scene { void doitforall( Visitor &); /* ... */ private: typedef vector< AbstractObject *> my_vector_; my_vector_ elements_;
Design Patterns Návrhové vzory
Design patterns Design patterns Abstract factory Adapter Bridge Builder Chain of responsibility Command Composite Decorator Facade Factory method Flyweight Generation gap Interpreter Iterator Mediator Memento Multicast Observer Prototype Proxy Singleton State Strategy Template method Typed message Visitor a mnoho dalších... Gamma, Helm, Johnson, Vlissides: Design Patterns, 1995
Design patterns Design patterns - proč? Vše již bylo vynalezeno - nevynalézat kolo Moudrost věků - neopakovat chyby Výuka a poučení - jak to dělají jiní Zpřístupnění objektového programování "méně kreativním" programátorům Společná terminologie Ekvivalenty v různých jazycích (C#, Java, CORBA, ...)
Původním záměrem je jejich používání při návrhu Design patterns Design patterns Původním záměrem je jejich používání při návrhu Je to návod, případně vzorová implementace Není to knihovna ani polotovar k úpravě Klasické návrhové vzory nevyužívají C++ šablony Ani "template function" není šablona Některé (ne všechny) vzory lze univerzálně implementovat jako šablony Zkušenost je obsažena především v pravidlech určujících, kdy je vhodné daný vzor použít V originále: "Intent" + "Applicability"
Design patterns Struktura Adapter Bridge Composite Decorator Facade Objekt upravující rozhraní objektu Bridge Oddělení implementace objektu od jeho rozhraní Composite Rozhraní umožňující jednotný přístup k celkům i částem určité hierarchické struktury Decorator Třída podílející se na implementaci objektu Facade Objekt reprezentující část rozhraní objektu Flyweight Objekt snižující objem dat pomocí sdílení Proxy Objekt zpřístupňující jiný objekt se stejným rozhraním
Design patterns Vytváření objektů Abstract factory Builder Vytváření rodin objektů bez určení jejich konkrétních tříd Builder Vytváření složených objektů Factory method Vytváření objektu bez určení konkrétní třídy Prototype Objekt se schopností vytvářet vlastní kopie Singleton Třída s jedinou instancí a globálním přístupem k ní
Design patterns Stav a synchronizace Iterator Mediator Memento Objekt umožňující procházení datovou strukturou Mediator Objekt evidující vztahy jiných objektů Memento Záznam vnitřního stavu objektu určený k pozdějšímu obnovení Observer Synchronizace objektu a jeho pozorovatele State Oddělení funkcí a stavu objektu
Design patterns Zprávy a příkazy Command Interpreter Multicast Zhmotněný pokyn k volání funkce/metody s parametry Interpreter Převodník zpráv Multicast Předávání zpráv dynamicky registrovaným příjemcům Typed message Typově bezpečné předávání zpráv různého druhu Visitor Rozšiřitelná náhrada virtuální funkce
Design patterns Implementace funkcí Chain of responsibility Soustava funkcí podílejících se na implementaci jedné akce Generation gap Úpravy chování neupravitelné třídy Strategy Objekt určený ke specializaci univerzálního postupu Template method Univerzální algoritmus s modifikovatelnými částmi
Šablony Hlubší pohled
***Nepoužité slajdy***
Teoretický pohled na šablony Triky s typovým konstrukcemi Seznam typů template< class H, class R> struct List { typedef H Head; typedef R Rest; }; struct EmptyList {}; Použití typedef List< char *, List< const char *, List< std::string, EmptyList> > > StringTypes;
Teoretický pohled na šablony Triky s typovým konstrukcemi Seznam typů template< class H, class R> struct List { typedef H Head; typedef R Rest; }; struct EmptyList {}; Jiné použití struct Apple {}; struct Pear {}; struct Plum {}; typedef List< Apple, List< Pear, List< Plum, EmptyList> > > Fruits;
Teoretický pohled na šablony Triky s typovým konstrukcemi Seznam typů template< class H, class R> struct List { typedef H Head; typedef R Rest; }; struct EmptyList {}; Funkce na seznamu typů template< class L> struct First { typedef typename L::Head Result;
Teoretický pohled na šablony Triky s typovým konstrukcemi Seznam typů template< class H, class R> struct List { typedef H Head; typedef R Rest; }; struct EmptyList {}; Funkce na seznamu typů template< class L> struct First { typedef typename L::Head Result; struct NullType {}; template<> struct First< EmptyList> { typedef NullType Result;
Teoretický pohled na šablony Triky s typovým konstrukcemi Seznam typů template< class H, class R> struct List { typedef H Head; typedef R Rest; }; struct EmptyList {}; Funkce na seznamu typů template< class L, int n> struct Nth { typedef typename Nth< typename L::Rest, n-1>::Result Result; template< class L> struct Nth< L, 0> { typedef typename L::Head Result;
Teoretický pohled na šablony Triky s typovým konstrukcemi Jiná implementace seznamu typů template< class H, class R> struct List; struct EmptyList; Funkce na seznamu typů template< class L, int n> struct Nth; template< class H, class R, int n> struct Nth< List< H, R>, n> { typedef typename Nth< R, n-1>::Result Result; }; template< class H, class R> struct Nth< List< H, R>, 0> { typedef H Result;
Teoretický pohled na šablony Výpočty při kompilaci Data: celá čísla typy Funkcionální programování: Funkce bez vedlejších efektů Neexistuje přiřazovací příkaz "Proměnné" se nemění Rekurze Odlišného chování funkcí pro různé hodnoty parametrů se dociluje definováním několika těl funkcí (tj. šablon) Výpočty za běhu Data: celá i reálná čísla, struktury ukazatelé Procedurální programování: Procedury s vedlejšími efekty Destruktivní přiřazení Proměnné se mění Podmínky, cykly, rekurze Odlišného chování procedur pro různé hodnoty parametrů se dociluje podmínkami uvnitř
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován polem Slabě bezpečná implementace: Při výjimce v konstruktoru proměnné top se nestane nic Při výjimce v new korektně zanikne proměnná top a zůstane zachován původní stav Funkce swap ani operator delete nezpůsobují výjimky class StringStack { public: // ... private: String * p_; int n_; }; String StringStack::pop() { if ( ! n_ ) return String(); String top = p_[ n_-1]; String * p2 = new String[ n_-1]; for ( int i = 0; i < n_-1; ++i) swap( p2[ i], p_[ i]); swap( p_, p2); --n_; delete p2; return top; }
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován polem Slabě bezpečná implementace Není silně bezpečná: Funkce vrací hodnotou Pokud při vracení dojde k výjimce v copy-constructoru, zásobník již bude zkrácen class StringStack { public: // ... private: String * p_; int n_; }; String StringStack::pop() { if ( ! n_ ) return String(); String top = p_[ n_-1]; String * p2 = new String[ n_-1]; for ( int i = 0; i < n_-1; ++i) swap( p2[ i], p_[ i]); swap( p_, p2); --n_; delete p2; return top; }
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován polem Slabě bezpečná implementace Není silně bezpečná: Funkce vrací hodnotou Pokud při vracení dojde k výjimce v copy-constructoru, zásobník již bude zkrácen Tuto výjimku lze ošetřit try-blokem okolo příkazu return Uvést zásobník do původního stavu Ale: co když se uvedení do původního stavu nezdaří? String StringStack::pop() { if ( ! n_ ) return String(); String top = p_[ n_-1]; String * p2 = new String[ n_-1]; for ( int i = 0; i < n_-1; ++i) swap( p2[ i], p_[ i]); swap( p_, p2); --n_; delete p2; try { return top; } catch ( ...) push( top); throw;
Exception-safe programming Příklad: StringStack::pop Zásobník prvků typu String Implementován polem Nefunkční implementace Není silně bezpečná: Funkce vrací hodnotou Pokud při vracení dojde k výjimce v copy-constructoru, zásobník již bude zkrácen Tuto výjimku lze ošetřit try-blokem okolo příkazu return Dokážeme udělat obnovení původního stavu bez nebezpečí výjimky Ale: jak zrušíme proměnnou p2, když k výjimce nedojde? String StringStack::pop() { if ( ! n_ ) return String(); String top = p_[ n_-1]; String * p2 = new String[ n_-1]; for ( int i = 0; i < n_-1; ++i) swap( p2[ i], p_[ i]); swap( p_, p2); --n_; // tady bylo delete p2; try { return top; } catch ( ...) ++n_; delete p2; throw;
Oblasti platnosti identifikátorů Modul/namespace: Funkce, proměnné, typy a výčtové konstanty Třída: Metody, položky (příp. typy a výčtové konstanty) Funkce (blok): Parametry a lokální proměnné (příp. typy a výčtové konstanty) Pořadí vyhledávání identifikátoru Nejprve ve funkci: Od nejvnitřnějšího k hlavnímu bloku Poté v třídách (uvnitř metod): Od potomků k předkům Nakonec v modulu resp. aktuálním namespace S uplatněním direktiv using
Vztah vyhledávání identifikátoru a kontroly přístupu Ochrana přístupu Vztah vyhledávání identifikátoru a kontroly přístupu V definovaném pořadí se najde první oblast platnosti, ve které se identifikátor vyskytuje Namespace slité direktivami using se považují za jednu oblast platnosti Jedná-li se o přetížený identifikátor funkce, vybere se v nalezené oblasti odpovídající varianta Neuplatní se tudíž jiné varianty v jiných oblastech, které by jinak byly viditelné Aplikuje se mechanismus ochrany přístupových práv. Pokud tedy nejlépe padnoucí varianta není přístupná, kompilátor ohlásí chybu bez ohledu na případnou existenci jiné aplikovatelné a přístupné varianty
Preprocesor a překlad stdio.h hello.o conio.h hello.i hello.c Code ... push call ret Import _printf _getch Export _main Data ‘H’,’e’,’l’,’l’, ’o’,10,0 hello.o hello.i /*...*/ int printf( const char *, ...); int getch(); int putch(); int main( int argc, char * * argv) { printf( “Hello\n”); getch(); return 0; } stdio.h hello.c #include <stdio.h> #include <conio.h> conio.h
Spojování modulů cstart.o printer.o hello.o creader.o exit.o Code ... push call ret Import _printf _getch Export _main Data ‘H’,’e’,’l’,’l’, ’o’,10,0 hello.o _exit entry point cstart.o printer.o creader.o syscall ... exit.o
Spustitelný program a spuštěný proces ... push call ret ‘H’,’e’,’l’,’l’, ’o’,10,0 hello entry point syscall ... Code Data 00000000 Heap Stack Adresový prostor IP R0 R1 FFFFFFFF
***** C++ ***** Identifikátory a kontexty Zapouzdření Dědičnost Přetěžování funkcí Předefinování operátorů Objekty Konstruktory a destruktory Pozdní vazba (late binding) Virtuální funkce Neviditelné konverze Drobná vylepšení Striktnější typová kontrola oproti C new a delete Komentáře Šablony Zpracování výjimek Knihovny Streams
Virtuální funkce – Specializace datové struktury class HashTable { public: void add( const void * key, int keylen, const void * data, int datalen); protected: virtual long hash( int keylen); private: SmartArray _tbl; }; class StringTable : public HashTable { public: void add( const char * key, const void * data, int datalen); protected: virtual long hash( const void * key, int keylen); };
Virtuální funkce – Užití datové struktury class Catalog : private StringTable { public: void add( const char * isbn, const char * author, const char * title); private: virtual long hash( const void * key, int keylen); }; class Catalog { public: void add( const char * isbn, const char * author, const char * title); private: StringTable _t; };
Virtuální funkce - Řešení v C-stylu Abstraktní třída struct File { int handle; int (* readf)( File * p); void (* writef)( File * p, int x); }; int read( File * p) { return p->readf( p); } Specializace int bread( File * p) { /* ... */ } File * bopen() { File * p = new File; p->handle = /*...*/; p->readf = bread; p->writef = bwrite; return p; }
Virtuální funkce - Řešení v C-stylu Jiná specializace struct TFile { File f; int info; }; int tread( File * p) { TFile * tp = (TFile *)p; /* ... */ } File * topen() { TFile * tp = new TFile; tp->f.handle = /* ... */; tp->f.readf = tread; tp->f.writef = twrite; tp->info = /* ... */ return &tp->f;
Virtuální funkce - Řešení v C-stylu Lepší implementace struct VTFile { int (* readf)( File * p); void (* writef)( File * p, int x); }; struct File { int handle; const VTFile * vt; const VTFile VTBFile = { bread, bwrite }; const VTFile VTTFile = { tread, twrite };
Virtuální funkce - Řešení v C++ Abstraktní třída class File { int handle; public: virtual int readf() = 0; virtual void writef( int x) = 0; }; int read( File * p) { return p->readf(); } Specializace class BFile : public File { virtual int readf(); virtual void writef( int x); }; int BFile::readf() { /* ... */ } File * bopen() { BFile * p = new BFile; p->handle = /* ... */; return p;
Virtuální funkce - Řešení v C++ Jiná specializace class TFile : public File { int info; virtual int readf(); virtual void writef( int x); }; int TFile::readf() { /* ... */ } File * topen() { TFile * p = new TFile; p->handle = /* ... */; p->info = /* ... */; return p; }
Statické a dynamické volání virtuálních metod void x() { A a; /* A::A */ B b; /* B::B */ A & raa = a; A & rab = b; B & rbb = b; A * paa = &a; A * pab = &b; B * pbb = &b; a.f(); /* A::f (c) */ b.f(); /* B::f (c) */ raa.f(); /* A::f (r) */ rab.f(); /* B::f (r) */ rbb.f(); /* B::f (r) */ paa->f(); /* A::f (r) */ pab->f(); /* B::f (r) */ pbb->f(); /* B::f (r) */ b.A::f(); /* A::f (c) */ b.B::f(); /* B::f (c) */ paa->A::f(); /* A::f (c) */ pab->A::f(); /* A::f (c) */ pbb->A::f(); /* A::f (c) */ pbb->B::f(); /* B::f (c) */ raa.A::f(); /* A::f (c) */ rab.A::f(); /* A::f (c) */ rbb.A::f(); /* A::f (c) */ rbb.B::f(); /* B::f (c) */ }
String č. 5 - operator [ ] class String { friend class StringPos; public: const char & operator[]( int pos) const { /* test pos */ return _body->buffer()[ _pos]; }; StringPos operator[]( int pos) return StringPos( this, pos); /* ... */
String č. 5 - operator [ ] class StringPos { public: StringPos( String * ref, int pos) { _ref = ref; _pos = pos; }; operator char() const return _ref->_body->buffer()[ _pos]; char operator=( char x) const _ref->make_private(); _ref->_body->buffer()[ _pos] = x; return x; } private: String * _ref; int _pos;
String č. 6 - Držadlo class StringBody; class String { public: String( const String &); const String & operator=( const String &); ~String(); String( const char *); const char * c_str() const; String cat( const String &) const; String cat( const char *) const; String catr( const char *) const; private: String( StringBody *); StringBody * _body; };
String č. 6 - Abstraktní tělo class StringBody { public: virtual ~StringBody() {} void inc() { _count++; }; void dec(); virtual bool private() { return _count == 1; } virtual StringBody * freeze() = 0; virtual StringBody * make_private() = 0; virtual int length() = 0; virtual void copy( char * dst) = 0; virtual const char * c_str() = 0; virtual char read_at( int i) = 0; virtual void write_at( int i, char ch) = 0; protected: StringBody() { _count = 0; } private: int _count; };
String č. 6 - Jednoduché tělo class StringBodySimple : public StringBody { public: static StringBodySimple * create( const char * s); protected: StringBodySimple( int n); virtual ~StringBodySimple() { delete _str; } virtual StringBody * freeze() { return this; } virtual StringBody * make_private(); virtual int length() { return _len; } virtual void copy( char * dst) { memcpy( dst, _str, _len); } virtual const char * c_str() { return _str; } virtual char read_at( int i) { /* test! */ return _str[ i]; } virtual void write_at( int i, char ch) { /* test! */ _str[ i] = ch; } private: char * _str; int _len; };
String č. 6 - Jednoduché tělo StringBodySimple * StringBodySimple::create( const char * s) { StringBodySimple * p = new StringBodySimple( strlen( s)); strcpy( p->_str, s); return p; } StringBodySimple::StringBodySimple( int n) { _str = new char[ n + 1]; _len = n; StringBody * StringBodySimple::make_private() { if ( private() ) return this; StringBodySimple * p = new StringBodySimple( _len); strcpy( p->_str, _str);
String č. 6 - Složené tělo class StringBodyConcatenate : public StringBody { public: static StringBodyConcatenate * create( StringBody * a, StringBody * b); protected: StringBodyConcatenate( StringBody * a, StringBody * b); virtual ~StringBodyConcatenate(); virtual StringBody * freeze(); virtual StringBody * make_private(); virtual int length() { return _a->length() + _b->length(); } virtual void copy( char * dst); virtual const char * c_str() { /* error */ return 0; } virtual char read_at( int i); virtual void write_at( int i, char ch); private: StringBody * _a, * _b; };
String č. 6 - Složené tělo StringBodyConcatenate * StringBodyConcatenate::create( StringBody * a, StringBody * b) { return new StringBodyConcatenate( a, b); } StringBodyConcatenate::StringBodyConcatenate( { _a = a; _a->inc(); _b = b; _b->inc(); StringBodyConcatenate::~StringBodyConcatenate() { _a->dec(); _b->dec();
String č. 6 - Složené tělo StringBody * StringBodyConcatenate::freeze() { StringBodySimple * p = new StringBodySimple( length()); copy( p->_str); return p; } StringBody * StringBodyConcatenate::make_private() { if ( private() ) return this; return new StringBodyConcatenate( _a->make_private(), _b->make_private());
String č. 6 - Složené tělo int StringBodyConcatenate::length() { return _a->length() + _b->length(); } void StringBodyConcatenate::copy( char * dst) { _a->copy( dst); _b->copy( dst + _a->length()); char StringBodyConcatenate::read_at( int i) { return i < _a->length() ? _a->read_at( i) : _b->read_at( i - _a->length()); void StringBodyConcatenate::write_at( int i, char ch) { i < _a->length() ? _a->write_at( i, ch) : _b->write_at( i - _a->length(), ch);
Problém: Vzájemné volání dec() a destruktoru je rekurzivní void StringBody::dec() { if ( ! --count ) delete this; }; StringBodyConcatenate::~StringBodyConcatenate() { _a->dec(); _b->dec(); } Hloubka vnoření rekurze může být neúnosně velká { String x; for ( int i=0; i < 1000000; i++ ) x = x + "a";
Nahradit rekurzi pomocnou strukturou a cyklem Řešení 1 Nahradit rekurzi pomocnou strukturou a cyklem typedef std::stack< StringBody *> KillStack; inline void StringBody::dec() { if ( ! --count ) killMe(); } void StringBody::killMe() { KillStack toBeKilled; StringBody * b = this; for (;;) b->prepareToDie( toBeKilled); delete b; if ( toBeKilled.empty() ) break; b = toBeKilled.top(); toBeKilled.pop();
Řešení 1 StringBodyConcatenate::prepareToDie( KillStack & tbk) { _a->dec( tbk); _a = 0; _b->dec( tbk); _b = 0; } void StringBody::dec( KillStack & tbk) { if ( ! --count ) tbk.push( this); };
Řešení 1 Pomocná struktura Nevýhody: typedef std::stack< StringBody *> KillStack; Nevýhody: Časová náročnost operací struktury není zanedbatelná K rozsáhlému mazání může dojít v rámci téměř každé operace Práce se strukturou vyžaduje alokaci Deadlock: Ke zrušení dat je třeba naalokovat další Exception-safety: Destruktor nemá vyvolávat výjimky
Řešení 2 Jiné řešení Ekvivalentní úloha V okamžiku zrušení držadla String lze určit množinu uzlů typu StringBodyConcatenate, které jsou dosažitelné pouze z tohoto držadla Tato množina je binární strom Ekvivalentní úloha Zrušit binární strom bez rekurze a dalších datových struktur Pokud možno v lineárním čase Tato úloha je řešitelná postupným přeskupováním stromu a umazáváním uzlů s méně než dvěma syny Stále přetrvává problém s občasnými výskyty časově náročných operací Nevhodné pro real-time operace apod.
Problém: Implementace mnoha funkcí je rekurzivní void StringBodyConcatenate::copy( char * dst) const { _a->copy( dst); _b->copy( dst + _a->length()); }
Pokus o náhradu rekurze opakováním Řešení ? Pokus o náhradu rekurze opakováním Funkce copySomething okopíruje tolik, kolik dokáže class StringBody { /* ... */ virtual int copySomething( char * dst, int offset) const = 0; }; Funkce copy ji volá tak dlouho, dokud něco vrací void String::copy( char * dst) const { int offset, len; offset = 0; while ( (len = _body->copySomething( dst + offset, offset)) > 0 ) offset += len; }
Řešení ? Problém: Implementace funkce copySomething je stále rekurzivní int StringBodyConcatenate::copySomething( char * dst, int offset) const { if ( offset < _a->length() ) return _a->copySomething( dst, offset); else return _b->copySomething( dst + _a->length(), offset - _a->length()); }
Řešení ? Tail-recursion Rekurzivní volání na konci funkce void F( T p) { H( p); if ( C( p) ) F( G( p)); } Obecně lze nahradit cyklem while ( C( p) ) p = G( p);
Řešení ? Tail-recursion Problém: Funkce copySomething je virtuální Řešení: Pomocná funkce redirect Vrací odkaz na uzel, který je zodpovědný za danou pozici class StringBody { /* ... */ virtual const StringBody * redirect( int offset) const { return 0; // no redirection } }; const StringBody * StringBodyConcatenate::redirect( int offset) const if ( offset < _a->length() ) return _a; else return _b;
Řešení ? Tail-recursion Problém: Funkce copySomething je virtuální Řešení: Pomocná funkce redirect void String::copy( char * dst) const { int offset, len; const String * p, * p2; offset = 0; do { p = _body; while ( !! (p2 = p->redirect( offset)) ) p = p2; len = p->copySomething( dst + offset, offset); offset += len; } while ( len > 0 ); }
Řešení ? Pyrrhovo vítězství: Rekurze je odstraněna Algoritmus má nyní kvadratickou složitost
Závěr Srovnání možností Srovnání požadavků Zrušení stromu lze provést bez rekurze a pomocných struktur v lineárním čase Průchod stromem, který nemá být zničen, takto udělat nelze Ledaže by ve stromě byly zpětné odkazy Naše struktura ovšem není strom, ale DAG Srovnání požadavků Rušení stromu musí být bezpečná, vždy proveditelná akce Protože se volá z destruktorů Běžné operace na stromě takto bezpečné být nemusejí Mohou tedy využívat pomocné struktury Je nutné definovat způsob indikace chyby
STL – vector template<class T, class A = allocator<T> > class vector { public: typedef A allocator_type; typedef A::size_type size_type; typedef A::difference_type difference_type; typedef A::reference reference; typedef A::const_reference const_reference; typedef A::value_type value_type; typedef /*...*/ iterator; typedef /*...*/ const_iterator; typedef /*...*/ reverse_iterator; typedef /*...*/ const_reverse_iterator; explicit vector(const A& al = A()); explicit vector(size_type n, const T& v = T(), const A& al = A()); vector(const vector& x); vector(const_iterator first, const_iterator last, const A& al = A()); /* ... */
STL – vector /* template<class T, class A = allocator<T> > class vector { ... */ iterator begin(); const_iterator begin() const; iterator end(); iterator end() const; reverse_iterator rbegin(); const_reverse_iterator rbegin() const; reverse_iterator rend(); const_reverse_iterator rend() const; size_type size() const; bool empty() const; reference at(size_type pos); const_reference at(size_type pos) const; reference operator[](size_type pos); const_reference operator[](size_type pos) const; /* ... */
STL – vector /* template<class T, class A = allocator<T> > class vector { ... */ reference front(); const_reference front() const; reference back(); const_reference back() const; void push_back(const T& x); void pop_back(); void assign(const_iterator first, const_iterator last); void assign(size_type n, const T& x = T()); iterator insert(iterator it, const T& x = T()); void insert(iterator it, size_type n, const T& x); void insert(iterator it, const_iterator first, const_iterator last); iterator erase(iterator it); iterator erase(iterator first, iterator last); void clear(); void swap(vector x); /* ... */
STL – vector /* template<class T, class A = allocator<T> > class vector { ... */ protected: A allocator; };
STL – vector – naivní implementace – hlavička template<class T, int delta> class oriented_iterator; template<class T, int delta> class const_oriented_iterator; template<class T> class vector { public: typedef unsigned int size_type; typedef int difference_type; typedef T & reference; typedef const T & const_reference; typedef T value_type; typedef oriented_iterator< T, 1> iterator; typedef const_oriented_iterator< T, 1> const_iterator; typedef oriented_iterator< T, -1> reverse_iterator; typedef const_oriented_iterator< T, -1> const_reverse_iterator; /* ... */ private: size_type _n; T * _p; };
STL – vector – naivní implementace – přístup template< class T> reference vector< T>::at(size_type pos) { return pos >= 0 && pos < n ? _p[ pos] : _dummy(); } template< class T> const_reference vector< T>::at(size_type pos) const template< class T> reference vector< T>::operator[](size_type pos) { return at( pos); } template< class T> const_reference vector< T>::operator[](size_type pos) const template< class T> reference vector< T>::dummy() const { return *(T*)0; }
STL – vector – naivní implementace - konstrukce template< class T> vector< T>::vector() { _n = 0; _p = 0; } template< class T> vector< T>::vector( size_type n, const T & v) { _n = n; _p = new T[ _n]; /* nevolat konstruktory !!! */ for ( int i = 0; i < _n; i++) { /* zavolat konstruktory !!! */ /* _p[ i].T( v); */ } template< class T> vector< T>::vector( const vector & x) _n = x._n; /* _p[ i].T( x._p[ i]); */
STL – vector – naivní implementace – push/pop template< class T> void vector< T>::push_back(const T& x) { T * p = _p; _n = _n + 1; _p = new T[ _n + 1]; /* nevolat konstruktory !!! */ for ( int i = 0; i < _n - 1; i++) { /* zavolat konstruktory !!! */ /* _p[ i].T( p[ i]); */ } template< class T> void vector< T>::pop_back() /* ... */
STL – vector – vylepšená implementace - konstrukce void * operator new( size_t s, void * p) { return p; } template< class T> vector< T>::vector( size_type n, const T & v) { _n = n; _p = reinterpret_cast< T *>( new char[ _n * sizeof( T)]); for ( int i = 0; i < _n; i++) new( _p[ i]) T( v); } template< class T> vector< T>::~vector() _p[ i]->T::~T(); delete[] reinterpret_cast< char *>( _p);
STL – vector – naivní implementace – iterátory template< class T, int delta> class oriented_iterator { public: T & operator *() const { return _i < _v->_n ? *( T *)0 : _v->_p[ _i]; } const oriented_iterator< T, delta> & operator ++() /* prefix */ { if ( _i < _v->_n ) _i++; return * this; oriented_iterator< T, delta> operator ++( int) /* postfix */ { oriented_iterator< T, delta> old = * this; operator ++(); return old; bool operator ==( const oriented_iterator< T, delta> & b) const { return _i == _b->_i; } private: friend class vector< T>; oriented_iterator( vector< T> * v, int i) { _v = v; _i = i; } vector< T> * _v; int _i; };
STL – vector – naivní implementace – iterátory template< class T> iterator vector< T>::begin() { return iterator( this, 0); } template< class T> const_iterator vector< T>::begin() const { return const_iterator( this, 0); } template< class T> iterator vector< T>::end() { return iterator( this, _n); } template< class T> iterator vector< T>::end() const { return const_iterator( this, _n); }
mohou být definovány jako globální nebo uvnitř třídy Operátory new a delete Operátory new a delete mohou být definovány jako globální nebo uvnitř třídy na rozdíl od ostatních operátorů jsou i uvnitř třídy považovány za statické funkce Operátory deklarované uvnitř třídy jsou použity při alokaci resp. dealokaci objektů této třídy (a jejích potomků) Globální operátory jsou používány pro třídy bez těchto operátorů a pro datové typy, které nejsou třídami, včetně polí tříd void * operator new( size_t s); void operator delete( void * p);
Operátor new může mít přídavné parametry a může tak být přetížen Operátory new a delete Operátor new může mít přídavné parametry a může tak být přetížen Typické použití void * operator new( size_t s, void * p) { return p; } void call_constructor( X * p, int param) { new( p) X( param); Vztah operátoru new a konstruktoru X * p = new X(a,b,c); (nekorektní) ekvivalent X * p = (X*)X::operator new(sizeof(X)); if (p) p->X::X(a,b,c); // zakázáno Vztah operátoru delete a destruktoru delete p; ekvivalent if (p) p->X::~X(); // povoleno X::operator delete((void*)p);
Ladění programů v C++
Nežádoucí chování programu Předčasné ukončení programu Nedobrovolné (zásah CPU+OS) Ochrana paměti Neplatná/nepovolená instrukce Hodnotová chyba (dělení nulou...) Přetečení zásobníku (rekurze) Dobrovolné Ochranné mechanismy standardních knihoven Neobsloužená C++ výjimka assert apod. Neukončení programu Nekonečná smyčka/deadlock Nesprávný výstup programu Korektní program počítá něco jiného Nekorektní program náhodou doběhl do konce
Nedefinované chování (undefined behavior) Pohled normy C++ Nedefinované chování (undefined behavior) Vyvolání operace s parametry, pro něž norma nedefinuje chování definuje chování jako nedefinované Norma neříká, jak má být nedefinované chování ošetřeno Detekce při překladu Ukončení při běhu Dokumentované předvídatelné chování Ignorování problému s nepředvídatelnými důsledky
Nedefinované chování (undefined behavior) Pohled normy C++ Nedefinované chování (undefined behavior) Normou nepovolená situace Unspecified behavior Normou povolená situace, ve které norma nedefinuje některé aspekty chování Implementation-defined behavior Situace, ve které norma vyžaduje, aby konkrétní implementace zvolila a dokumentovala chování Včetně možnosti zákazu Locale-specific behavior Chování závislé na místním prostředí (vč. nastavení OS)
Nejčastější nedefinované chování Pohled normy C++ Nejčastější nedefinované chování Dereference nulového ukazatele Nekorektní přístup k paměťovému místu Paměťové místo neobsahuje žádný korektní objekt Paměťové místo obsahuje objekt jiného typu Paměťové místo obsahuje korektní objekt správného typu, ale pouze shodou okolností Nekorektní manipulace s iterátory Překročení intervalu [begin(), end()] Manipulace s invalidovaným iterátorem Nekorektní přetypování ukazatele Skutečný typ objektu je jiný než předpokládaný Nekorektní nepřímé volání Volání virtuální funkce na špatném objektu/ve špatný okamžik Neinicializovaný/nulový ukazatel na funkci
Typické důsledky nedefinovaného chování Dereference nulového ukazatele Ukončení operačním systémem (ochrana paměti) Nekorektní přístup k paměťovému místu - čtení Ukončení operačním systémem Načtení nežádoucí hodnoty Nekorektní přístup k paměťovému místu - zápis Ohlášení ochrannými prostředky knihovny - později Poškození režijních dat (alokace, tabulky virtuálních funkcí,...) Poškození hodnoty cizího paměťového místa Nekorektní manipulace s iterátory Detekováno ochrannými prostředky knihovny Nedetekováno vůbec Nekorektní přetypování ukazatele Nekorektní nepřímé volání Ukončení OS (neplatná instrukce/ochrana paměti)
Nejčastější příčiny nedefinovaného chování Dereference nulového ukazatele Zřejmé - toto je žádoucí chování Nekorektní přístup k paměťovému místu - malá odchylka Přetečení/podtečení mezí pole Nekorektní přístup k paměťovému místu - velká odchylka Neinicializovaný ukazatel/index/iterátor Ukazatel/index/iterátor poškozený nesprávným čtením/zápisem Nekorektní přístup k paměťovému místu - pozdě Ponechání ukazatele na odalokovaná data Vracení reference na lokální proměnnou Nekorektní přístup k paměťovému místu - chybný typ Nesprávný static_cast - špatný odhad konkrétního typu Příliš odvážný reinterpret_cast union Nekorektní nepřímé volání Neinicializovaný/nulový ukazatel na funkci
Principy detekce nekorektního chování Ochrana paměti stránkováním - granularita ~4KB Ochrana kódu proti zápisu (čtení) Z hlediska ladění málo významné (Ochrana konstant proti zápisu) Nevýznamné, týká se pouze řetězcových a reálných konstant Ochrana proti přístupu mimo data/heap Účinné pouze proti přístupům na nesmyslné adresy Ochrana proti provádění instrukcí mimo kód (DEP) Z hlediska ladění chyb bezvýznamné Nárazníkové zóny dynamicky alokovaných dat Detekce mírného přetečení dynamicky alokovaných polí Naplněny nepravděpodobnými daty Zvyšuje pravděpodobnost rychlé detekce při čtení Testovány na poškození při dealokaci bloku Detekce zápisů
Principy detekce nekorektního chování Ochrana paměti stránkováním - granularita ~4KB Nárazníkové zóny dynamicky alokovaných dat Inicializační hodnoty dynamicky alokovaných dat Vyplnění nepravděpodobnou hodnotou Včasná detekce čtení neinicializovaných objektů Čištění při dealokaci Včasná detekce čtení odalokovaných objektů Kontrola heapu při ukončení programu Neodalokované bloky (není nekorektní, ale obvykle indikuje chybu) Poškození alokačních záznamů (detekce nesprávných zápisů)
Naučte se chybu vyvolávat opakovaně Postup při ladění chyb Naučte se chybu vyvolávat opakovaně Nahraďte interaktivitu vstupem ze souboru Prostudujte pečlivě chybové hlášení Zastavte v debuggeru program v okamžiku chyby Nedobrovolné ukončení - debuggery umí samy Dobrovolné ukončení: breakpoint na vhodné místo uvnitř systému Zjistěte, která data jsou špatně Prostudujte pečlivě špatné hodnoty Zjistěte, jak se tam špatná data dostala Znovuspouštění programu Datové breakpointy
Ladění výkonu programu Za 99% výkonu odpovídá 1% kódu Určete který Profilery Instrumentace Kód se doplní o instrukce zaznamenávající průchod Ovlivňuje čas Sampling Běh se periodicky přerušuje a zaznamenává se pozice (call stack) Řízeno časem nebo performance-counters Sbírá i jiné informace než čas Rychlost bývá ovlivňována mnoha faktory Úspešnost vyrovnávacích pamětí je nejdůležitější