1.69k likes | 1.85k Views
Pokro čilé p rogramování v C++ (část B). David Bednárek www.ksi.mff.cuni.cz. C++ 11. C++11. Jazyk && (rvalue reference) Lambda auto Variadic templates Inicializ átory Podpora metaprogramov ání (constexpr, decltype, static _assert, ... ) ... a mnoho dal ších drobností Knihovny
E N D
Pokročilé programování v C++(část B) David Bednárek www.ksi.mff.cuni.cz
C++11 • Jazyk • && (rvalue reference) • Lambda • auto • Variadic templates • Inicializátory • Podpora metaprogramování (constexpr, decltype, static_assert, ...) • ... a mnoho dalších drobností • Knihovny • Nové kontejnery (C++ 2003 TR1) • Podpora paralelismu • Atomické operace, fence • Vlákna, thread-local storage • Synchronizační primitiva (mutex, lock, condition_variable,...) • regexp, random, ...
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 Typyiterator, 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); Kontejnery a iterátory C++11 C++11
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 Kategorie 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 ! { ... } } Použití iterátorů
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::copy( x.begin(), x.end(), std::ostream_iterator( o)); input category istream_iterator std::istream & i = ...; std::copy( std::istream_iterator( i), std::istream_iterator(), std::back_inserter( a)); Standardem definované iterátory
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; }; iterator_traits
std::iterator • Pomůcka pro vytváření vlastních iterátorů • šablona std::iterator použitelná jako předek třídy template<class Category, class T, class Distance = ptrdiff_t, class Pointer = T*, class Reference = T&> struct iterator { typedef T value_type; typedef Distance difference_type; typedef Pointer pointer; typedef Reference reference; typedef Category iterator_category; };
std::iterator • Příklad my_iterator b, e; std::distance( b, e) • distance potřebuje znát iterator_category a difference_type std::iterator_traits< my_iterator>::iterator_category • iterator_traits vyřeší případ ukazatelů a přesměruje problém na samotný iterátor my_iterator::iterator_category • uživatelský iterátor dědí instanci std::iterator s vhodnými parametry std::iterator<...>::iterator_category
Š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
Š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 volání 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) } • problém lze také vyřešit pomocí using template< typename T> class X : public T { using T::m; void h() { return m(); } }
my_iterator • Iterator je obvykle šablona template< typename T> class my_iterator : public std::iterator< std::forward_iterator_tag, T> { private: typedef std::iterator< std::forward_iterator_tag, T> base_; public: using typename base_::reference; using typename base_::pointer; reference operator*() const; pointer operator->() const; //... };
my_const_iterator • Kompletní kontejner musí zvládat čtení nemodifikovatelného kontejneru void f( const my_container<T> & k) { for ( my_container<T>::const_iterator it = k.begin(); it != k.end; ++it) // ... } • Kontejner musí poskytovat dvojice metod ošetřující const template< typename T> class my_container { public: typedef my_iterator< T> iterator; typedef my_const_iterator< T> const_iterator; iterator begin(); const_iterator begin() const; const_iterator cbegin() const; // podle vzoru C++11 } • Vyžaduje dvě třídy implementující iterátor
Rvalue reference Francisco de Goya. Sv. František Borgiášu lože umírajícího (detail). 1788.
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ů
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?
Zbytečná kopírování Řešení pomocí rvalue referencí
Ř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
Ř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
Ř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
copy/move Speciální metody tříd – 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
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
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á ze4 copy/move metod ani destruktor 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( const T & x); T( T && x); T & operator=( const T & x); T & operator=( T && x); ~T(); 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
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
lvalue/rvalue • U výrazu překladač zkoumá • 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
lvalue/rvalue C++11
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) { return std::move( a += b); } C++11
Podpora move ve funkcích/metodách • Parametr typu T && se vyplatí,pokud funkce dokáže využít prostor přinesený parametrem • Typicky se prostor použije pro návratovou hodnotu funkce • Příklad: sčítání matic • Nevyplatí se pro násobení matic • Algoritmus násobení neumí pracovat "na místě" • Nevyplatí se pro zřetězení řetězců • Výsledek má jinou velikost • Funkce pak musí mít dvě varianty • Parametr typu const T & • Parametr typu T && • Pokud je variabilních parametrů víc, exponenciální počet variant • Viz "perfect forwarding" C++11
lvalue/rvalue Nové rozhraní STL
lvalue/rvalue - STL • Nová implementace swap 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
lvalue/rvalue - STL • Copy/move insertion 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
Š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 template <class ... Types> class tuple { public: tuple(const Types&...); /* black magic */ }; template < size_t I, class T> class tuple_element { public: typedef /* black magic */ type; }; template <size_t I, class... Types> typename tuple_element<I, tuple<Types...> >::type& get(tuple<Types...>& t); • použití typedef tuple< int, double, int> my_tuple; typedef tuple_element< 1, my_tuple> alias_to_double; my_tuple t1( 1, 2.3, 4); double v = get< 1>( t1); C++11: <utility>
lvalue/rvalue Perfect forwarding