Newsgroup: it.comp.lang.c++
Titolo: Leggere da un buffer dati di tipo diverso

Ciao a tutti!
ho un buffer di memoria che può contenere dati di tipo unsigned char,
oppure unsigned, oppure unsigned long. Il buffer contiene sempre dati
di un solo tipo alla volta, cioè o contiene solamente valori unsigned
char, o contiene solo valori unsigned o solo unsigned long, ma mai
insieme.
Dispongo di un puntatore all'inizio del buffer sempre di tipo unsigned
char, e ho necessità di leggere il contenuto del buffer un valore alla
volta. Conosco anche la dimensione dei tipo contenuto (cioè se il
buffer contiene unsigned char so che ogni valore occupa 1 byte, se
contiene unsigned so che ne occupa 2 e se contiene unsigned long so
che ne occupa 4).
E' possibile in qualche modo effettuare questa lettura senza uno
switch sulla dimensione dei valori?
Grazie

danilob...@tiscali.it
 ha scritto:

Penso che si potrebbe evitare di fare uno switch, ma a costo di scrivere 
codice meno efficiente e meno leggibile. Potresti leggere tutto a 
livello di bytes e poi assemblarli con una iterazione su 1, 2 o 4 bytes, 
conoscendo l'endianess dei dati nel buffer.
Ma perché vuoi evitare lo switch?
E.

danilob...@tiscali.it
 ha scritto:

Suppongo che il tuo codice sia qualcosa di simile al seguente:
unsigned long a;
for (int i = 0; i < n; ++i) {
     switch (size) {
     case 1: a = ((unsigned char*)buffer)[i]; break;
     case 2: a = ((unsigned*)buffer)[i]; break;
     case 4: a = ((unsigned long*)buffer)[i]; break;
     }
     process(a);
}
Dato che in C++ non esistono istruzioni di cast generano un tipo diverso 
a seconda del valore di variabili, cioe' non e' possibile scegliere il 
tipo a run-time, credo che, se la dimensione del dato (size) e' definita 
solo in fase di esecuzione, sostanzialmente il codice non possa essere 
reso più semplice.
Faccio presente che tali istruzioni non potrebbero comunque essere 
efficienti, in quanto dovrebbero caricare dalla memoria al processore in 
un caso un solo byte, in un altro caso due byte, e nel terzo caso 
quattro byte. Tipicamente, non esistono istruzioni in linguaggio 
macchina che caricano nel processore un numero di byte specificato da 
una variabile, ma si usano istruzioni diverse per lunghezze di dati 
diverse. Quindi e' comunque necessaria un'istruzione di branch per 
saltare all'istruzione desiderata. Pertanto, anche in un linguaggio ad 
alto livello non si puo' sperare di evitare tale istruzione di salto nel 
codice compilato.
Tuttavia, si puo' ottimizzare il codice scritto sopra spostando fuori 
dal ciclo l'istruzione di switch, eseguendo cosi' un solo branch invece 
che uno per ogni valore letto. Pertanto, il seguente codice e' piu' 
efficiente:
switch (size) {
case 1:
     for (int i = 0; i < n; ++i) {
         process(((unsigned char*)buffer)[i]);
     }
     break;
case 2:
     for (int i = 0; i < n; ++i) {
         process(((unsigned*)buffer)[i]);
     }
     break;
case 4:
     for (int i = 0; i < n; ++i) {
         process(((unsigned long*)buffer)[i]);
     }
     break;
}
-- 
Carlo Milanesi
http://digilander.libero.it/carlmila

Carlo Milanesi ha scritto:

condivido la premessa, che hai spiegato molto meglio di 
quanto avrei potuto anche solo immaginare, ma non del tutto 
le conclusioni.
Uno switch si può evitare tramite un array di puntatori a 
funzione (basta che queste funzioni di lettura ritornino 
tutte il tipo promosso alla massima ampiezza, long nel 
caso). Size (o un suo valore mappato in 0,1,2,...) si può 
allora usare direttamente per indicizzare.
Se poi questo sia più o meno efficiente dello switch, lungi 
da me sostenerlo ! E' soltanto più compatto da scrivere, 
tutto lì ... Poi lo ammetto, gli array di puntatori a 
funzione mi son molto simpatici

