[C++] Costruttore per Riferimento
Salve,
chiedo un piccolo chiarimento su una questione.
Conosco il C++ derivando le informazione tra Java e C, perciò alcuni ragionamenti tipici di questi linguaggi mi portano ad errori di sintassi nel C++ stesso. Visto che devo lavorarci con questo linguaggio, devo chiarire alcune cose.
Per il momento la questione è questa, propongo un esempio:
se il costrutture ha come parametro un passaggio per riferimento, è possibile ragionare come in Java, facendo un'allocazione dinamica fresh nella chiamata del costruttore?
es.
mi pare che non sia possibile farlo, ma allora che serve un tipo di costruttore del genere, se si passano solo oggetti dello scope locale allocati staticamente? Per identerci immagino che le uniche politiche ammesse siano:
nel primo caso penso ci sia una copia di wid, essendo per valore. Nel secondo?
Ringrazio chi chiarisce la questione
chiedo un piccolo chiarimento su una questione.
Conosco il C++ derivando le informazione tra Java e C, perciò alcuni ragionamenti tipici di questi linguaggi mi portano ad errori di sintassi nel C++ stesso. Visto che devo lavorarci con questo linguaggio, devo chiarire alcune cose.
Per il momento la questione è questa, propongo un esempio:
class T{ public: T(gin::Widget& t); };
se il costrutture ha come parametro un passaggio per riferimento, è possibile ragionare come in Java, facendo un'allocazione dinamica fresh nella chiamata del costruttore?
es.
T temp = new T(new gin::Widget())
mi pare che non sia possibile farlo, ma allora che serve un tipo di costruttore del genere, se si passano solo oggetti dello scope locale allocati staticamente? Per identerci immagino che le uniche politiche ammesse siano:
gin::Widget wid(); T temp = new T(wid); gin::Widget& wid1 = wid; T temp = new T(wid1);
nel primo caso penso ci sia una copia di wid, essendo per valore. Nel secondo?
Ringrazio chi chiarisce la questione

