posts - 644, comments - 2003, trackbacks - 137

My Links

News

Raffaele Rialdi website

Su questo sito si trovano i miei articoli, esempi, snippet, tools, etc.

Archives

Post Categories

Image Galleries

Blogs

Links

Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Festeggiamo la Service Pack 1 di Visual C++ 2008 perché ha rilasciato l'implementazione delle TR1 (Technical Report 1 della commissione ISO C++) la maggior parte delle quali è in sostanza l'integrazione della celeberrima libreria boost la cui integrazione nel compilatore VC++ era possibile ma non proprio una passeggiata.

Adesso non ci sono più scuse, se ancora con auto_ptr (uno smart pointer che sembrava più che altro un fool pointer) le cose erano difficili, adesso finalmente shared_ptr fa dimenticare cosa sia un puntatore.

NON sto dicendo che VC++ cambi nulla del linguaggio, anzi, implementa sempre di più quelli che sono gli standard ISO. Sto invece dicendo che grazie a queste nuove librerie, e senza installare nulla di extra, è possibile scrivere codice decisamente più pulito e realmente object oriented.

Al linguaggio C++ usato come superset del C ho sempre detto 'no grazie'.

Dimenticare i puntatori non è una penalizzazione ma un importante salto di qualità che possiamo far fare ai nostri listati. Ogni volta che usiamo un puntatore ci leghiamo indissolubilmente a una specifica zona di memoria e quindi all'hardware sottostante. Future tecnologie come transactional memory saranno inutilizzabili con listati che fanno pesante uso di puntatori.

Prendiamo ad esempio una funzione che internamente chiama una API Win32 che trasforma un SID in nome dominio e utente.

Nonostante l'uso di classi, la funzione lavora in modo C-Style. La prima chiamata a LookupAccountSid serve a farsi dire quanto devono essere grossi i buffer allocati e la seconda recupera i dati, avendo preventivamente allocato un buffer adeguato. Il problema è sempre il solito, le malefiche delete[].

Inoltre la funzione, che funge da Factory, restituisce una nuova istanza come puntatore, lasciando al chiamante il compito di ricordarsi di disallocare la memoria.

static Account* GetAccountFromSid(PSID Sid)
{
    Account *pAccount = new Account();
    pAccount->_Sid = Sid;

    LPWSTR Name, Domain;
    DWORD NameLen = 0, DomainLen = 0;
    SID_NAME_USE eUse = SidTypeUnknown;
    LookupAccountSid(NULL, Sid, NULL, &NameLen, NULL, &DomainLen, &eUse);
    if(NameLen == 0)
    {
        pAccount->_LastError = ::GetLastError();
        return NULL;
    }
    Name = (LPWSTR)new BYTE[NameLen * 2];
    Domain = (LPWSTR)new BYTE[DomainLen * 2];
    if(!LookupAccountSid(NULL, Sid, Name, &NameLen, Domain, &DomainLen, &eUse))
    {
        pAccount->_LastError = ::GetLastError();
        delete [] Name;
        delete [] Domain;
        return NULL;
    }

    pAccount->_Name = Name;
    pAccount->_Domain = Domain;
    delete [] Name;
    delete [] Domain;

    return pAccount;
}

Una implementazione alternativa che fa uso di shared_ptr è la seguente (Account e SecurityIdentifier sono la stessa classe che ha subito refactoring):

static SecurityIdentifierPtr GetFromSid(PSID Sid)
{
    SecurityIdentifierPtr spSecurityIdentifier(new SecurityIdentifier());
    spSecurityIdentifier->StoreSid(Sid);

    DWORD NameLen = 0, DomainLen = 0;
    SID_NAME_USE eUse = SidTypeUnknown;
    LookupAccountSid(NULL, Sid, NULL, &NameLen, NULL, &DomainLen, &eUse);
    if(NameLen == 0)
    {
        spSecurityIdentifier->_LastError = ::GetLastError();
        return SecurityIdentifierPtr();
    }

    shared_ptr<WCHAR> spName(new WCHAR[NameLen]);
    shared_ptr<WCHAR> spDomain(new WCHAR[DomainLen]);
    if(!LookupAccountSid(NULL, Sid, spName.get(), &NameLen, spDomain.get(), &DomainLen, &eUse))
    {
        spSecurityIdentifier->_LastError = ::GetLastError();
        return spSecurityIdentifier;
    }

    spSecurityIdentifier->_Name = spName.get();
    spSecurityIdentifier->_Domain = spDomain.get();
    spSecurityIdentifier->_SidType = eUse;
    return spSecurityIdentifier;
}
 

Il puntatore al buffer interno è allocato e mantenuto all'interno dello smart pointer e spName.get() permette il passaggio del puntatore senza mai il rischio di subire un memory leak.

