Newsgroup: it.comp.lang.c++
Titolo: copia di un oggetto bit a bit II

Siccome il precedente thread ha generato una lunga discussione ho pensato
di ripostare qui la mia idea in un modo più coerente ed ordinato.
Inoltre il codice che avevo portato come esempio conteneva degli errori
e poteva dare dei problemi non imputabili all'idea stessa.
Comunque prima voglio precisare alcune cose.
Per prima cosa riporto il codice che mi ha ispirato.
E' una tipica implementazione di std::vector::reserve.
template<class T, class Allocator> class vector
{
  private:
    // ...
    typedef typename Allocator::pointer pointer;
    typedef typename Allocator::pointer iterator;
 
    pointer start, finish, end_of_storage;
  public:
    // ...
    void reserve(size_type);
    iterator insert(iterator, const T&);
    void insert_aux(pointer, const T&);
};
template <class T, class Allocator>
void vector<T, Allocator>::reserve(size_type n)
{
   if((end_of_storage - start) < n)
   {
      size_type old_size = finish - start;
      pointer tmp = get_allocator().allocate(n);
      try
      {
         uninitialized_copy(start, finish, tmp);
      }
      catch
      {
         get_allocator().deallocate(tmp, n);
         throw;
      }
      pointer i = start;
      while(i != finish) get_allocator().destroy(i++);
      if(start) get_allocator().deallocate(start, end_of_storage - start);
      start = tmp;
      finish = start + old_size;
      end_of_storage = start + n;
   }
}
Quello che subito ho pensato è questo:
quello che si vuole qui è semplicemente trasferire un oggetto da una locazione
di memoria ad un'altra.
Tuttavia in C++ non si può esprimere questa cosa direttamente ed allora
si ricorre all'ARTIFICIO di effettuare una copia dell'oggetto in una nuova
locazione di memoria e quindi distruggere il vecchio oggetto.
Questo tuttavia non è ideale.
Si pensi, ad esempio, di avere un vettore con 64 elementi di tipo X, 
con X che alloca 1 MB di memoria;
si arriverebbe ad un punto in cui sono stati allocati 128 MB di memoria!
Ma la cosa che mi convince della validità della mia idea è questa.
L'utente che usa reserve 'non sa' quello che sta accadendo, quindi
per lui sarebbe la stessa cosa se, in effetti, gli oggetti restassero
SE STESSI.
Ossia NULL'ALTRO accadesse se non ritrovarsi con dello spazio libero in più 
per il vettore.
Infine un'ultima cosa.
La mia implementazione per 'trasferire' un oggetto in un'altro
effettua una copia bit a bit.
Quello che voglio sia chiaro è che non sto dicendo che questo è il modo
giusto di procedere.
Invece quello che chiedo è se è possibile implementare questa funzione
come built-in in C++ (tipo new o reinterpret_cast).
La mia implementazione 'sembra' funzionare correttamente con il 
BCC 5.5 in Windows 98, con DJGPP in DOS e con lo GNU C++ in Linux.
Tuttavia vorrei avere una più sicura conferma.
Ed ora ecco la mia implementazione:
<my_alloc.hpp>
#include <memory>     // per l'allocatore standard
#include <cstring>    // per memcpy
//
// --------- Propongo che la seguente funzione diventi built-in ---------
// Trasferisce l'oggetto puntato da src -- alla locazione di memoria void*(src) --
// nella locazione di memoria void*(dest).
// NOTA: non si deve più accedere a *src dopo aver chiamato questa funzione!
//
template<class T> inline void transfer(T* dest, const T* src) throw()
{
  std::memcpy(static_cast<void*>(dest), static_cast<const void*>(src), sizeof(T));
}
//
// Algoritmi specializzati per trasferire oggetti
//
template <class Iter, class CIter>
inline Iter transfer_copy(CIter first, CIter last, Iter result) throw()
{
  Iter next = result;
  for(; first != last; ++first, ++next) transfer(&*next, &*first);
  return next;
}
template <class Iter, class CIter>
inline
Iter transfer_copy_backward(CIter first, CIter last, Iter result) throw()
{
  while (first != last) transfer(&*--result, &*--last);
  return result;
}
//
// Definizione per My_alloc;
// usa i servizi e le definizioni dell'allocatore standard più transfer.
//
template<class T> class My_alloc: public std::allocator<T>
{
  private:
    typedef std::allocator<void> void_allocator;
    typedef std::allocator<T> allocator_type;
 
  public:
    typedef allocator_type::size_type        size_type;
    typedef allocator_type::difference_type  difference_type;
    typedef allocator_type::pointer          pointer;
    typedef allocator_type::const_pointer    const_pointer;
    typedef allocator_type::reference        reference;
    typedef allocator_type::const_reference  const_reference;
    typedef allocator_type::value_type       value_type;
 
    My_alloc() {  }
    void transfer(pointer dest, const_pointer src) throw()
    {
      ::transfer(&*dest, &*src);
    }
};
Di seguito è riportato il programma di esempio (o meglio, di test)
#include <iostream>
#include <vector>
#include <algorithm>
#include "myalloc.hpp"
template<class T> inline
std::ostream& operator<<(std::ostream& out, const std::vector<T>& v)
{
  if(v.size() == 0) return out << "{}";
  out << "{";
  std::copy(v.begin(), v.end(), std::ostream_iterator<T>(out, ", "));
  return out << "\b\b}";
}
typedef std::vector<int> ivec;
typedef std::vector<ivec> ivecvec;
using std::cout;
int main()
{
  My_alloc<ivecvec> alloc;
  int _base[] = {1, 2, 3, 4, 5};
  ivecvec base;
  base.push_back(ivec(_base, _base + 5));
  base.push_back(ivec(5, 7));
  base.push_back(ivec(5, 11));
  // stile per l'allocazione di memoria ed inizializzazione come usato dai container standard
  cout << "allocazione ed inizializzazione per a...\n";
  ivecvec* a = alloc.allocate(1);
  alloc.construct(a, base);
  cout << "a = " << *a << '\n';
  cout << "\nallocazione per a2...\n";
  ivecvec* a2 = alloc.allocate(1);     // alloca solo la momoria per a2
  // inizializza a2
#ifdef THE_USUAL_WAY
  cout << "\ncostruzione di a in a2...\n";
  alloc.construct(a2, *a);
#else
  // transferisce a in a2
  cout << "\ntransferimento di a in a2...\n";
  alloc.transfer(a2, a);
#endif
  cout << "a2 = " << *a2 << '\n';
  cout << "\nrilascio delle risorse di a...\n";
#ifdef THE_USUAL_WAY
  alloc.destroy(a);
#else
  // Non chiama alloc.destroy(a) perchè ora le risorse di a appartengono ad a2;
  // a2 è un alias di a (ossia è come se a2 fosse in realtà a)
#endif
  alloc.deallocate(a, 1);              // dealloca solo la momoria
 
  cout << "a2 = " << *a2 << '\n';
  //
  // Qualche test
  //
  cout << "\nmodificazione di a2...\n";
  a2->push_back(ivec(5, 666));
  cout << "a2 = " << *a2 << '\n';
  ivecvec::iterator found = std::find(a2->begin(), a2->end(), ivec(5, 11));
  if(found != a2->end()) a2->insert(found, ivec(5, 999));
  cout << "\na2 = " << *a2 << '\n';
  a2->pop_back();
  cout << "\na2 = " << *a2 << '\n';
  //
  // Un ultimo test
  //
  cout << "\ncopia di *a2 in a3...\n";
  ivecvec a3 = *a2;
  cout << "a3 = " << a3 << '\n';
  cout << "\na2 = " << *a2 << '\n';
  cout << "\na3 == a2: " << (a3 == *a2) << '\n';
  // --- Rilascio delle risorse di a2 ---
  cout << "\nrilascio delle risorse di a2...\n";
  alloc.destroy(a2);
  alloc.deallocate(a2, 1);
}
L'uso della macro THE_USUAL_WAY permette di confrontare facilmente il comportamento 
standard con quello causato dall'uso di transfer.
Ed infine ecco la mia implementazione per vector::reserve:
template <class T, class Allocator>
void vector<T, Allocator>::reserve(size_type n)
{
   if((end_of_storage - start) < n)
   {
      pointer tmp = get_allocator().allocate(n);
      pointer tmp_end = transfer_copy(start, finish, tmp);
      if(start) get_allocator().deallocate(start, end_of_storage - start);
      start = tmp;
      finish = tmp_end;
      end_of_storage = start + n;
   }
}
Comunque i maggiori vantaggi nell'uso di transfer si ottengono per vector<X>::insert.
Infatti lo standard specifica che, a patto che il costruttore di copie di X non 
lanci eccezioni, insert debba fornire una strong guarantee.
(vedi appendice E del libro "The C++ programming language", disponibile anche alla
home page di Stroustrup).
Questo significa che in caso di eccezzioni l'operazione non ha effetto, ossia
il vettore rimane immutato.
Nel caso in cui il costruttore di copie di X lanci una eccezione il vettore è ancora in
uno stato valido, ma alcuni elementi potrebbero essere spostati o replicati.
Questa è la basic guarantee.
Ebbene, con l'uso di transfer viene garantita sempre una strong guarantee.
Spero che questo possa aiutare ad avere una migliore discussione.
Altrimenti mi scuso per aver spedito un messaggio così lungo
(301 righe, 1180 parole).
Sarei inoltre grato a chiunque mi riportasse eventuali errori 'formali' presenti nel codice.
Manlio Perillo

 per il vettore.
Be il container std::vector deve avere tra le sue caratteristiche il fatto
che tutti gli
elementi dentro al vettore devono trovarsi in locazioni continue di memoria,
quindi
se il vecchio vector si trovava "intrappolato" in aree di memoria gia usate
non si puo
far altro che spostare il vecchio e crearne uno nuovo.
Gaetano
PS: non ho avuto modo e voglia di leggere tutto quel malloppo.

 BCC 5.5 in Windows 98, con DJGPP in DOS e con lo GNU C++ in Linux.
"sembra" è la parola giusta. E' facile scrivere una classe che non sia
possibile utilizzare nel tuo container senza produrre "disastri".
Max