Newsgroup: it.comp.lang.c++
Titolo: separazione interfaccia/implementazione

Vorrei separare l'implementazione dall'interfaccia di un oggetto. In 
particolare vorrei evitare di dover ricompilare tutto il progetto per 
ogni piccola modifica della classe. Ho trovato questa soluzione:
// HEADER .h:
class A {
	protected:
		A() {};
	public:
		static A* create();
	// interfaccia:
		virtual void f()=0;
		// ...
};
// SOURCE .cpp
class AImpl: public A {
	// ...
	public:
		AImpl() {
			//....
			};
		void f() {
			//...
		};
};
A *A::create() {
	return new AImpl;
}
Solo che ora stavo pensando che non č necessario utilizzare le funzioni 
virtuali, potrei anche fare cosě:
// HEADER .h:
class A {
	protected:
		A() {};
	public:
		static A* create();
	// interfaccia:
		void f();
 
};
// SOURCE .cpp
class AImpl: public A {
	public:
		AImpl() {
			....
			};
		void f() {
			...
		};
};
A *A::create() {
	return new AImpl;
}
void A::f() {
	return static_cast<AImpl *>(this)->f();
}
...che č piů efficiente e usa meno memoria. Ovviamente č meno elegante, 
ma vi pare una soluzione sensata?
E

 ogni piccola modifica della classe.
Perche' non semplicemente:
// A.h
class A {
  public:
    A();
    void f();
};
// A.cpp
A::A() {}
void A::f() {}
Altrimenti usa la tua versione originale. La "furbata" non vale la
fragilita' che introduce.
Ciao,
Nicola

 fragilita' che introduce.
Sì, infatti ho deciso di mantenere la versione con i virtual. Però mi da 
un po' fastidio che se stessi scrivendo la cosa in C, l'implementazione 
  furbesca sarebbe naturale. E nessuno si preoccuperebbe della fragilità.
E.

   furbesca sarebbe naturale. E nessuno si preoccuperebbe della fragilità.
Puoi sempre scegliere di rinunciare a usare i costrutti che permettono
di esprimere meglio le astrazioni, ma mi pare stupido. In C
l'implementazione furbesca sarebbe naturale perche' mancano strumenti
migliori e non e' vero che nessuno si preoccuperebbe della fragilita',
solo ci sarebbe ben poco da fare.
Trovo deprimenti i salti mortali che tanti hanno fatto o fanno
tutt'ora per implementare meccanismi di orientamento agli oggetti in
C. Oggi come oggi secondo me sono pochissimi i progetti per i quali ha
piu' senso sviluppare in C che in C++.
Ciao,
Nicola

 wrote:

Non puoi usare una pimpl?
file.h
class A
{
public:
// tutti i metodi
A();
void f();
private:
struct pimpl;
smart_ptr<pimpl> pi;
}
file.cpp
A::A() : pimpl()  {}
void A::f()
{
uso pi->qualcosa
}
struct A::pimpl
{
pimpl() { // costruttore }
// tutte le variabili membro
}
Nota: smart_ptr<> e' un qualsiasi puntatore intelligente che tu possa
trovare a giro, sconsiglio gli auto_ptr delle std per motivi che mi
fatica di argomentare scusate ( in breve quando copi un auto_ptr in un
altro anche la sorgente si modifica... ). personalmente gli smart_ptr
delle boost o il grin_ptr di Alan Griffiths credo che siano i
migliori.
Daniele

 Non puoi usare una pimpl?
Credo di aver capito quello che suggerisci. E l'idea č molto sensata. 
Solo che non mi pare migliore dei virtual... La classe interfaccia 
contiene un puntatore all'implementazione, giusto? Quindi hai un 
puntatore in piů in ogni classe (come con i virtual) e le chiamate 
diventano indirette (come con i virtual). Inoltre devi allocare due 
oggetti invece che uno solo...
E.

 oggetti invece che uno solo...
D'altra parte (se opportuno) puoi copiarne uno solo e condividere il
pimpl.
-- 
blog:  
http://www.akropolix.net/rik0/blogs
 | Uccidete i filosofi,
site:  
http://www.akropolix.net/rik0/
      | tenetevi riso e
forum: 
http://www.akropolix.net/forum/
     | bacchette per voi.

