Molte volte il C++, essendo un “figlio” del C, viene identificato con in puntatori. Altro non possiamo dire che “non è vero“.
Ma cominciamo con le basi. Nota bene: nel seguito utilizzeremo termini come “heap” e “stack“, anche se lo standard ISO C++ non ne fa menzione (di fatti non è detto che una piattaforma disponga di stack, ad esempio).
Tutti voi sapete che ogni variabile dichiarata in un blocco di codice, di fatti è allocata in modo automatico sullo stack, una memoria molto veloce ed estremamente limitata:
if (true)
{
int i; // Stack
i = 42;
}
Nell’esempio, la variabile i è allocata sullo stack. Ma cosa succede se dovessimo utilizzare classi molto onerose dal punto di vista dell’occupazione di memoria?
Lo stack non è più una opzione valida, perché è una memoria preziosa. Come hanno insegnato, è necessario allocare tutto sullo heap. E questo sicuramente vi fa venire in mente i puntatori:
if (true)
{
myHugeClass *p; // Stack
p = new myHugeClass(); // Heap
// ...
delete p;
}
Funziona tutto benissimo. Come si nota, il puntatore è allocato sullo stack, è dunque una variabile automatica, e la memoria è deallocata quando si esce dal blocco. Il dato, è invece allocato sullo heap dall’operatore new. Al termine dell’utilizzo della variabile p, dobbiamo ricordarci di deallocare la memoria, e questo è il compito dell’operatore delete.
C’è solo un piccolo problema: e se scordassimo il delete? Benvenuti nel tragico mondo dei memory leak.
La memoria non verrebbe mai deallocata, e la vostra applicazione allocherà nuovamente uno spazio per myHugeClass ogni volta che verrà eseguito il codice. Potenzialmente, potremmo saturare la RAM, con conseguenze catastrofiche. Come ovviare al problema? Essenzialmente dovremmo fare una accoppiata di tutti i new, con un delete, ma questo risulta impraticabile, come vedremo fra breve: non è sempre ovvio dove si trovi una deallocazione.
Smart Pointers
Lo standard ISO C++11 fornisce una soluzione elegante e semplice: utilizzare uno smart pointer, ovvero un puntatore “intelligente”, che dealloca la memoria automaticamente come se fosse sullo stack. La sintassi è estremamente semplice, e l’esempio precedente si riassume in questo codice:
if (true)
{
unique_ptr<myHugeClass> p(new myHugeClass()); // Heap
// C++14: auto p = make_unique<myHugeClass>();
// ...
}
Abbiamo risolto il problema. In questo caso, p è allocato sullo stack, mentre l’istanza di myHugeClass è allocata sullo heap, come ogni puntatore. La cosa “intelligente” degli smart pointers è che alla fine dello scope di p, tutta la memoria verrà automaticamente deallocata, sia ovviamente quello sullo stack, che quella sullo heap. Un delete non è più necessario, e dite addio al memory leak.
Solo per menzionarlo, un altro tipo di smart pointer è lo shared_ptr. Mentre lo unique_ptr consente che ci sia solo un “proprietario” dell’oggetto referenziato, lo shared_ptr permette di condividerne la proprietà:
auto ptr = make_shared<myHugeClass>();
auto ptr2 = ptr; // Istanza condivisa
La vita dell’oggetto puntato viene gestita tramite reference-counting, cioè si contano quanti shared_ptr referenziano l’oggetto puntato e quando uno shared_ptr viene distrutto è come se dicesse “io non sono più interessato alla vita dell’oggetto”. Quando l’ultimo shared_ptr viene distrutto si porta dietro anche l’oggetto puntato e il gioco è fatto!
Per chi viene dal mondo Java, potete vedere il parallelo facilmente: ogni oggetto in Java può essere pensato come uno shared_ptr (con un garbage collector). Solo che in C++ avete la possibilità di scegliere se usare un pointer o uno smart pointer.
Eccezioni
Una ulteriore motivazione sull’uso degli smart pointers al posto dei raw pointers, è nel caso di eccezioni. Prendiamo il secondo esempio. Se allochiamo con new una istanza di myHugeClass, ed il costruttore va in eccezione? A questo punto il codice è inutilizzabile, perché p punta ad una zona di memoria non valida. Cosa dovremmo fare per evitare la catastrofe di utilizzare un pointer invalido? Un semplice trucco consisterebbe nell’uso di try/catch:
if (true)
{
myHugeClass *p = nullptr; // Stack
try
{
p = new myHugeClass(); // Heap
// ...
delete p; // Rilascio la memoria: ho terminato
}
catch(...)
{
delete p; // Rilascio la memoria: errore rilevato
}
}
Sembra semplice, ma questo ci fa venire in mente il problema dell’accoppiamento new con delete. Mentre prima potevamo contare i new, e controllare che il numero di delete fosse uguale, ora non è più valida questa soluzione: abbiamo due deallocazioni, una per un funzionamento fisiologico, una per quello patologico con eccezioni. E la cosa si complica notevolmente con molte variabili e più modi di gestire varie eccezioni.
E dunque gli smart pointers ci aiutano? Certamente: è garantito dallo standard che, nel caso in cui una eccezione venga lanciata, la memoria debba essere automaticamente deallocata. Ecco come diventa l’esempio di prima:
if (true)
{
auto p = make_unique<myHugeClass>(); // C++14 style
} // Il delete è automatico
Conclusioni
Gli smart pointers sono utili classi da utilizzare sempre, ove possibile. Certo è che non è sempre praticabile l’uso degli smart pointers, alcune volte serviranno i cari vecchi raw pointers, ma sono casi particolari. In genere, utilizzare un raw pointer è sconsigliato.
Ma le buone notizie non terminano con questo. Se utilizzate ad esempio i containers, come vector, l’implementazione garantisce che la variabile sia sullo stack, mentre i dati siano allocati dinamicamente sullo heap. E questo non vale ovviamente solo per vector!
Ancora più interessante è il caso in cui voi vogliate usare una funzione che come valore di ritorno ha una istanza molto grande, come ad esempio un vector con molti elementi. Potreste pensare che, associando una variabile al valore di ritorno della vostra funzione, venga copiato ogni elemento dentro il vettore, con chiamate a non finire al costruttore dell’oggetto contenuto nel vector (ad esempio un vettore di myHugeClass): questo sarebbe un overhead enorme. In realtà, dipendentemente dal compilatore però, il C++ fornisce una soluzione automatica, non copiando l’elemento, ma muovendolo, con una tecnica semplice chiamata return value optimization. Ma questa, è un’altra storia.
Ottima l’osservazione su make_unique da C++14 in poi, che di fatto elimina l’ultimo ragionevole motivo per dover usare new() nel proprio codice.
Personalmente non ho mai avuto problemi di memoria non rilasciata quando uso i puntatori o se vuoi i raw pointers. Ovviamente devi usarli nel modo corretto. Ma questo dipende da quanto codice scritto male hai alle spalle.
Capisco che per ragioni di spazio il contenuto dell’articolo ne risente.
Una piccola nota in cui non sono d’accordo con quanto racconti.
– Lo stack non è estremamente limitato. Ha un suo limite di spazio dovuto alla sua natura o scopo. Da questo punto di vista si può dire che anche lo heap ha un suo limite.
– L’uso di dati allocati nello stack non sono più “veloci”, immagino da usare, di dati allocati nello heap. Visto che entrambe le zone risiedono in memoria principale. Semmai dipende da come il compilatore ottimizza il tuo codice e quindi alcune dati possono riesiedere nelle memoria di primo livello, secondo etc. Ma questo non dipende dall’allocazione nello stack o nello heap.
– Ma cosa succede se dovessimo utilizzare classi molto onerose dal punto di vista dell’occupazione di memoria? Lo stack non è più una opzione valida, perché è una memoria preziosa. Come hanno insegnato, è necessario allocare tutto sullo heap
Personalmene solo una o due volte in tutti i mei anni di sviluppo ho superato lo spazio di stack associato ad una funzione. E quando è avvenuto ho potuto riscrivere la funzione in modo tale da evitare il problema. Si può dire che non capita così spesso che una istanza di una classe (o più calssi) dichiarate in una funzione (o in un blocco di codice) superi lo spazio di stack disponibile per quella funzione.
Lo stack è prezioso tanto quanto lo heap.
Chi ha insegnato ad allocare tutto sullo heap?!?!
Ok. Se si usano dati allocati nello stack si ha il grosso vantaggio di avere lo spazio riservato nello stesso a tempo di compilazione. Il dato viene riservato in memoria (stack) all’entrata della funzione (o del blocco di codice) e quando si esce dallo spazio di visibilità della variabile esso viene autmaticamente deallocato, senza nessun onere da parte di scrive il codice.
Viceversa, l’allocazione di memoria nello heap avviene solo a runtime e questo ha un costo in termini di velocità. In più se si usa lo new e delete spesso si ha il grosso svantaggio di frammentare lo heap. E’ chiaro che con l’hardware che abbiamo oggi, cpu e ram, il runtime cost per il new/delete e la frammentazione dell ram, sono superflui ma questo non vuol dire che i costi non si hanno.
Personalmente e sopratutto rivolto ai giovani sviluppator/programmatori in C++ suggerirei di usare molto spesso variabili allocati nello stack e di evitare quando possibile l’allocazione nelllo heap. Quando è necessario l’allocazione nello hep, allora usate gli smart pointers. Così evitano di farvi pensare all’uso del delete.
Giusta l’osservazione sul fatto di valutare bene se sia effettivamente necessario allocare una risorsa (non solo memoria) sullo heap prima che sullo stack. Ma l’importnza degli smart pointer sta nella condivisione *sicura* di risorse “fra scope diversi”: qui ovviamente l’opzione stack non c’entra più niente (forse l’articolo poteva evidenziare meglio questo concetto tramite un semplice factory quando accenna alla proprietà di reference-counting).
Ciao. Premetto che sono meno di un neofita, ma mi sembra che il codice
();
();
auto ptr = make_shared
auto ptr2 = ptr2; // Istanza condivisa
dovrebbe invece essere
auto ptr = make_shared
auto ptr2 = ptr; // Istanza condivisa
Confermate?
Vorrei chiarire meglio quel punto dell’articolo in cui si dice “Potenzialmente, potremmo saturare la RAM, con conseguenze catastrofiche.”
Innanzitutto, una precisazione scontata: l’esaurimento della memoria può avvenire a prescindere dal fatto se il programma soffra di memory leaking o meno (ovviamente, l’inconveniente diventa più probabile nel primo caso).
In realtà, la vera “catastrofe” per un programma non è quando è previsto che termini, cosa che accade di default se new operator esaurisce la memoria. Almeno, ciò non è tanto catastrofico quanto i ben peggiori “comportamenti indefiniti” a cui lo standard, invece, ci abbandona quando il programma commette operazioni non consentite.
Mi spiego meglio. “l’overload throw” di operator new() (richiamato da new operator) ha un comportamento ben definito dallo standard nel caso in cui non ci sia memoria libera per allocare un oggetto in memoria: prima di tutto fa un nuovo tentativo di recupero della memoria invocando il new_handler correntemente installato e, solo nel caso in cui quest’ultimo dovesse fallire (o non fosse presente), viene sollevata un’eccezione di tipo bad_alloc. Se l’eccezione non viene gestita il programma termina.
Il programmatore ha quindi tre possibilità per influenzare il comportamento predefinito:
1. definire ed impostare un new_handler ad-hoc
2. gestire l’eccezione di tipo bad_alloc;
3. ignorare l’eccezione lasciando che il programma termini in maniera voluta.
Nel primo caso si può tentare una strategia di recupero “intelligente” della memoria libera, che individui ed elimini, per esempio, eventuali memory leaks (cosa apparentemente difficile ma non impossibile).
Nel secondo caso, il più ovvio, si gestisce l’eccezione con i noti meccanismi, “rinunciando”, evidentemente, a perseguire la stessa strada intrapresa prima.
Nel terzo caso si lascia terminare il programma a seguito dell’invocazione di std::terminate(), che a sua volta chiama il terminate_handler correntemente installato, che di default chiama abort(). Se la terminazione con abort() non è tollerabile, quando anche il normale meccanismo di gestione delle eccezioni è risultato insufficiente, si può pensare di installare, come misura drastica, un terminate_handler ad-hoc, che porti l’esecuzione ad un nuovo livello della strategia di tolleranza degli errori.
Ottima risposta, c’è ben poco da aggiungere 🙂
Per quanto riguarda lo stack, gli smart pointer garantiscono di uscire con sicurezza da uno scope dove c’è stata una “allocazione condizionale”: quando le variabili sono sullo stack, l’allocazione è incondizionata:
{
BigObject b; // viene sempre allocato
DoAllTheWork(&b);
}
nessuno smartpointer è necessario, a meno che BigObject metta il programma a rischio di esaurire lo stack (v. sotto).
ma se il listato fosse:
// non so se l’oggetto mi servirà o no
BigObject* p = nullptr;
if (IsReallyNeeded())
p = new BigObject;
DoAllTheWork(p);
se DoAllTheWork può lanciare eccezioni, in questo caso è bene proteggersi con uno smartpointer che distrugga p all’uscita.
Addirittura, è frequente questo “idiom”:
void f(SomeType* use_me_and_delete_me) auto_del(use_me_and_delete_me);
{
std::unique_ptr
// continua…
}
Un ultimo inciso: la dimensione dello “stack” è molto variabile, in molti casi è davvero “estremamente limitata”. I programmatori Windows sono abituati bene, in altre piattaforme i thread secondari hanno qualcosa come 2K o 4K. il tipico bug è:
void f()
{
double data[10000];
// blabla
}
int main()
{
f(); // ok, funziona tutto…
}
poi qualcuno modifica il codice in modo che f venga eseguita in un altro thread… questo causa l’abort immediato dell’intero programma (al momento dell’esecuzione, il compilatore non se ne accorge…), quindi a volte è meglio non rischiare.
Ovviamente, quando l’articolo suggerisce di allocare “tutto dall’heap”, non significa “tutto”, ma “tutti gli oggetti di un certo peso” (oppure, come rilevava il commento precedente, gli oggetti che devono lasciare lo scope, ma questo è inutile dirlo).
Una nota per gli amministratori del sito: non riesco a vedere la possibilità di rieditare la risposta per correggere (imbarazzanti) errori di grammatica di cui purtroppo mi accorgo solo dopo aver riletto la risposta stessa quando già è stata inviata e impaginata. Mi è stato detto che esiste un pulsante “Modifica” sotto il testo della risposta, ma l’unica cosa che vedo personalmente è un pulsante “Rispondi”. E’ possibile fare qualcosa? Grazie.
E’ in lista. Grazie della pazienza
Luca, ho installato un plugin che pare funzionare con un utente di prova. Puoi verificare? Grazie del feedback
Va bene, adesso vedo un link “clicca per modificare” che mi ha consentito di correggere la risposta. La modifica è in attesa di approvazione del moderatore: quindi, una volta approvata, dovrei vederla finalmente in linea. Non ho dubbi che tutto funzioni questa volta; facciamo che se non è così ti faccio un fischio 🙂 Grazie per l’aiuto.
Sì esatto, la modifica viene inviata di nuovo in moderazione (te l’ho appena riapprovata e dovresti vederla modificata). Grazie a te!
Nota bene: nel seguito utilizzeremo termini come “heap” e “stack“, anche se lo standard ISO C++ non ne fa menzione
Diciamo che lo standard introduce “indirettamente” i due concetti come proprietà *[della vita] degli oggetti* più chè della memoria in sè. Si veda il paragrafo”3.7 Storage duration” (draft N3960), in particolare “3.7.3 Automatic Storage Duration” e “3.7.4 Dynamic Storage Duration”. E poi anche il paragrafo “12.5 Free Store”, che indica appunto gli operatori usati per gestire la vita degli oggetti allocati sull’heap (“free store” è appunto sinonimo di “heap”)
Ciao CodeVisio,
scusa il ritardo nella risposta! Questo articolo è un articolo “beginner”, quindi non possiamo trattare cose molto avanzate.
Ma veniamo alle obiezioni, che sono abbastanza utili per una discussione.
Saturare lo stack è molto semplice, come dice Davide! Se però lo spazio richiesto è molto esiguo, è difficile notarlo. Io nel mio lavoro ho bisogno di spazi molto grandi, qualche giga, e lo stack muore (almeno nella mia piattaforma), prova anche tu:
Se usi la versione stack, va ovviamente in core dump. L'esempio è accademico, io alloco molta più RAM, quindi lo stack è improponibile.
Diciamo così, si usino variabili nello stack se piccole, nello heap se grandi.
à plus!
PS. Uso Clang 3.2 (-std=c++11 -stdlib=libc++), OSX 10.8.4, compilato per x86_64.
Vero, Luca, lo standard parla di storage duration, dynamic storage & co. Ma come in ogni documento (perdona la parola) legale, una cosa non detta non si intende sottointesa. 🙂
Non posso che concordare con quello che hai scritto. L’articolo non va assolutamente nei dettagli (ho usato un tag “basic” apposta), per questo usiamo i commenti, per mettere dettagli che l’articolo non può contenere! 🙂
Ciao a tutti e grazie per la partecipazione. Vorrei intervenire solo per fare una precisazione: come specificato nei tag di questo articolo, il livello è “basic” ovvero orientato ad iniziare e non ad essere molto deep. Questo perché abbiamo diversi utenti begineer o che stanno provando a ricominciare a programmare in C++ (l’hanno lasciato molto prima del C++11).
Sicuramente in futuro tratteremo anche argomenti più avanzati e ripasseremo per topic già discussi ma con occhio più attento e preciso. Questo articolo, ad esempio, vuole solo essere un’introduzione “leggera” agli smart pointer, al fatto che esistono e i motivi principali per utilizzarli.
Ringrazio nuovamente tutti e, tra l’altro, ricordo che se avete voglia di pubblicare le vostre idee (anche molto avanzate) potete diventare autori e inviarci i vostri contenuti!
Ciao CodeVisio, vorrei solo commentare la tua prima affermazione:
“Personalmente non ho mai avuto problemi di memoria non rilasciata quando uso i puntatori o se vuoi i raw pointers. Ovviamente devi usarli nel modo corretto. Ma questo dipende da quanto codice scritto male hai alle spalle.”
Io sono sicuro che tu non abbia mai avuto problemi di questo genere, il problema è quando lavori in un team perché gli altri purtroppo possono sbagliare. Oppure quando lasci il tuo codice in mano a gente meno in gamba di te, che magari è molto più lenta a manutenerlo, ecc.
Questo solo per dire che nel C++11 ci sono moltissimi strumenti volti a migliorare la produttività singola e di un intero gruppo di lavoro. Se usati bene e nel contesto giusto, gli smart pointer aiutano a scrivere meno codice con anche maggiore livello di sicurezza (e.g. exception safety).
Un’ultima nota: perché usare oggetti allocati sullo heap? Un altro motivo molto semplice: polimorfismo? Classi astratte? Dependency Injection? Puoi ottenere queste cose anche con delle reference, ma il gioco si fa più complicato.
Ciao franco,
non mi aspettavo una risposta celere, quindi personalmente non c’è niente di cui scusarti.
Sarà l’orario ma non sono riuscito a leggere la nota riguardo la natura ‘beginner’ dell’articolo.
“Saturare lo stack è molto semplice, come dice Davide! Se però lo spazio richiesto è molto esiguo, è difficile notarlo.”
Certo, se lo fai a posta come esempio è normale che lo esaurisci. Discorso diverso è l’uso quotidiano dello stack. Quind, stai parlando di un caso specifico o di una prassi quotidiana?
“Io nel mio lavoro ho bisogno di spazi molto grandi, qualche giga, e lo stack muore (almeno nella mia piattaforma), prova anche tu”
Ripeto, a me è capito pochisseme volte su codice facente parte di un programma usato da tantissime persone.
Il discorso è questo, franco.
Se devi fare degli esempi accademici o estremizzare alcuni punti per spiegare un concetto mi va bene, ma se presenti la possibilità di esaurire lo stack come se fosse un cosa che può capitare, con notevole probabilità (coem da tuo articolo), nel lavoro quotidiano (in C++) allora il discorso è diverso.
il concetto di “variabili piccole o grandi” è relativo. Cosa vuol dire variabili piccole o grandi per franco? e la misura è la stessa per sempronio e caio?
Non è una questione dell’articolo ‘beginner’ o avanzato, è che se le cose le tieni per te è un fatt ma se vuoi trasmettere questa tua conoscenza o opinione ad un pubblico potenzialmente vasto devi fare due conti.
Io, personlmante, non punterei sul faftto che l’articolo è breve o ‘advanced’, la cosa che conta è sottolineare che l’ìinformazione da te trasmessa è una tua opinione o è un fatto concretto indipendente da valutazioni soggettive.
Io non sarei capace di scrivere un periodo di senso compiuto in italiano e non mi cimento nel farlo, ma se dovessi farlo mi aspetterei di scrivere cose certe.
Ciao marco,
“Io sono sicuro che tu non abbia mai avuto problemi di questo genere,”
Io ho poche certezze, invidio te che hai certezze riguardo altri che non conosci.
” il problema è quando lavori in un team perché gli altri purtroppo possono sbagliare”
In qualsiasi mestiere si sbaglia. E anche le persone con esperienza sbagliano, meno frequetemente, se si suppone che imparino dai propri sbagli, ma sicuramente sbagliano anche loro.
“Oppure quando lasci il tuo codice in mano a gente meno in gamba di te, che magari è molto più lenta a manutenerlo, ecc”
Non c’entra il concetto di essere in gamba. C’entra il concetto di quali cose sai per certo. Il team si forma con il tempo. E’ chiaro che un neofita commetter degli sbagli di natura indipendente dal settore e anche (ovviamente) dipendente dal settore. Ma nel team si presuppone che ci siano persone con esperienza più vasta. O, in caso negativo, si presuppone che chi commette uno sbaglio lo riconosca e impari a non ripeterlo.
“Questo solo per dire che nel C++11 ci sono moltissimi strumenti volti a migliorare la produttività singola e di un intero gruppo di lavoro. Se usati bene e nel contesto giusto, gli smart pointer aiutano a scrivere meno codice con anche maggiore livello di sicurezza (e.g. exception safety).”
Non è una questione dell’ X linguaggio, è una questione di approccio. Io so Y e non so T. Cosa consco per certo? e per averlo messo in pratica? limiti? punti di forza?
Attenzione ad usare la parola “produttività”.
“Un’ultima nota: perché usare oggetti allocati sullo heap? Un altro motivo molto semplice: polimorfismo? Classi astratte? Dependency Injection? Puoi ottenere queste cose anche con delle reference, ma il gioco si fa più complicato.”
Stai parlando in generale o di casi particolari? Non ho mai detto che usare la memoria allocata nello heap sia sbagliato o non utile.
Ritorno al concetto di “produttività”.
Cosa vuol dire produttività per marco? la quantità di codice scritto per secondo?
O quanto guadgana la tua azienda per codice scritto?
Occhio che quella parola vuol dire tutto come vuol dire nulla, ma putroppo ha un fore senso commerciale.
I puntatoti intelligenti aumentano la mia produvvitaà? sei sicuro? cioè riesco a produrreo molto più codice di quello che produrrei se non li usassi?
O mi permettono di evitare alcuni errori? (e quindi c’entra poco con la produvvità intesa come quantità di codice scritto).
Se è così, allora posso non ottenere lo stesso effetto se non uso i puntatoiri intelligenti? o serve solo a non farmi pensare a rialsciare la memori allocata, cosa a cui ho pensato quando l’ho allocata?
Siccome il discorso si fa lungo mi limito a dire sottolineare i lseguente fatto.
Mia personale opininone: nel tempo si è sottovalutao il concetto di professionalità nella scrittua di codice. Ci si è dimenticati che chi scrive codice in teoria è un professionista con tanto di studi “universitari” (o lavoro professionale) alle spalle. Non è poi così diverso da uno che progetta un ponte su cui passano migliagia di auto al giorno e quindi con tutta una serie di requisiti. Invece, si è passati da un lavoro professionale con delle responsaibilità, il software architech/designer/developer/engineer (o cioò che più ti aggrada), ad un “smanettone” con tutto il rispetto per gli smanettoni.
Come in tutte le professioni ci vuole esperienza (ottenuta con anni di lavoro)per ottenre degli effetti qualitativi alti e teoria. E occorre anche una forte voglia di imparare dagli sbagli. Pensa un po’ al software, non del dentista sotto casa, ma a quello real time che gira sugli aeroplani.
Oppure ribalta il punto di vista: se ti dicessi “non ti preoccupare, marco, ho notato che nel 99% tu non badi a rilasciare la memoria che esplicitamente hai rischiesto di allocare, ci penso io, tu non ti devi preoccupare, pensa ad altro, concentrati sul tuo business layer, vedrai che aumenterai la tua produttività e avrai più guadagni!”. Mi sembra molto pià appetibile quest’ultima ipotesi non trovi?
P.S. non mi serve il preambolo diplomatico sul “sicuramente tu sei bravo” etc. Mi accontento dell’opinione sui fatti.
Io non credo sia solo un problema di professionalità.
Il software dell’Ariane 5 ha mandato in fumo 500M$ a causa di una exception non gestita (http://www.savive.com/casestudy/ariane5.html)
Il buon senso ci insegna che se si possono prendere delle precauzioni è meglio farlo perché anche il più attento dei professionisti sbaglia, tutto qui.
Al contrario del C, C++ sceglie la strada delle eccezioni su cui si fonda tutta la Standard Library. Questo rende decisamente più infido scrivere codice intrinsecamente a prova di leak.
La soluzione più semplice è l’adozione del paradigma RAII (http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) che funge come la rete del trapezista. In alcuni casi può avere un minimo di overhead ma complessivamente fornisce i benefici impagabili spiegati da Franco.
Il mio punto di vista è che sia necessario trasmettere il messaggio che gli strumenti di C++ sono molto diversi da quelli del linguaggio C. Io scrivo ancora assembler oggi (ormai poco su PC, molto di più su microcontrollori) ma questo non mi allontana dagli strumenti più evoluti che offre il nuovo standard con in primis unique_ptr e shared_ptr.
Apprezzo l’articolo di Franco proprio perché è necessario far conoscere i nuovi strumenti e dire chiaramente che C++ e C sono due linguaggi molto diversi, poi starà al lettore decidere se usarli o meno.
“In qualsiasi mestiere si sbaglia. E anche le persone con esperienza sbagliano, meno frequetemente, se si suppone che imparino dai propri sbagli, ma sicuramente sbagliano anche loro.”
Ok, ha senso, ma quindi vuol dire “lasciamo che tutti sbaglino così imparano dai loro errori”? Quindi meglio curare che prevenire? Sono d’accordo che sbagliando si impara, ma penso anche che in contesti con responsabilità e quattrini (e nel software ne girano parecchi) gli errori a volte si pagano caro e a seconda dei settori possono accadere anche cose parecchio spiacevoli (ops, crash del software per la dialisi). Se alcuni strumenti aiutano a prevenire perché non usarli? E quando dico “alcuni” intendo che molte cose, spesso, “fanno brodo”.
Produttività: visto che scrivere software non è una scienza esatta, la produttività (termine di fabbricazione economica) applicata al software è un concetto vago, hai ragione. Mi piace pensare alla produttività come una misura di efficienza, ovvero quanto valore riesci a ottenere in quanto tempo. Cos’è il valore? Questo è l’elemento “vago” del concetto di produttività. Può essere il numero di linee prodotte, può essere il numero di bug fixati, i task completati… Dipende dalle metriche con cui lavori, può essere deciso dall’alto, può essere una convenzione, può anche non esistere proprio. In questi termini anche prevenire un errore è incluso nel fantomatico concetto di produttività. Oppure riuscire a fare una modifica in 2 ore invece che in due giorni.
Mi trovi in perfetta sintonia quando dici che scrivere software è un’attività professionale. Mi paragono sempre ad un “artigiano specializzato”. Non dimentichiamo che con buona probabilità i sistemi software sono i più grandi manufatti di sempre. Sottolineo manufatti, cioè scritti a mano da qualcuno. Lasciamo stare generatori di codice e librerie su librerie che astraggono un po’ di cose. Ed essere specializzati richiede esperienza, formazione, passione (molti l’hanno persa), saper sbagliare, …
Ultima cosa: il software è comunque un business, le responsabilità sono divise, vengono creati diversi layer (tra cui quello di business) e ci lavorano diverse figure professionali. Ha probabilmente senso che esista un business layer che non sappia nulla che sotto ci sono dei puntatori, non lo so. A me piace il controllo del mio software, quindi voglio capire cosa c’è sotto, però mi rendo conto che in molti contesti è complicato. “Aumenterai la produttività e avrai più guadagni” è molto appetitosa per chi fa business o sta su un layer più alto. Ad esempio, se la ditta X fa piccoli giochini per smartphone, forse comprare un game-engine che fa tutto quello che serve è una soluzione sensata e può garantire guadagni elevati. La domanda è sempre: dove sei? Dove vuoi arrivare?
Ciao CodeVisio,
l’esempio che ti ho postato è accademico in un senso: nessuno alloca SOLO per allocare. Tutt’altro è nel mondo reale, t’assicuro. Magari non ne hai mai sentito il bisogno, ma una mole di dati elevata è all’ordine del giorno soprattutto ora (“big data” è la nuova buzzword). Proprio ora ho scritto un software che utilizza qualche giga di RAM, fai tu!
Fosse una mia opinione, ti lascio con il dubbio: perché hanno usato “new”? Bastava l’allocazione statica sullo stack?
Non voglio poi aggiungere nulla sul fatto che, se usi solo lo stack, non puoi avere una lista (come già detto da altri), ad esempio, di oggetti derivanti da un unico padre. Se pensi alle GUI, al FEM, alle mesh, insomma ci sono milioni di modi in cui dovresti usare l’heap.
à plus!
PS. Non vedi “beginner”, quindi confermi che non vedi alla fine dell’articolo la scritta Tags:basic, containers, puntatori, stl. Puoi dare un riscontro?
Ciao,
a me sembra un buon articolo dall’evidente taglio basic. Lo trasformerei in una serie.
Credo che sarebbe utile.
Puntatori 1, basic
Puntatori 2, cominciamo a vedere polimorfismo (es. una factory che restituisce un StdoutLogger piuttosto che un FileLogger), vediamo anche qualche differenza sul campo fra unique e shared (es: personalmente preferisco che la factory restituisca unique)
poi in altri episodi…vediamo gli altri smart pointers, smart pointers nei container, smart pointers e threading ecc. ecc.
P
[…] e ownership “manuale”. Questo implicherebbe una serie di problemi discussi anche qui. Ma come unire i benefici delle variabili automatiche (e della RAII) con l’allocazione […]