Inoltre anche il passaggio dello smart pointer della istanza creata con il metodo Factory al chiamante viene fatta con lo smart pointer SecurityIdentifierPtr così definito:

class SecurityIdentifier;    // forward declaration
typedef shared_ptr<SecurityIdentifier> SecurityIdentifierPtr;

class SecurityIdentifier
{
...

Cioè abbiamo definito SecurityIdentifier come un nuovo tipo che equivale a shared_ptr<SecurityIdentifier>

Lo smart pointer verrà distrutto alla fine del metodo e ricostruito in uno nuovo del chiamante:

SecurityIdentifierPtr spsid = SecurityIdentifier::GetFromSid(Info.Sid);

 

Che dire, sembra codice tanto pulito quanto quello di C#! ... ma con tutti i vantaggi di C++ :-)

Una volta costruite le classi C++ native, non rimane che wrapparle con una dll C++/CLI e usare il tutto da C#. E con questo ci possiamo dimenticare di quelle cervellotiche dichiarazioni PInvoke.

 

Dulcis in fundo, shared_ptr può anche essere usato dentro i container come list e vector delle STL, creando così liste di puntatori che disallocano correttamente gli oggetti puntati (e quindi senza i memory leak che sarebbero derivato dall'uso di questi container con auto_ptr).

class Aces : public list<AcePtr> {};

 

Fino a prima del Feature Pack (adesso assorbito dalla SP1 di VS2008) mi ero arrangiato con smart pointer casalinghi e liste di puntatori casalinghe, etc. Ma ora è decisamente tutto più pulito e le novità delle TR1 sono ovviamente molte molte di più e tutte da scoprire.

Print | posted on martedì 26 agosto 2008 03:23 |

Feedback

Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

questa si che è una bella notizia!
26/08/2008 03:33 | Fabrizio Lapiello
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Ricordo che quando seguivo l'Ansi C il mio peggiore incubo erano i puntatori, perchè a causa di un mio limite di puntatori non ho mai capito tanto. L'esame di C l'ho superato perchè ero riuscito a non usare i puntatori (anche se era il topic dell'esame) mi sono preso 24 e non usato più l'ansi C per il motivo sopraddetto.
26/08/2008 10:23 | #gio#
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Ciao Andrea,
nel tentativo di rendere più semplice il listato, ho tralasciato questo importantissimo aspetto. Grazie per avermelo ricordato.
La soluzione non è purtroppo quella di usare shared_array che non c'è nelle TR1 implementate nella SP1 di VS2008.
Però la soluzione è semplice e cioè di 'pluggare' un disallocatore custom:

template<class T>
class ArrayDeleter
{
public:
void operator()(T *p)
{
delete[] p;
}
};

Usare il disallocatore è banale:
shared_ptr<BYTE> s(new BYTE[100], ArrayDeleter<BYTE>());

Però così comincia a diventare complesso e non più C# like :) ... quindi ecco la soluzione più pulita:

template<class _Ty>
class shared_ptrarr : public shared_ptr<_Ty>
{
public:
shared_ptrarr()
: shared_ptr()
{
}

template<class _Ty>
shared_ptrarr(_Ty *_Px)
: shared_ptr(_Px, ArrayDeleter<_Ty>())
{
}

};

In pratica derivo la shared_ptr per fornire ArrayDeleter e il suo template senza doverlo specificare tutte le volte.
Il suo uso è come quello nell'esempio iniziale:

shared_ptrarr<BYTE> q(new BYTE[100]);

Ma si può fare di meglio (ed è quello che ho nel mio listato reale, da cui puoi capire il perché della mia 'svista' nel riadattare il listato per il blog):

typedef shared_ptrarr<BYTE> BytePtr;

E il suo uso diventa veramente C# like :)))

BytePtr p(new BYTE[100]);

Quando nelle librerie ci sono basi toste come boost ha sempre dimostrato, quello che segue non può che essere fantastico :)
27/08/2008 00:41 | Raffaele Rialdi
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

@Gio. Occhio però che i puntatori sono un concetto indispensabile per *qualsaisi* linguaggio userai, anche C# e vb.net.
Il vantaggio a non usarli esplicitamente è quello di scrivere ad un livello più elevato senza rinunciare a smanettare a basso livello quando realmente serve.
27/08/2008 00:44 | Raffaele Rialdi
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

@Gianluca. ROTFL!!! ... e per il gruppo su facebook secondo me se lo apri fa dei gran numeri :-D
27/08/2008 00:46 | Raffaele Rialdi
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Sigh, è veramente tanto tempo che non scrivo codice C++, debbo dire che mi manca e poi vedere le nuove estensioni in azione e decisamente interessante.

alk.
27/08/2008 14:21 | Gian MAria
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Con riferimento a codice tipo:

shared_ptr<WCHAR> spName(new WCHAR[NameLen]);

