Newsgroup: it.comp.lang.c++
Titolo: placement delete?

Come mai non è previsto un placement delete (per simmetria
rispetto al placement new?)
Volevo realizzare una cosa del genere:
class Allocator
{
   public:
      virtual void* allocate(std::size_t n);
      virtual void deallocate(void* p);
};
void* operator new(size_t n, Allocator& alloc)
{
   return alloc.allocate(n);
}
void operator delete(void* p, Allocator& alloc)
{
   alloc.deallocate(p);
}
class SharedAllocator: public Allocator
{
    // ...
};
class foo
{
    // ...
};
int main()
{
   SharedAllocator alloc();
   foo* f = new(alloc) foo();
   delete(alloc) f;
}
In questo modo potevo gestire diversi tipi di allocazioni
(memoria condivisa etc).
Il problema è che non volevo ricorrere alla tecnica :
class SharedObjectAllocator
{
    public:
        void* operator new(std::size_t);
        void operator delele(void* p);
};
class SharedFoo: public foo, public SharedObjectAllocator
{
    // ...
};
(che a dire il vero non so nemmeno se sia corretto - vista la
 ereditarietà multipla)
perchè la trovo meno flessibile dell'altra (anche se più
sicura).
Oltre al fatto che credo che il tipo di allocazione debba essere
disaccopiato dal tipo di oggetto.
Saluti   Manlio Perillo

 rispetto al placement new?)