si, questo è pure vero, e probabilmente più veloce dell'uso 
di puntatori a funzioni
Ciao
Soviet_Mario

 che uno per ogni valore letto. 
...e si potrebbero utilizzare i template per evitare di scrivere tre 
volte lo stesso codice...
E.

 reso più semplice.
Ciao, grazie a tutti per le risposte, alla fine penso che userò lo
switch.
Vi spego meglio qual'è il problema. Sto lavorando con la libreria
FreeImage.
Il programma carica una immagine RGB in memoria, la divide nei canali
R, G, B, e elabora un canale alla volta con il medesimo algoritmo,
parametrizzato con il puntatore al buffer che contiene il canale
(puntatore a unsigned char appunto) ed eventualmente la dimensione del
singolo dato in byte.
L'algoritmo è lo stesso per ogni tipo di immagine, ma i dati nel
buffer possono essere  dei tre tipi descritti sopra. Speravo di poter
risolvere la cosa in maniera elegante, ma a questo punto non vedo
altre soluzioni che usare lo switch e tre funzioni diverse solo nel
tipo del puntatore al buffer.
In alternativa potrei usare una sola funzione che riceve un puntatore
al tipo più grande (unsigned long in questo caso) facendo sì che prima
della chiamata i dati vengano copiati in un altro buffer di unsigned
long, ma nel caso peggione questo comporta avere un buffer molto più
grande quindi non mi piace molto.
Ciao!

 tipo del puntatore al buffer.
