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.