Opinioni sulla punzonatura in C ++?
-
19-08-2019 - |
Domanda
Sono curioso di conoscere le convenzioni per puntatori / matrici di tipo punzonatura in C ++. Ecco il caso d'uso che ho al momento:
Calcola un semplice checksum a 32 bit su un BLOB binario di dati trattandolo come un array di numeri interi a 32 bit (sappiamo che la sua lunghezza totale è un multiplo di 4), quindi riassume tutti i valori e ignora l'overflow.
Mi aspetto che una tale funzione sia simile a questa:
uint32_t compute_checksum(const char *data, size_t size)
{
const uint32_t *udata = /* ??? */;
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
checksum += udata[i];
return udata;
}
Ora la domanda che ho è: cosa consideri il "migliore" modo per convertire i dati
in udata
?
Cast in stile C?
udata = (const uint32_t *)data
Cast C ++ che presuppone che tutti i puntatori siano convertibili?
udata = reinterpret_cast<const uint32_t *>(data)
C ++ ha eseguito il cast tra tipi di puntatore arbitrari usando void *
intermedio?
udata = static_cast<const uint32_t *>(static_cast<const void *>(data))
Cast attraverso un sindacato?
union {
const uint32_t *udata;
const char *cdata;
};
cdata = data;
// now use udata
Mi rendo pienamente conto che questa non sarà una soluzione portatile al 100%, ma mi aspetto solo di usarla su un piccolo set di piattaforme dove so che funziona (vale a dire accessi di memoria non allineati e ipotesi del compilatore sull'aliasing dei puntatori). Cosa consiglieresti?
Soluzione
Per quanto riguarda lo standard C ++, litb ' la risposta è completamente corretta e la più portatile. Trasmissione di const char * data
in un const uint3_t *
, indipendentemente dal fatto che si tratti di un cast di tipo C, static_cast
o reinterpret_cast
, infrange le rigide regole di aliasing (vedi Comprensione di Aliasing rigoroso ). Se esegui la compilazione con l'ottimizzazione completa, è probabile che il codice non sia corretto.
Trasmettere attraverso un sindacato (come my_reint
di litb) è probabilmente la soluzione migliore, anche se tecnicamente viola la regola che se scrivi a un sindacato attraverso un membro e lo leggi attraverso un altro, provoca un comportamento indefinito. Tuttavia, praticamente tutti i compilatori supportano questo, e si ottiene il risultato atteso. Se desideri assolutamente conformarti allo standard 100%, scegli il metodo di spostamento dei bit. Altrimenti, consiglierei di passare attraverso un sindacato, che probabilmente ti darà prestazioni migliori.
Altri suggerimenti
Ignorando l'efficienza, per semplicità di codice farei:
#include <numeric>
#include <vector>
#include <cstring>
uint32_t compute_checksum(const char *data, size_t size) {
std::vector<uint32_t> intdata(size/sizeof(uint32_t));
std::memcpy(&intdata[0], data, size);
return std::accumulate(intdata.begin(), intdata.end(), 0);
}
Mi piace anche l'ultima risposta di litb, quella che sposta a turno ogni carattere, tranne per il fatto che poiché il carattere potrebbe essere firmato, penso che abbia bisogno di una maschera extra:
checksum += ((data[i] && 0xFF) << shift[i % 4]);
Quando il tipo di punzonatura è un potenziale problema, preferisco non digitare il gioco di parole piuttosto che provare a farlo in modo sicuro. Se in primo luogo non si creano puntatori con alias di tipi distinti, non è necessario preoccuparsi di ciò che il compilatore potrebbe fare con gli alias, e nemmeno il programmatore di manutenzione che vede i tuoi più static_cast attraverso un sindacato.
Se non si desidera allocare troppa memoria aggiuntiva, quindi:
uint32_t compute_checksum(const char *data, size_t size) {
uint32_t total = 0;
for (size_t i = 0; i < size; i += sizeof(uint32_t)) {
uint32_t thisone;
std::memcpy(&thisone, &data[i], sizeof(uint32_t));
total += thisone;
}
return total;
}
Un'ottimizzazione sufficiente eliminerà la memcpy e la variabile extra uint32_t interamente su gcc e leggerà un valore intero non allineato, qualunque sia il modo più efficiente per farlo che è sulla tua piattaforma, direttamente dall'array di origine. Spero che lo stesso valga per gli altri "gravi" compilatori. Ma questo codice è ora più grande di quello di Litb, quindi non c'è molto da dire perché a parte il mio è più facile trasformarlo in un modello di funzione che funzionerà altrettanto bene con uint64_t, e il mio funziona come endian-native invece di scegliere poco -endian.
Questo ovviamente non è completamente portatile. Presuppone che la rappresentazione di archiviazione dei caratteri sizeof (uint32_t) corrisponda alla rappresentazione di archiviazione di un uin32_t nel modo che desideriamo. Ciò è implicito nella domanda, poiché afferma che si può essere "trattati come" l'altro. Endian-ness, se un carattere è 8 bit e se uint32_t usa tutti i bit nella sua rappresentazione di archiviazione può ovviamente intromettersi, ma la domanda implica che non lo faranno.
Ci sono i miei cinquanta centesimi - modi diversi per farlo.
#include <iostream>
#include <string>
#include <cstring>
uint32_t compute_checksum_memcpy(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
// memcpy may be slow, unneeded allocation
uint32_t dest;
memcpy(&dest,data+i,4);
checksum += dest;
}
return checksum;
}
uint32_t compute_checksum_address_recast(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
//classic old type punning
checksum += *(uint32_t*)(data+i);
}
return checksum;
}
uint32_t compute_checksum_union(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
//Syntax hell
checksum += *((union{const char* c;uint32_t* i;}){.c=data+i}).i;
}
return checksum;
}
// Wrong!
uint32_t compute_checksum_deref(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
checksum += *&data[i];
}
return checksum;
}
// Wrong!
uint32_t compute_checksum_cast(const char *data, size_t size)
{
uint32_t checksum = 0;
for (size_t i = 0; i != size / 4; ++i)
{
checksum += *(data+i);
}
return checksum;
}
int main()
{
const char* data = "ABCDEFGH";
std::cout << compute_checksum_memcpy(data, 8) << " OK\n";
std::cout << compute_checksum_address_recast(data, 8) << " OK\n";
std::cout << compute_checksum_union(data, 8) << " OK\n";
std::cout << compute_checksum_deref(data, 8) << " Fail\n";
std::cout << compute_checksum_cast(data, 8) << " Fail\n";
}
So che questa discussione è inattiva da un po ', ma ho pensato di pubblicare una semplice routine di casting generica per questo tipo di cose:
// safely cast between types without breaking strict aliasing rules
template<typename ReturnType, typename OriginalType>
ReturnType Cast( OriginalType Variable )
{
union
{
OriginalType In;
ReturnType Out;
};
In = Variable;
return Out;
}
// example usage
int i = 0x3f800000;
float f = Cast<float>( i );
Spero che aiuti qualcuno!
Questo sembra un esempio di caso di quando usare reinterpret_cast
, qualsiasi altra cosa ti darà lo stesso effetto senza l'esplicitazione che ottieni dall'usare un costrutto linguistico per il suo uso ufficiale.