 problematiche che hai descritto.
Quali problematiche? Le problematiche che io ho visto per l'uso dei 
virtual ci sono identiche con questo metodo... Soprattutto mi pare che 
utilizzare le funzioni virtuali si molto piů appropriato per 
rappresentare il problema che si sta risolvendo. Con il pimpl le 
funzioni membro della classe interfaccia devono chiamare le funzioni 
corrispondenti della classe pimpl... ogni funzione esportata viene 
duplicata. Con i virtual succede lo stesso, ma la duplicazione viene 
gestita dal linguaggio automaticamente.
Diverso č il discorso se volessi implementare la copia "lazy" (non mi 
ricordo come si chiama...) come con le string. In tal caso sě, farei 
come suggerite voi.
E.

 gestita dal linguaggio automaticamente.
Non per dire, ma te la puoi cavare certamente con una macro (se non vuoi
scrivere il codice ridondante. O magari con oggetti tipo il
preprocessore di boost.
Ad ogni buon conto io personalmente e in genere trovo il pimpl comodo
per una grossissima serie di motivi. Poi chiaro, ci sono anche i casi in
cui stanno meglio le funzioni virtuali.
In generale comunque a me non fanno impazzire. E' una delle parti di C++
che mi garbano meno (anche perchč si mischiano male con il codice
templatico).


?manu* ha scritto:

Non mi pare che l'uso del pimpl comporti un'inefficienza significativa.
Infatti, in primo luogo, la barriera di compilazione risulta opportuna 
solo per classi che possiedono (direttamente o indirettamente) un 
discreto numero di variabili membro. Per cui, in tali casi, l'aggiunta 
di un puntatore a ogni oggetto comporta un'inefficienza trascurabile 
nell'occupazione di memoria.
In secondo luogo, l'uso delle funzioni virtuali o dello static_cast 
costringe ad allocare l'oggetto nello heap, tramite la factory "create", 
che tral'altro comporta il rischio di dimenticarsi di eliminare 
l'oggetto allocato.
Usando il pimpl invece si alloca direttamente l'oggetto interfaccia 
sullo stack e solo l'implementazione sullo heap.
Pertanto, tutte e tre le soluzioni hanno un puntatore in piu' e un 
livello di indirettezza in piu'.
Ho fatto qualche benchmark con le tre soluzioni con Visual C++ 7.1, 
usando come variabili membro solo una variabile "double x" e come 
funzione "f" la seguente:
     double f() { x += 1.1; return x; };
Mi risulta che tutte e tre le soluzioni hanno tempi identici per la 
chiamata a "f" (pari a 10.9 ns), mentre il tempo per creare e 
distruggere l'oggetto e' 406.2 ns per la soluzine che usa il pimpl e per 
la soluzione che usa lo static_cast, mentre e' 419 ns per la soluzione 
che usa la funzione virtuale.
La soluzione in cui non si usa una barriera di compilazione, ha tempo 
nullo per la sola creazione e la distruzione, in quanto l'ottimizzatore 
elimina ogni operazione, e 3.5 ns per la chiamata a "f".
-- 
     Carlo Milanesi
 
http://digilander.libero.it/carlmila

 che usa la funzione virtuale.
Ma nel caso della pimpl, l'oggetto l'hai messo sullo stack o sullo heap?
Non capisco perché ci mette più tempo ad allocare la "virtual" rispetto 
alla "pimpl".
Comunque ripeto che ho capito il senso della pimpl, è che mi è chiaro 
che in molte situazioni sia la soluzione migliore (in certe situazioni 
l'unica soluzione). Nel mio caso specifico preferisco lasciare le 
funzioni virtual.
Il vantaggio vero sarebbe la possibilità che le funzioni membro vengano 
eseguite "inline", ma questo è ovviamente in contraddizione con la 
richiesta di non esporre i dati della classe nel .h. Però il metodo dei 
virtual lascia aperta la possibilità di riportare, senza grandi 
modifiche, la classe implementazione nel .h, per sfruttare l'"inline". 
Questo potrebbe pure risultarmi utile perché in effetti la separazione 
mi serve solo per poter compilare più in fretta durante il lo sviluppo, 
ma alla fine potrei decidere di esporre tutte le definizioni delle classi.
E.

 alla "pimpl".
Non so. Ma probabilmente dipende dal compilatore.
Per completezza ha rifatto i benchmark con maggior precisione e 
considerando piu' casi.
Le operazioni misurate sono:
A=creazione e distruzione dell'oggetto (conteente solo un double 
inizializzato a zero)
B=chiamata di f su un oggetto gia' costruito (la funzione incrementa la 
variabile membro di 1.1 e rende il valore ottenuto).
Ho provato anche ad attivare l'opzione /GL per l'ottimizzatione 
sull'intero programma, che ovviamente rende inutile la barriera di 
compilazione in quanto vengono comunque elaborati tutti i file sorgente,
mettendo i risultati in A' e B'
                                 A      B    A'  B'
1) virtual                     407   10.1  398 10.1
2) static_cast                 393   10.6  404 11.9
3) stack pimpl                 395   10.3  414 12.0
4) heap pimpl                  842   10.4  847 12.2
5) stack senza barriere          4.4 11.3    0  3.4
6) heap senza barriere         433   11.2  414 12.0
7) stack inline senza barriere   0    3.4    0  3.4
8) heap inline senza barriere  407   11.8  414 11.9
A parte modeste fluttuazioni difficili da spiegare, si nota che:
- L'allocazione e deallocazione richiede comunque circa 410 ns per ogni 
chiamata a new.
- La chiamata a f richiede comunque tra i 10 e i 12 ns, tranne quando 
puo' essere espansa inline; nel qual caso ne richiede solo 3.4.
- L'ottizzazione sull'intero programma permette di espandere inline 
funzioni definite in altri file sorgente, ma in qualche caso (2A, 2B, 
3A, 3B, 4A, 4B, 6B, 8A) stranamente rallenta l'esecuzione fino al 16%.


 considerando piu' casi.