IMHO non occorre scomodare shared_ptr... (o shared_ptrarr).

Infatti, già prima del rilascio del TR1, le STL (incluse in VC6, VS2003, VS2005, VS2008) forniscono l'ottimo container std::vector.

Quindi, se si ha l'esigenza di allocare un array di caratteri (WCHAR) dinamicamente, in modalità RAII ed exception-safe (quindi senza usare esplicitamente delete[]), è sufficiente:

vector< WCHAR > name( NameLen );

E se si vuole ottenere il puntatore WCHAR *, basta semplicemente:

&name[0]

:-)

shared_ptr è utilissimo invece in altri contesti, come scrive lo stesso Raf a proposito dei contenitori STL di puntatori.
O ancora, ottimo l'esempio da lui fornito, in cui si ritorna SecurityIdentifierPtr (smart pointer) invece del "raw" pointer.
Questi sì che sono dei veri punti di forza di shared_ptr.

@Andrea: E' vero, puoi usare Boost (e quindi boost::shared_ptr) a partire da VS.NET 2003 (A.K.A. VC++7.1).
Però uno dei grandi vantaggi dell'implementazione del C++ Feature Pack rilasciata dal Visual C++ Team è che ottieni i *visualizer*. Questi si integrano benissimo nel debugger di VS2008, e offrono un'ottima esperienza di debugging.
IMHO questo è uno dei principali vantaggi del VC++ Feature Pack TR1 rispetto a Boost.

My 2 cents,
Giovanni


27/08/2008 15:11 | Giovanni Dicanio
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Giovanni però AFAIK vector non garantisce l'allocazione contigua. L'allocazione contigua è il modo preferito per vector di allocare memoria ma l'implementatoe potrebbe scegliere un modo differente perché le specs (afaik) non lo obbligano ad allocare in modo contiguo.
Visto che quella allocazione nell'esempio viene passata ad una API, la contiguità è fondamentale.

IMHO il vero vantaggio del feature pack / SP1 è quello di essere supportato ufficialmente da Microsoft. Quando una enterprise sviluppa un prodotto basandosi su una libreria, non solo deve avere supporto ufficiale, ma anche la garanzia di non essere rimpallato da un vendor all'altro (nell'esempio da Microsoft all'assistenza di boost o viceversa).
Le librerie free sono belle quando giochiamo, ma quando si passa alle cose serie, allora queste cose sono assolutamente vitali.

Il vero cambio di svolita della TR1 che ho voluto sottolineare nel post è che, dal momento in cui il vendor della libreria e del compilatore coincidono e ti danno supporto, può finalmente cambiae il *modo* di scrivere in C++. Si può finalmente cambiare il proprio modo *standard* di scrivere codice con quel linguaggio.
Prima non era possibile farlo perché molte aziende NON vogliono l'integrazione di librerie esterne, siano pure celeberrime come boost.
27/08/2008 18:11 | Raffaele Rialdi
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Ciao Raf,

condivido al 100% con te il fatto che l'allocazione contigua sia *fondamentale* in quel contesto e anche in altri.

Quindi, se vector non dovesse garantire allocazione contigua, il mio codice va scartato (ora però mi fai venire un dubbio, perché codice di quel tipo usando vector e assumendo l'allocazione contigua ne ho scritto molto! :)

Thanks!
Gio
27/08/2008 19:29 | Giovanni Dicanio
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Ciao Raf,

grazie per aver trovato la citazione esatta sullo standard C++ ISO (che ovviamente è molto più autorevole della FAQ da me citata).
Quindi sulla contiguità possiamo stare tranquilli.

Poi: hai pienamente ragione sul discorso push_back! Io credo che se il numero di elementi da inserire nel vector superi la capacity() dello stesso vector, allora viene allocata una nuova area di memoria per garantire la contiguità, e in questo caso ovviamete il puntatore precedente viene invalidato.

Quindi se nel codice si fa riferimento a quel puntatore, esso potrebbe puntare a "spazzatura" dopo la push_back, e quindi sarebbe buona norma, dopo una push_back, rileggere il contenuto del puntatore con una nuova invocazione del tipo &v[0].

Alla fine, sono tutti aspetti di cui occorre tenere conto, e IMHO fai benissimo a sottolinearli.

Thanks!

Ciao!
Gio
28/08/2008 04:01 | Giovanni Dicanio
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Chi non sa usare i puntatori non sa programmare in C
27/09/2013 20:04 | Nicol
Gravatar

# re: Chi scrive codice con Visual C++ cominci a dare l'addio ai puntatori (se non l'ha già fatto)

Nicol, verissimo ma mi preme sottolineare che C e C++ sono due linguaggi molto differenti e che usare C++ come superset del C è clamorosamente sbagliato.
12/10/2013 00:16 | Raffaele Rialdi
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET