3.54k likes | 3.65k Views
Pokro čilé p rogramování v C++ (část B). David Bednárek ulita.ms.mff.cuni.cz. Klíčové vlastnosti C++. 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
E N D
Pokročilé programování v C++(část B) David Bednárek ulita.ms.mff.cuni.cz
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é
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. • Bjarne Stroustrup • 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
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 Singletony
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
Kanonické tvary tříd Hodnotové typy
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+( const Matrix &, const Matrix &); 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+( const Matrix &, const Matrix &); Kanonické tvary tříd
Kanonické tvary tříd Funktory, visitory
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 { public: PrintVisitor( Printer * p) : p_( p) {} virtual void visitEllipse( Ellipse *); virtual void visitRectangle( Rectangle *); virtual void visitLine( Line *); private: Printer * p_; }; Kanonické tvary tříd
Kanonické tvary tříd Dynamicky alokovaná data
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 { public: 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 { public: 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; }; public: class NodeRef { public: /* ... */ private: TreeNode * p; }; Tree() : root( 0) {} ~Tree(){ /* ... */ } NodeRef create_node( /* ... */); void insert_node( NodeRef p); NodeRef find_node( /* ... */ ); private: TreeNode * root; Tree( const Tree &); /* ... */ }; Kanonické tvary tříd
Kanonické tvary tříd Třídy s dědičností
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=( const AbstractObject&); }; 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 */; }; Kanonické tvary tříd
Kanonické tvary tříd Neinstanciované třídy
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 structallocation_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 structmy_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 template< class T> struct type_traits { typedef const T & param_t; }; Explicitní specializace traits template<> struct type_traits< char> { typedef char param_t; }; Kanonické tvary tříd
Polymorfismus Kompilační a běhový
Polymorfismus • Polymorfismus • Stejný zdrojový kód v různých situací 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
Polymorfismus • Kompilační polymorfismus • Šablony • 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
class functor { public: virtual void f( ...) = 0; }; void for_each( functor & x) { for( ... it = ... ) x.f( it); // run-time binding } class functor_add { 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()); Polymorfismus běhový a kompilační
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>(); Kompilační polymorfismus s objektem a bez
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; template< typename K> 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); Policy class vs. traits
template< typename K> struct for_each_traits; template< typename K> 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 kontejnery
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* Traits a iterátory
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)
for_each( []( int x){ return 2 * x }); for_each( constant( 2) * _1); Lambda (C++0x vs. boost)
struct functor_1 { template< typename T> T operator()( T x) const { return x; } }; extern functor_1 _1; template< typename T> struct functor_const { functor_const( T c) : c_( c) {} T operator()( T x) const { return c_; } T c_; }; template< typename T> 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_; }; template< typename F1, typename F2> functor_mul< F1, F2> operator+ ( F1 f1, F1 f2) { return functor_const< F1, F2> ( f1, f2); } for_each( constant( 2) * _1); Lambda (boost)
for_each( std::cout << "x=" << _1); for_each( constant( 2) * _1); Lambda (boost) - problémy