In realtà è previsto quello che tu chiami placement delete, ma non
funziona come pensi tu (e soprattuto non si chiama placement delete)!
Come spesso avviene le trattazioni del C++ danno una bella infarinatura
ma non sempre scendono nei particolari. Il risultato sono tante idee ma,
più spesso di quanto vorremmo, sono anche ben confuse. Quindi facciamo
ordine.
La scritta "new foo" rappresenta una NEW-EXPRESSION. La scritta "delete
p" rappresenta una DELETE-EXPRESSION. Fissiamo bene questi nomi e
ricordiamoci che non sono la stessa cosa di una ALLOCATION-FUNCTION e
neppure di una DEALLOCATION-FUNCTION. Se questi quattro termini
risultano chiari, possiamo aggiungere i concetti di
PLACEMENT-ALLOCATION_FUNCTION di PLACEMENT-DEALLOCATION-FUNCTION e di
PLACEMENT-NEW-EXPRESSION.
Ora che abbiamo a disposizione i vari nomi "tecnici" vediamo meglio le
differenze.
Sappiamo che "new foo" oppure "new int*[4]" sono delle new-expressions.
Sappiamo anche che le new-expressions provocano due cose: allocazione di
memoria e costruzione di oggetti. L'allocazione di memoria avviene
chiamando una allocation-function. Un esempio di allocation function è
"void* operator new(size_t)". Nota che la allocation function NON
COSTRUISCE nessun oggetto. E' la new-expression che prima chiama una
allocation function e poi costruisce gli oggetti (chiamata a
costruttore).
Sappiamo anche che la delete-expression "delete p" distrugge l'oggetto e
libera la memoria. Questi sono due processi distinti. Prima distrugge
l'oggetto (chiamata a distruttore) poi libera la memoria invocando una
deallocation-function. Un esempio di deallocation function (non quella
chiamata da "delete p"!) è "void operator delete[](void*)".
Facciamo un passo avanti e introduciamo la placement-new-expression. La
sua sintassi permette di passare parametri extra alla
allocation-function che in questo caso diventa un overload di operator
new conosciuto anche come placement-allocation-function. Un esempio è
"new (bar) Foo" che costruisce un oggetto di classe Foo dopo aver
allocato la memoria facendo una chiamata del tipo "operator
new(sizeof(Foo),bar)" che deve mappare in qualche modo ad un qualche
overload di operator new (la placement-allocation-function, appunto).
Perchè questo meccanismo? Semplice: perchè una classe può avere molti
costruttori e questo permette di selezionare un costruttore anche senza
in realtà allocare memoria. Per questo esiste un apposito overload di
operator new che prende un void* come parametro extra (e che lo ritorna
senza fare altro).
Nota che abbiamo parlato di placement-new-expression che chiama una
placement-allocation-function e poi chiama il costruttore come
richiesto. Tu vorresti avere una placement-delete-expression ma questa
NON ESISTE. Per il semplice fatto che non è necessaria (ok, ti
permetterebbe di battere meno tasti, ma questo non è un problema dello
standard!). Non è necessaria perchè di distruttore ne esiste uno solo e
pertanto è chiamabile tramite la normale sintassi di chiamata di un
metodo. Le due operazioni che una ipotetica placement-delete-expression
dovrebbe eseguire sono entrambe eseguibili con la sintassi normale (nota
che invece la stessa cosa non è possibile per la costruzione dato che un
costruttore non può essere chiamato esplicitamente ad un indirizzo dato
se non tramite, appunto, la placement-new-expression accoppiata con una
placement-allocation-function "neutra").
Vediamo quindi cosa dovresti fare tu.
// definisci l'interfaccia dei meccanismi di allocazione e dellocazione.
// Questo è Ok (nota: non è che le due funzioni sono pure?)
struct Allocator {
      virtual void* allocate(std::size_t n);
      virtual void deallocate(void* p);
};
// overload degli operatori
// ovvero definisci una placement-allocation-function e
// la relativa placement-deallocation-function
// anche questo è Ok.
void* operator new(size_t n, Allocator& alloc)
{
   return alloc.allocate(n);
}
void operator delete(void* p, Allocator& alloc)
{
   alloc.deallocate(p);
}
// specializzi un meccanismo di gestione.
// sempre tutto Ok.
class SharedAllocator: public Allocator
{
    // ...
};
// questa è la classe di esempio.
class foo
{
    // ...
};
// e questo è l'esempio d'uso.
// Occhio che ci sono alcune cose che non vanno
int main()
{
   SharedAllocator alloc();
// NO: questa è una dichiarazione di una funzione
// chiamata alloc che non prende parametri e ritorna
// per valore una istanza di SharedAllocator.
// Ti consiglio di togliere le parentesi e scrivere:
//
// SharedAllocator alloc;
   foo* f = new(alloc) foo();
// allright. Abbiamo una placement-new-expression che
// alloca la memoria usando la placement-allocation-function
// "void* operator new(size_t n, Allocator& alloc)"
// che hai definito prima e poi chiama il costruttore d foo
// Nota: le due parentesi dopo foo non servono.
   delete(alloc) f;
// ops! Questo non funzionerà mai. Ricorda che ci sono due
// cose da fare: distruggere l'oggetto e deallocarlo.
// Il modo giusto (previsto dallo standard) è che tu faccia le
// due cose separatamente:
//
// f->~foo();
// alloc.deallocate(f);
//
// al posto della seconda delle suddette due istruzioni
// puoi anche usare la seguente:
//
// operator delete( f, alloc );
}
Ora resta da risolvere uno ed un solo dubbio: io ho parlato di
placement-deallocation-function e ho anche detto che è giusto che tu la
scriva come overload di operator delete. Però dato che non esiste una
sintassi per una placement-delete-expression ne consegue che l'unico
modo per richiamarla è proprio quello di scrivere esplicitamente
"operator delete(f, alloc);". Verrebbe da chiedersi allora a cosa serve.
Basterebbe la sintassi "alloc.deallocate(f);" o una qualunque funzione
di deallocazione senza il bisogno di fare l'overload di operator delete.
Possibile che questo sia possibile solo per "simmetria" con l'overload
di operator new necessario per la placement-new-expression?
La risposta è ovviamente no! E' vero che puoi chiamare indifferentemente
"alloc.deallocate(f);" oppure "operator delete(f, alloc);". Ma anche se
decidi di usare sempre la forma "alloc.deallocate(f);", per esempio
perchè ti sei fatto un bel tamplate che riduce le due righe ad una sola
in questo modo:
template < class T >
  void MioDelete( T* p, Allocator& a ) {
     p->~T();
     a.deallocate(f);
  }