Grazie, i test sono interessanti... penso che le piccole differenze di 
prestazioni siano proprio delle fluttuazioni, come dici tu. Io avevo 
anche cominciato a fare gli stessi test nel mio ambiente di sviluppo, ma 
per me è troppo complicato...
Tra l'altro è la prima volta che sento parlare di questa opzione di 
ottimizzazione globale... mi chiedo se si può ottenere 
"artificialmente". Forse concatendando tutto il codice sorgente in un 
unico file?
E.

 unico file?
Vedi:
http://msdn2.microsoft.com/en-us/library/0zza0de8(VS.71).aspx
Tradizionalmente, le ottimizzazioni dei compilatori si dividevano in 
ottimizzazione della singola istruzione sorgente o pinhole, e 
ottimizzazione di un'intera routine per volta.
Poi hanno inventato le ottimizzazioni che prendono in cosiderazione piu' 
routine della stessa unita' di compilazione, e le hanno chiamate, 
stupidamente, "globali", come se tutto il mondo stesse in un file sorgente.
Nel 2003 il Visual C++ ha introdotto un tipo di ottimizzazione che 
prende in considerazione piu' unita' di compilazione, e, non potendo 
chiamarla "globale", la ha chiamata "whole program optimization".
Anche questa ottimizzazione non e' realmente globale in quanto ottimizza 
il singolo modulo eseguibile. Se un'applicazione e' composta da piu' 
file EXE e da piu' DLL, ognuno di essi e' ottimizzato indipendentemete.
Per ottenere questo risultato, e' necessario che sia il linker a 
generare il codice macchina. Tipicamente, con tale opzione la 
compilazione fa ben poco ed e' abbastanza veloce, ma il linking e' 
lentissimo e richiede molta memoria. Inoltre, i file .obj intermedi non 
sono compatibili con quelli prodotti senza tale opzione.
Penso anch'io che per ottenere tale risultato senza un supporto da parte 
di compilatore e linker, l'unico modo sia proprio concatenare tutti i 
file .c e .cpp e compilare il file risultante, attivando le opzioni di 
ottimizzazione "globale", ... se il compilatore ce la fa.


 gestita dal linguaggio automaticamente.
A mio avviso la discriminante fondamentale nel decidere di usare o
meno pimpl in casi come quello che descrivi e' stabilire se la classe
in questione deve comportarsi piuttosto come un valore o piuttosto
come un'entita'. Nel primo caso di solito si vuole poter allocare
sullo stack, effettuare delle copie, ecc. e pimpl e' un modo per
fornire queste funzionalita' anche quando e' necessario avere un
comportamento polimorfico; nel secondo e' piu' comune utilizzare
direttamente puntatori, magari intelligenti.
Secondo me questo tipo di considerazioni viene prima di quelle
sull'intrinseca fragilita' dell'approccio pimpl, che hai indicato
altrove, e a maggior ragione di quelle sulle prestazioni.
Ciao,
Nicola

 ma alla fine potrei decidere di esporre tutte le definizioni delle classi.
Non credo che complicare il codice solo per una velocita' di compilazione
maggiore sia una buona idea... Perche' non vedi se tools come ccache[1]
possono essere sufficienti per ridurre i tempi di compilazione?
[1] 
http://ccache.samba.org/
Ciao,
        Teo.
-- 
* Matteo Gottardi | 
mat...@tin.it
* ICQ UIN 20381372
* Linux - the choice of a GNU generation
* GPG Fingerprint:
* B9EE 108F 52C8 D50C B667 B1F2 AB56 8A01 BA3D 36A1