PDA

Visualizza la versione completa : [C++] Ereditarietà vs Templates


lovesemiramide
14-05-2014, 22:20
L' altro giorno mi è capitato codice simile a questo:



struct StreamBase {
virtual bool open() = 0;
virtual int write(const std::string& data) = 0;
virtual void close() = 0;
virtual void flush() = 0;
}

struct FileStream : StreamBase {
// implementazioni
}

// altre implementazioni

void foo(std::unique_ptr<StreamBase> stream) { // stuff }

struct bar
{
explicit bar(std::unique_ptr<StreamBase> stream) : _stream(std::move(stream)) {}
private:
std::unique_ptr<StreamBase> _stream;
};



Il fatto è che penso che tutto ciò possa essere ottimizzato usando i template, in particolare eliminando tutta la gerarchia delle classi e far diventare foo e bar una funzione e una classe template e poi utilizzando direttamente i metodi che espone la classe base. Ho fatto notare ciò al fornitore della libreria, il quale mi ha risposto piuttosto male dicendo che le ottimizzazioni portate dai template sono trascurabili rispetto ai costi delle vtable e che anzi introducono solo complessità nel codice.

A questo punto sorgre la questione: quando usare i template e quando l' ereditarietà?

Secondo me è preferibile usare i template quando la funzione o classe necessita solo di una "conformità" di interfaccia, mentre usare l' ereditarietà solo nei casi in cui è necessaria il polimorfismo a runtime.

Vorrei sapere se ciò che penso è corretto e se qualcun può darmi qualche altra dritta.

MItaly
15-05-2014, 00:03
L'uso così pervasivo dei template ha alcuni importanti drawback:

il template finisce con l'"infettare" tutto il codice; se foo e bar devono a loro volta passare stream in giro, anche le altre funzioni dovranno essere template;
il codice deve essere tutto inline e in header, il che rallenta sensibilmente i tempi di parsing e compilazione;
sempre sui tempi di compilazione, espandere i template non è gratis; inoltre, ogni cambiamento nella classe passata come argomento comporta la ricompilazione di tutto l'ambaradam;
un template non specifica un contratto di interfaccia in alcuna maniera - se sbagli qualcosa esplode tutto con messaggi illeggibili;
inoltre, un template verifica solo quello che viene effettivamente usato, e non l'interfaccia richiesta "ufficialmente"; magari in questo rilascio della libreria non uso tutti i metodi dell'interfaccia, ma li ho preventivati perché nella prossima versione mi servono. Con un'interfaccia "stile OOP" i client della libreria sono obbligati a fare le cose correttamente da subito, mentre con un template si rompe tutto inaspettatamente solo quando inizio ad usare la nuova versione della libreria.


Inoltre, l'overhead del metodo virtuale spesso è risibile; in genere, conta solo se il metodo è estremamente breve nella misura in cui impedisce l'inlining (la doppia indirezione sarebbe costosa solo in un tight loop, e lì il branch predictor impara rapidamente qual è l'indirizzo giusto a cui saltare); inoltre, in un contesto in cui il compilatore sappia provare il tipo su cui foo o bar operano (ad esempio se foo viene chiamata su una variabile locale di tipo definito) e il loro body è disponibile (o è attiva l'LTCG) è possibile che il body della funzione venga espanso in linea e con dispatch statico.

In sintesi: il passaggio a template in casi come questi in genere è un'ottimizzazione dubbia per le prestazioni, e una pessimizzazione marcata in termini di leggibilità, chiarezza del codice, "autodocumentazione" del contratto e comodità di build. Starei a pensarci solo dopo aver verificato tramite profiling che si tratta effettivamente di un collo di bottiglia del codice.

shodan
15-05-2014, 00:07
Forse questo articolo sarà utile al riguardo:
http://www.eptacom.net/pubblicazioni/pub_it/oopgen.html

lovesemiramide
15-05-2014, 14:45
Grazie a tutti e due per aver risposto (l' articolo è stato veramente interessante).

Ho provato a far diventare alcune classi della libreria dei template e in effetti il codice risultante era poco comprensibile e se come dice MItaly i benefici non sono poi così elevati mi rendo conto che in questo caso i template non sono adatti alla situazione.

In questo modo però si riduce (e di molto) la mia stima nei confronti dei template in C++. Infatti l' unico utilizzo, a parte la tecnica del metaprogramming, che riesco a trovare è quello di creare wrapper o collezioni che non hanno bisogno di conoscere l' interfaccia, o almeno gran parte, dei tipi a cui vengono applicate, ma solo delle sue "proprietà" (se è DefaultConstructible, etc...) . Corretto?

Inoltre nella libreria vi è anche codice di questo tipo:


struct base
{
std::list<std::string> string_list() const;
private:
std::list<std::string> _string_list;
};

struct derived : base { }


Il fatto è che _string_list in derived dovrebbe chiamarsi in un' altra maniera per lo scopo, ma di fatto utilizza il nome dei metodi della classe base. Tutto ciò è buona norma o invece, come penso io, complica la comprensione del codice?

Essenzialmente base esiste perché ci sono più classi che hanno come attributo quel tipo di lista e quindi per evitare codice duplicato hanno deciso di creare questa classe padre con gli attributi comuni.

shodan
15-05-2014, 20:32
Infatti l' unico utilizzo, a parte la tecnica del metaprogramming, che riesco a trovare è quello di creare wrapper o collezioni che non hanno bisogno di conoscere l' interfaccia

Il che non è poco, se ci pensi. Decidere di applicare un certo algoritmo in base al tipo di parametro, o specializzare una classe generica tramite una classe policy è qualcosa che risulta più semplice farlo con il polimorfismo compile time che con quello run time.
Però questo non giustifica l'utilizzo "a ogni costo" dei template.
Anche perché con il polimorfismo runtime, le classi derivate sono specializzazioni di un'unica classe base.
Con i template ti ritrovi con N classi distinte, che avranno pure le stesse funzioni ma sono cose diverse.



Tutto ciò è buona norma o invece, come penso io, complica la comprensione del codice?

Per dirla tutta, non ti dovrebbe proprio interessare come sono chiamati i membri privati della classe: a te interessa solo la sua interfaccia. In ogni caso, si dovrebbe definire prima cosa si intende "buona norma", visto che, praticamente, ognuno ha la sua.

lovesemiramide
15-05-2014, 21:48
Il che non è poco, se ci pensi. Decidere di applicare un certo algoritmo in base al tipo di parametro, o specializzare una classe generica tramite una classe policy è qualcosa che risulta più semplice farlo con il polimorfismo compile time che con quello run time.
Probabilmente mi sono espresso male, ma intendevo dire che fino ad ora ho utilizzato molto i template in design simili pensando che fosse la maniera più corretta di fare, mentre invece mi accorgo che nella maggior parte dei casi non lo è.


Per dirla tutta, non ti dovrebbe proprio interessare come sono chiamati i membri privati della classe: a te interessa solo la sua interfaccia. In ogni caso, si dovrebbe definire prima cosa si intende "buona norma", visto che, praticamente, ognuno ha la sua.

Infatti a me non interessa come si chiami l' attributo privato, bensì il getter che ha un nome non sempre adatto nelle sottoclassi.
Ecco un esempio forse più chiaro:


struct list
{
std::list<std::string> list() const { return _list; }
private:
std::list<std::string> _list;
};

struct banned_list : public list
{
// per chiarezza vorrei che list() si chiamasse banned_list()
};

shodan
15-05-2014, 23:35
Infatti a me non interessa come si chiami l' attributo privato, bensì il getter che ha un nome non sempre adatto nelle sottoclassi.

Hai scritto:


Il fatto è che _string_list in derived dovrebbe chiamarsi in un' altra maniera per lo scopo, ma di fatto utilizza il nome dei metodi della classe base.

Hai citato _string_list che è un dato membro. E questo io ho commentato.



Ecco un esempio forse più chiaro:

Ho capito che intendi, ma l'esempio è infelice. (Hai definito un costruttore che restituisce qualcosa, il che è vietato.)

Nulla vieta di scrivere (ho modificato un po' per chiarezza):


struct list
{
virtual std::list<std::string> get_list() const { return _list; }
private:
std::list<std::string> _list;
};

struct banned_list : public list
{
std::list<std::string> get_banned_list() const { return _list; }

};

get_banned_list(), però, non fa parte dell'interfaccia di list pertanto non si può usare con puntatori/reference di tipo list. In pratica non serve.

Qui trovi un manuale con consigli e indicazioni utili per una buona codifica (applicabili in generale, non al caso in questione).
http://www.eptacom.net/pubblicazioni/cpp_stile/all.html

MItaly
16-05-2014, 00:18
In questo modo però si riduce (e di molto) la mia stima nei confronti dei template in C++. Infatti l' unico utilizzo, a parte la tecnica del metaprogramming, che riesco a trovare è quello di creare wrapper o collezioni che non hanno bisogno di conoscere l' interfaccia, o almeno gran parte, dei tipi a cui vengono applicate, ma solo delle sue "proprietà" (se è DefaultConstructible, etc...) . Corretto?

Infatti è il solo ed unico motivo per cui sono stati introdotti i template in C++, la metaprogrammazione template è stata scoperta in seguito - e intendo davvero "scoperta", nel senso che qualcuno un giorno ha visto che si poteva abusare dei template per ottenere tutta una serie di caratteristiche di metaprogrammazione.

(rimane comunque ridicola come potenzialità rispetto - ad esempio - a linguaggi come il LISP, oltre a costringere a contorsioni mentali e sintattiche notevoli)

lovesemiramide
16-05-2014, 19:22
Hai scritto:

Hai citato _string_list che è un dato membro. E questo io ho commentato.


Ho capito che intendi, ma l'esempio è infelice. (Hai definito un costruttore che restituisce qualcosa, il che è vietato.)

Nulla vieta di scrivere (ho modificato un po' per chiarezza):


struct list
{
virtual std::list<std::string> get_list() const { return _list; }
private:
std::list<std::string> _list;
};

struct banned_list : public list
{
std::list<std::string> get_banned_list() const { return _list; }

};

get_banned_list(), però, non fa parte dell'interfaccia di list pertanto non si può usare con puntatori/reference di tipo list. In pratica non serve.

Qui trovi un manuale con consigli e indicazioni utili per una buona codifica (applicabili in generale, non al caso in questione).
http://www.eptacom.net/pubblicazioni/cpp_stile/all.html

Hai ragione te, errori miei.
Comunque per quanto riguarda il manuale gli darò un' occhiata, sembra interessante.

Nell' esempio però qualcosa non mi torna: se _list è private allora banned_list non la potrà utilizzare in get_banned_list().

shodan
16-05-2014, 19:45
Nell' esempio però qualcosa non mi torna: se _list è private allora banned_list non la potrà utilizzare in get_banned_list().
Qui hai ragione tu: errore mio.
Comunque la cosa si può risolvere con:



std::list<std::string> get_banned_list() const { return get_list(); }


dato che banned_list eredita la funzione get_list() da list.

Loading