e poi nel main invece della tua sintassi
  delete(alloc) f; // non funge
o di una delle due corrette che richiedono due linee usi questa:
  MioDelete( alloc, f );
anche se fai una cosa del genere e pertanto usi sempre la chiamata al
membro, DEVI lo stesso definire il corretto overload di operator delete
(ovvero la placement-deallocation-function).
Ed ora la domanda da un milione di dollari: perchè?
Prima di seguire lo spoiler e rivelare la soluzione ... prova a pensare
all'unico caso che non abbiamo ancora preso in considerazione.
spoiler
spoiler
spoiler
spoiler
S
P
O
I
L
E
R
S
P
O
I
L
E
R
spoiler
spoiler
spoiler
spoiler
Il problema è che, anche se molti non se ne rendono conto, è possibile
che una new-expression, e quindi anche una placement-new-expression,
richiami una deallocation-function. Ovvero un operator delete.
Cosa cosa cosa? Sto forse dicendo che una istruzione come:
MyClass *p = new MyClass;
potrebbe eseguire il codice definito in "void operator delete(void*);" ?
la risposta è un sonoro SI.
E allo stesso modo quando scrivi, nel nostro esempio
   foo* f = new(alloc) foo();
potresti finire con l'eseguire
void operator delete(void* p, Allocator& alloc)
{
   alloc.deallocate(p);
}
E pensandoci bene è abbastanza ovvio.
Ecco infatti cosa fa ESATTAMENTE una new-expression:
-) chiama l'appropriata allocation-function per ottenere memoria
-) l'allocation-function può essere di due tipi: può avere un
exception-specification vuoto (ovvero throw()) e ritornare 0 in caso di
end-of-memory, oppure deve segnalare l'end-of-memory lanciando
l'eccezione bad_alloc. In caso di end-of-memory la new-expression lancia
bad_alloc o ritorna un puntatore 0 (convertito al tipo opportuno) a
seconda del tipo di allocation-function usata. In entrambe i casi non
vengono chiamati costruttori.
-) se l'allocazione va a buon fine l'oggetto viene costruito. La
costruzione implica l'eventuale valutazione della espessione di
inizializzazione e l'eventuale chiamata di costruttore (o di più
costruttori in caso di array).
-) il processo di costruzione (che ricordo comprende non solo la
chiamata al costruttore ma anche la valutazione di eventuali espressioni
di inizializzazione, compresi eventuali operatori di conversione) può
andare a buon fine o lanciare una eccezione. Se va a buon fine, la
new-expression ritorna il puntatore all'oggetto costruito.
-) se la costruzione lancia una eccezione, la new-expression verifica se
esiste una deallocation-function corrispondente alla allocation-function
chiamata nel primo passo. Se questa esiste viene chiamata per liberare
la memoria allocata nel primo passo prima che l'eccezione venga
propagata. Se invece questa non esiste, l'eccezione viene propagata
SENZA LIBERARE LA MEMORIA allocata con successo al primo passo.
Pertanto devi fare l'overload di operator delete anche se non lo usi per
coprirti il didietro in caso di eccezione durante il processo di
costruzione (sia eccezioni lanciate da un costruttore sia da eventuale
codice che serve per calcolare ed eventualmente convertire i parametri
del costruttore stesso.
--
Andrea Ferro
---------
Brainbench C++ Master. Scored higher than 97% of previous takers
Scores: Overall 4.46, Conceptual 5.0, Problem-Solving 5.0
More info 
http://www.brainbench.com/transcript.jsp?pid=2522556