Risposte
"hamming_burst":
se il costrutture ha come parametro un passaggio per riferimento, è possibile ragionare come in Java, facendo un'allocazione dinamica fresh nella chiamata del costruttore?
es.
T temp = new T(new gin::Widget())
Potresti farlo, però "new" restituisce un puntatore e devi prima dereferenziarlo.
T temp = new T( * new gin::Widget())
Ma in questo caso "T" acquisisce il nuovo oggetto, nel senso che è l'unico che abbia un riferimento ad esso. Quindi dovrà deallocarlo in seguito con "delete", per evitare memory leak. Ma come fa "T" a sapere se il riferimento passato era un oggetto che deve deallocare, oppure se ci penserà il chiamante (oppure è allocato sullo stack)?
In generale si usano puntatori quando si vuole gestire esplicitamente la vita di un oggetto. Il chiamante crea l'oggetto con "new" e lo distrugge con "delete". Per passarlo ad altre funzioni però può usare i riferimenti, che sono più sicuri.
Se qualche funzione "g" accetta come parametro un riferimento, assume che tale riferimento sia sempre valido finché "g" non termina. Della allocazione / deallocazione dell'oggetto si occupa il chiamante. Ma per il resto la semantica è la stessa dei puntatori.
mi pare che non sia possibile farlo, ma allora che serve un tipo di costruttore del genere, se si passano solo oggetti dello scope locale allocati staticamente? Per identerci immagino che le uniche politiche ammesse siano:
gin::Widget wid(); T temp = new T(wid); gin::Widget& wid1 = wid; T temp = new T(wid1);
nel primo caso penso ci sia una copia di wid, essendo per valore. Nel secondo?
Se "T" ha solo un costruttore per riferimento anche il primo caso è per riferimento, e non per valore. Nel secondo è esplicitamente per riferimento.
Ho ritrovato un articolo di Herb Sutter [1] che avevo letto tempo fa, che mostra quali problemi ci siano se si crea un nuovo oggetto con new come parametro di una funzione (caso generale di quello che vorresti fare tu).
Ne approfitto per approfondire un po'.
Regola numero 1: in C++ non si dovrebbero mai usare i puntatori. I riferimenti hanno (quasi) la stessa semantica ma sono più sicuri, non c'è il caso di riferimenti non inizializzati.
Quindi tutte le funzioni devono accettare o un parametro T, o un riferimento T &, o un riferimento costante const T &; niente puntatori T *. Nei casi dove in C si usavano gli array (esempio stringhe e vettori) in C++ si usano classi della libreria standard (std::string e std::vector).
Purtroppo l'operatore new restituisce un puntatore, quindi quando si crea dinamicamente un oggetto si devono necessariamente usare i puntatori. Il problema è che ogni oggetto creato con new deve essere distrutto con delete.
Questo è un esempio banale, "a" potrebbe essere creato o distrutto in altri punti, ed è facile dimenticarsi o sbagliare un delete, oppure riutilizzare "a" dopo averlo distrutto. Ma anche in questo esempio banale, se "Codice vario" lancia un'eccezione, "a" non verrà distrutto e si avrà un memory leak.
Per questo motivo si usa l'idioma RAII, ad esempio con uno smart pointer.
Regola numero 2: nei rari casi in cui sia necessario usare esplicitamente un puntatore lo si deve incapsulare in uno smart pointer.
Ora il puntatore è acquisito dallo smart pointer. È quest'ultimo che occupa di chiamare delete, non va fatto esplicitamente. Nel distruttore di "ab" viene appunto chiamato delete sul puntatore. Inoltre "ap" è una variabile creata sullo stack, il suo distruttore viene automaticamente chiamato quando esce dallo scope, anche in caso di eccezioni. All'uscita da "funzione()" l'oggetto creato con new è stato sicuramente distrutto.
Ovviamente ci sono eccezioni a queste regole (ad esempio per usare il polimorfismo bisogna usare puntatori o riferimenti, e non si può creare una lista dei secondi), ma in generale è bene seguirle. La tesi su cui sto lavorando, un progetto medio / piccolo da poco più di 10 k linee di codice, usa appena 2 puntatori, incapsulati in smart pointer (e l'unico motivo per cui uso puntatori è diminuire il tempo di compilazione con l'idioma PIMPL, altrimenti potrei farne a meno). Risultato: praticamente nessun errore di segmentazione.
Purtroppo C++ è molto diverso sia da C che da Java, ed è decisamente complesso. I siti (e i libri) di Herb Sutter [1], Scott Meyers [2] e Andrei Alexandrescu [2] sono un'ottima risorsa.
Edit: corretti errori nel codice.
[1]: http://herbsutter.com/gotw/_102/
[2]: http://docs.online.bg/PROGRAMMING/effec ... /INDEX.HTM
[3]: http://erdani.com/
Ne approfitto per approfondire un po'.
Regola numero 1: in C++ non si dovrebbero mai usare i puntatori. I riferimenti hanno (quasi) la stessa semantica ma sono più sicuri, non c'è il caso di riferimenti non inizializzati.
Quindi tutte le funzioni devono accettare o un parametro T, o un riferimento T &, o un riferimento costante const T &; niente puntatori T *. Nei casi dove in C si usavano gli array (esempio stringhe e vettori) in C++ si usano classi della libreria standard (std::string e std::vector).
Purtroppo l'operatore new restituisce un puntatore, quindi quando si crea dinamicamente un oggetto si devono necessariamente usare i puntatori. Il problema è che ogni oggetto creato con new deve essere distrutto con delete.
void funzione() { Tipo * a = new Tipo; // Codice vario delete a; }
Questo è un esempio banale, "a" potrebbe essere creato o distrutto in altri punti, ed è facile dimenticarsi o sbagliare un delete, oppure riutilizzare "a" dopo averlo distrutto. Ma anche in questo esempio banale, se "Codice vario" lancia un'eccezione, "a" non verrà distrutto e si avrà un memory leak.
Per questo motivo si usa l'idioma RAII, ad esempio con uno smart pointer.
Regola numero 2: nei rari casi in cui sia necessario usare esplicitamente un puntatore lo si deve incapsulare in uno smart pointer.
#include <memory> void funzione() { // std::auto_ptr è sostituito da std::unique_prt in C++11. // Inoltre std::auto_ptr non può essere parametro di contenitori // come std::vector, std::list... (std::unique_ptr invece sì). std::auto_ptr<Tipo> ap( new Tipo ); // Codice vario }
Ora il puntatore è acquisito dallo smart pointer. È quest'ultimo che occupa di chiamare delete, non va fatto esplicitamente. Nel distruttore di "ab" viene appunto chiamato delete sul puntatore. Inoltre "ap" è una variabile creata sullo stack, il suo distruttore viene automaticamente chiamato quando esce dallo scope, anche in caso di eccezioni. All'uscita da "funzione()" l'oggetto creato con new è stato sicuramente distrutto.
Ovviamente ci sono eccezioni a queste regole (ad esempio per usare il polimorfismo bisogna usare puntatori o riferimenti, e non si può creare una lista dei secondi), ma in generale è bene seguirle. La tesi su cui sto lavorando, un progetto medio / piccolo da poco più di 10 k linee di codice, usa appena 2 puntatori, incapsulati in smart pointer (e l'unico motivo per cui uso puntatori è diminuire il tempo di compilazione con l'idioma PIMPL, altrimenti potrei farne a meno). Risultato: praticamente nessun errore di segmentazione.
"hamming_burst":
Conosco il C++ derivando le informazione tra Java e C, perciò alcuni ragionamenti tipici di questi linguaggi mi portano ad errori di sintassi nel C++ stesso. Visto che devo lavorarci con questo linguaggio, devo chiarire alcune cose.
Purtroppo C++ è molto diverso sia da C che da Java, ed è decisamente complesso. I siti (e i libri) di Herb Sutter [1], Scott Meyers [2] e Andrei Alexandrescu [2] sono un'ottima risorsa.
Edit: corretti errori nel codice.
[1]: http://herbsutter.com/gotw/_102/
[2]: http://docs.online.bg/PROGRAMMING/effec ... /INDEX.HTM
[3]: http://erdani.com/
Ti ringrazio davvero molto!!
Davvero interessante il concetto di smart pointer, che onestamente non avevo mai sentito. Una feature del C++ che non immaginavo esistesse, mi sembra molto una "toppa" per far assomigliare il linguaggio più Java-like, cosa che penso molto sensata per distaccarsi completamente dal C. Interessante pure questa politica RAII e PIMPL...
Mi leggo il post meglio un altro momento, anche perchè devo riscrivere una parte di una libreria che mi serve per un lavoro, dove è scritta in C ed aggiornarla in C++
direi ottimo risultato...in questi giorni il "core dump", invece, è di casa da me
Davvero interessante il concetto di smart pointer, che onestamente non avevo mai sentito. Una feature del C++ che non immaginavo esistesse, mi sembra molto una "toppa" per far assomigliare il linguaggio più Java-like, cosa che penso molto sensata per distaccarsi completamente dal C. Interessante pure questa politica RAII e PIMPL...
Mi leggo il post meglio un altro momento, anche perchè devo riscrivere una parte di una libreria che mi serve per un lavoro, dove è scritta in C ed aggiornarla in C++

Risultato: praticamente nessun errore di segmentazione.
direi ottimo risultato...in questi giorni il "core dump", invece, è di casa da me

"hamming_burst":
Davvero interessante il concetto di smart pointer, che onestamente non avevo mai sentito. Una feature del C++ che non immaginavo esistesse, mi sembra molto una "toppa" per far assomigliare il linguaggio più Java-like, cosa che penso molto sensata per distaccarsi completamente dal C.
Il C++ è stato progettato per essere (quasi) compatibile con il C a livello di sorgente, però a livello idiomatico è molto diverso: buon codice C è normalmente pessimo codice C++. L'idea è che di default si dovrebbero usare i costrutti ad alto livello, come in Java, Python... ma se e solo se necessario si può tranquillamente scendere e gestire manualmente la memoria, come in C.
[quote]Risultato: praticamente nessun errore di segmentazione.
direi ottimo risultato...in questi giorni il "core dump", invece, è di casa da me

In realtà mi capitano ancora, principalmente perché mi ostino ad accedere ai vettori con l'operatore [], quando c'è la funzione at() che invece controlla gli indici e lancia un'eccezione. Però abbastanza raramente, ed è abbastanza semplice localizzare il problema.
Te affermi (cmq è una politica di gestione...) di utilizzare il minimo possibile i puntatori, questo perciò implica il non utilizzo dell'allocazione dinamica.
Come gestisci il fatto che la memoria statica è limitata (i riferimenti sono ad allocazioni statiche), in C++ è disponibile più memoria a stack?
me ne sto accorgendo di questo fatto in prima persona.
Percaso hai qualche spunto teorico (un link, una dispensa,...) su una qualche procedura standard (un metodo concettuale intendo es. simili ai Design Pattern) per convertire in modo abb automatico una serie di funzioni di una libreria scritta in C, in uno pseduo-C++? Una specie di reverse engineering in codice O-O.
Non vorrei proprio ridurmi a riscrivere gli algoritmi, ma solo riscrivere con metodologia O-O il tutto.
Come gestisci il fatto che la memoria statica è limitata (i riferimenti sono ad allocazioni statiche), in C++ è disponibile più memoria a stack?
"claudio86":
Il C++ è stato progettato per essere (quasi) compatibile con il C a livello di sorgente, però a livello idiomatico è molto diverso: buon codice C è normalmente pessimo codice C++. L'idea è che di default si dovrebbero usare i costrutti ad alto livello, come in Java, Python... ma se e solo se necessario si può tranquillamente scendere e gestire manualmente la memoria, come in C.
me ne sto accorgendo di questo fatto in prima persona.
Percaso hai qualche spunto teorico (un link, una dispensa,...) su una qualche procedura standard (un metodo concettuale intendo es. simili ai Design Pattern) per convertire in modo abb automatico una serie di funzioni di una libreria scritta in C, in uno pseduo-C++? Una specie di reverse engineering in codice O-O.
Non vorrei proprio ridurmi a riscrivere gli algoritmi, ma solo riscrivere con metodologia O-O il tutto.
"hamming_burst":
Te affermi (cmq è una politica di gestione...) di utilizzare il minimo possibile i puntatori, questo perciò implica il non utilizzo dell'allocazione dinamica.
Come gestisci il fatto che la memoria statica è limitata (i riferimenti sono ad allocazioni statiche), in C++ è disponibile più memoria a stack?
Puoi incapsulare la gestione della memoria dinamica in una classe. Prendi ad esempio std::vector.
int main() { std::vector<int> v; v.push_back(2); v.push_back(5); v.push_back(6); v.push_back(3); std::vector<int> v2 = v; v.push_back(9); ... }
Il client (tu) dichiara variabili sullo stack, che si comportano più o meno come i tipi di base (int, double...), puoi assegnarli, copiarli, modificarli... senza preoccuparti di allocare e deallocare memoria. In realtà std::vector usa internamente l'allocazione dinamica per memorizzare i suoi elementi, infatti puoi creare vettori molto grandi, ma questo è trasparente al client. È anche possibile usare allocatori personalizzati con i contenitori della libreria standard, nel caso ce ne fosse la necessità (es. per allocare da un pool di memoria condivisa).
In questo modo la gestione manuale della memoria è localizzata dentro la classe std::vector ed è più semplice verificarne la correttezza.
"hamming_burst":
Percaso hai qualche spunto teorico (un link, una dispensa,...) su una qualche procedura standard (un metodo concettuale intendo es. simili ai Design Pattern) per convertire in modo abb automatico una serie di funzioni di una libreria scritta in C, in uno pseduo-C++? Una specie di reverse engineering in codice O-O.
Non vorrei proprio ridurmi a riscrivere gli algoritmi, ma solo riscrivere con metodologia O-O il tutto.
No, mi spiace. Ho trovato questi due thread [1][2], non sono molto dettagliati ma magari riesci a trovare qualche spunto.
[1]: http://stackoverflow.com/questions/2761 ... -c-library
[2]: http://stackoverflow.com/questions/2690 ... -c-library
"claudio86":
[quote="hamming_burst"]Te affermi (cmq è una politica di gestione...) di utilizzare il minimo possibile i puntatori, questo perciò implica il non utilizzo dell'allocazione dinamica.
Come gestisci il fatto che la memoria statica è limitata (i riferimenti sono ad allocazioni statiche), in C++ è disponibile più memoria a stack?
Puoi incapsulare la gestione della memoria dinamica in una classe. Prendi ad esempio std::vector.
[..]
In questo modo la gestione manuale della memoria è localizzata dentro la classe std::vector ed è più semplice verificarne la correttezza.
[/quote]
in effetti così è più simpatica la questione.
No, mi spiace. Ho trovato questi due thread [1][2], non sono molto dettagliati ma magari riesci a trovare qualche spunto.
[1]: http://stackoverflow.com/questions/2761 ... -c-library
[2]: http://stackoverflow.com/questions/2690 ... -c-library
ti ringrazio, ho trovato qualche idea.
Ora vorrei chiederti come gestiresti te un caso particolare, che non riesco ancora a risolvere:
#include <iostream> #include <vector> #include <fstream> void ifd(std::ifstream d); std::vector<std::ifstream> aa; void creat(){ std::ifstream io("pippo.txt",ios::in | ios::binary); aa.push_back(io); } int main(){ ifd(aa.at(0)); }
Considera che il tutto fa parte di una classe, ma così è più semplice da scrivere...
Io avrei inserito come type del vector un tipo puntatore, ma cmq anche se allocco dinamicamente il tutto ho ancora errori. Come scriveresti le funzioni e il tipo, devo avere un vettore di ifstream.ù
Non pensavo che il C++ mi desse tutti sti pensieri...
Scusa il ritardo, ma ho passsatp qualche giorno di vacanza lontano da internet.
std::vector (ed anche altri contenitori) richiede che T sia un tipo copiabile ed assegnabile (*), cioè che sia definito un costruttore per copia e un operatore=. Questo perché l'implementazione è libera di fare copie degli oggetti contenuti. std::ifstream (e tutti gli stream) non sono copiabili, quindi non si può avere una variabile di tipo std::vector. Nemmeno i riferimenti sono assegnabili, quindi niente vettori di riferimenti. Non puoi nemmeno passare std::ifstream per valore come fai nella funzione ifd(), per farlo dovrebbe esistere un costruttore per copia.
Un'alternativa è usare un vettore di puntatori a std::ifstream, magari incapsulato in una classe, simulando un vettore di riferimenti.
Purtroppo non puoi ereditare da std::vector e sfruttare le funzioni già implementate, poiché i contenitori standard non hanno distruttore virtuale. Devi scrivere dei wrapper alle funzioni che ti servono. In questo modo il codice client non vede nemmeno un puntatore, ma solo riferimenti.
Oppure puoi semplicemente restituire un riferimento al vettore. Questo caso è meno sicuro poiché il client potrebbe modificare direttamente gli elementi del vettore, magari inserendo un puntatore nullo.
Questo modo è simile alla gestione manuale, però sei sicuro che alla fine tutti i file siano chiusi e la memoria liberata.
Alcuni sistemi impongono un limite sul numero di file aperti da un processo, quindi un vettore di file di lunghezza arbitraria potrebbe causare problemi. Forse sarebbe meglio cambiare completamente approccio, magari leggendo tutti i file all'inizio e memorizzando il contenuto. Però dipende da cosa ti serve esattamente.
*: Nel nuovo standard, C++11, il requisito è che T sia spostabile e assegnabile per spostamento (movable). Dovrebbe essere quindi possibile avere vettori di stream (però non ho un compilatore C++11 sotto mano e non posso controllare).
std::vector
Un'alternativa è usare un vettore di puntatori a std::ifstream, magari incapsulato in una classe, simulando un vettore di riferimenti.
class IfstreamVector { public: ~IfstreamVector() { for (int i = 0; i < v.size(); ++i) { std::ifstream * stream = v.front(); stream->close(); delete stream; } v.clear(); } void push_back(const std::string & filename) { std::ifstream * stream = new std::ifstream(filename.c_str()); v.push_back(stream); } const std::ifstream & operator[](int i) const { return *v[i]; } std::ifstream & operator[](int i) { return *v[i]; } private: std::vector<std::ifstream *> v; }; int main() { IfstreamVector a; a.push_back("/djkdk"); const std::ifstream & ss = a[0]; }
Purtroppo non puoi ereditare da std::vector
Oppure puoi semplicemente restituire un riferimento al vettore. Questo caso è meno sicuro poiché il client potrebbe modificare direttamente gli elementi del vettore, magari inserendo un puntatore nullo.
class IfstreamVector { public: ~IfstreamVector() { for (int i = 0; i < v.size(); ++i) { std::ifstream * stream = v.front(); stream->close(); delete stream; } v.clear(); } std::vector<std::ifstream *> v; }; int main() { IfstreamVector a; std::ifstream * stream = new std::ifstream(filename, mode); a.v.push_back(stream); ... }
Questo modo è simile alla gestione manuale, però sei sicuro che alla fine tutti i file siano chiusi e la memoria liberata.
Alcuni sistemi impongono un limite sul numero di file aperti da un processo, quindi un vettore di file di lunghezza arbitraria potrebbe causare problemi. Forse sarebbe meglio cambiare completamente approccio, magari leggendo tutti i file all'inizio e memorizzando il contenuto. Però dipende da cosa ti serve esattamente.
*: Nel nuovo standard, C++11, il requisito è che T sia spostabile e assegnabile per spostamento (movable). Dovrebbe essere quindi possibile avere vettori di stream (però non ho un compilatore C++11 sotto mano e non posso controllare).
Ti ringrazio molto! Son stati consigli molto di aiuto.