Usa un template per ottenere il tipo, poi l'algoritmo lo scrivi solo una 
volta, eventualmente nella classe template stessa o esternamente tipo in 
questo esempio scritto al volo e non testato (e ottimizzabile 
sicuramente rimuovendo uno dei tre dati della classe):
template <typename T>
class MyElaboration
{
	void *buf_;
	void *pos_;
	size_t len_;
public:
	MyElaboration(void *b, int l) : buf_(b), len_(l), pos_(b) {}
	T Get() {
		if (pos_ > (buf_ + len_ + sizeof(T))
			throw std::string("Overflow!");
		T val = *((T *)buf_);
		pos_ += sizeof(T);
	}
	bool EOF() { return (pos_ == (buf_ + len_); }
};

 sostenerlo ! E' soltanto più compatto da scrivere, tutto lì ... 
Mi permetto di dissentire da questa conclusione.
Ecco due programmi completi equivalenti, il primo che usa uno switch:
#include <iostream>
using namespace std;
void process(int n) {
     cout << n << ' ';
}
void run(void* buffer, int n_items, int item_size) {
     cout << "item_size=" << item_size << endl;
     switch (item_size) {
     case 1:
         for (int i = 0; i < n_items; ++i) {
             process(((unsigned char*)buffer)[i]);
         }
         break;
     case 2:
         for (int i = 0; i < n_items; ++i) {
             process(((unsigned short*)buffer)[i]);
         }
         break;
     case 4:
         for (int i = 0; i < n_items; ++i) {
             process(((unsigned long*)buffer)[i]);
         }
         break;
     }
     cout << endl;
}
int main() {
     unsigned char b[] = "
12345678901
";
     run(b, sizeof b, 1);
     run(b, sizeof b / 2, 2);
     run(b, sizeof b / 4, 4);
}
e il secondo che usa un array di puntatori a funzione:
#include <iostream>
using namespace std;
void process(int n) {
     cout << n << ' ';
}
void f1(void* buffer, int n_items) {
     for (int i = 0; i < n_items; ++i) {
         process(((unsigned char*)buffer)[i]);
     }
}
void f2(void* buffer, int n_items) {
     for (int i = 0; i < n_items; ++i) {
         process(((unsigned short*)buffer)[i]);
     }
}
void f4(void* buffer, int n_items) {
     for (int i = 0; i < n_items; ++i) {
         process(((unsigned long*)buffer)[i]);
     }
}
void (*f[])(void*,int) = { 0, f1, f2, 0, f4 };
void run(void* buffer, int n_items, int item_size) {
     cout << "item_size=" << item_size << endl;
     f[item_size](buffer, n_items);
     cout << endl;
}
int main() {
     unsigned char b[] = "
12345678901
";
     run(b, sizeof b, 1);
     run(b, sizeof b / 2, 2);
     run(b, sizeof b / 4, 4);
}
Questi programmi hanno molte righe in comune. Le righe che appartengono 
al primo programma e non al secondo sono le seguenti:
     switch (item_size) {
     case 1:
         break;
     case 2:
         break;
     case 4:
         break;
     }
Mentre le righe che appartengono al secondo programma e non al primo 
sono le seguenti:
void f1(void* buffer, int n_items) {
}
void f2(void* buffer, int n_items) {
}
void f4(void* buffer, int n_items) {
}
void (*f[])(void*,int) = { 0, f1, f2, 0, f4 };
     f[item_size](buffer, n_items);
Queste porzioni di codice sono entrambe di 8 righe, ma la prima contiene 
solo 21 token contro i 72 token della seconda porzione, e, eliminando 
tutti gli spazi non necessari, la prima contiene solo 58 caratteri 
contro i 168 della seconda.
Non vedo come si possa definire "piu' compatto da scrivere" l'uso dei 
puntatori a funzioni.
Certamente la chiamata alle funzioni e' compatta, ma solo grazie al 
fatto che si sono gia' definite le funzioni.


 Ecco due programmi completi equivalenti, il primo che usa uno switch:
Utilizzare i puntatori a funzione dovrebbe servire ad evitare di 
riscrivere 4 volte il ciclo for che andrebbe messo nella funzione "run" 
non nelle funzioni f1,f2,f3,f4. Se il ciclo fosse più complicato (magari 
più cicli annidiati, con altri controlli da fare tra una riga e l'altra) 
diventerebbe molto sensata la soluzione con i puntatori a funzioni. 
Oppure, visto che siamo nel gruppo del C++, una chiamata a funzione 
virtuale.
E.


Carlo Milanesi ha scritto:

E io, in generale, dissento dal dissentimento :-)
Mi spiego meglio : okay, l'esempio che hai fatto dimostra 
quel che dici, e sin li non ci piove.
Ma è chiaro (o almeno l'ho dato per sottinteso) che 
l'investimento di codificare corpi di funzioni ad hoc non 
sono ragionevoli laddove le funzioni stesse vengano invocate 
in una sola posizione. Così stando le cose, hai ragione a 
dire che è pesante la mia proposta.
Ma se le funzioni sono invocate in molti punti, allora la 
compattezza della codifica col puntatore ripaga.
Non vedo tanto comune il caso di scrivere una funzione, 
specialmente di conversione o simili commodities, che debba 
essere invocata in un solo punto del codice.
Se già la chiami in dieci posti, ecco che dieci switch 
diventano fastidiosi, e magari sottilmente diversi tanto da 
non poter nemmeno incapsulare la parte con switch ...

Si ma, ribadisco, in un caso più reale mi aspetterei 
chiamate disseminate in vari posti, e allora il peso dei 
corpi delle funzioni viene a contare meno.
Imho ovviamente
ciao
Soviet_Mario

 virtuale.
Questo e' il modo un po' meno efficiente di fare le cose.
In tal modo si eseguirebbe una chiamata di funzione (non espandibile
inline) per ogni iterazione.
Comunque, anche la soluzione con lo switch ripeteva tre volte il ciclo
for. Se anche in quel caso si inserisce lo switch dentro il ciclo for, 
risulta che quella soluzione e' strutturalmente meno complessa della 
soluzione con i puntatori a funzione.
Lo stesso discorso vale anche per le funzioni virtuali, che poi sono 
ancora array di puntatori a funzione.
Per rispondere anche a Soviet_Mario, sono d'accordo che questo vale se 
il problema e' proprio cosi' semplice come e' stato proposto.
Tuttavia, se tali funzioni (f1,f2,f4) sono chiamate anche altrove, puo' 
convenire l'uso dei puntatori a funzione o delle funzioni virtuali.
Inoltre questa soluzione risulta abbastanza semplice solo se il ciclo 
for e' proprio cosi' semplice e si puo' definire una sola funzione (qui 
chiamata "process") che elabora il dato convertito a long.
Se invece il ciclo e' piu' complesso oppure se comunque si devono avere 
tre funzioni di elaborazione distinte (process_1, process_2, process_4), 
allora e' meglio costruire un template di funzione (process<T>) 
parametrizzata dal tipo di dato.
