2008-12-06 11 views
20

Tengo curiosidad acerca de las convenciones para los punteros tipo-punning/matrices en C++. Aquí está el caso de uso que tengo en este momento:Opiniones sobre tipo-juego de palabras en C++?

Calcule una suma de comprobación simple de 32 bits sobre un conjunto binario de datos al tratarlo como una matriz de enteros de 32 bits (sabemos que su longitud total es un múltiplo de 4), y luego resumir todos los valores e ignorar el desbordamiento.

Yo esperaría una función de este tipo de aspecto:

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; 
} 

Ahora la pregunta que tengo es, ¿qué se tiene en cuenta la "mejor" para convertir a dataudata?

C-style cast?

udata = (const uint32_t *)data 

C++ cast que supone que todos los punteros son convertibles?

udata = reinterpret_cast<const uint32_t *>(data) 

C++ fundido que entre tipos de puntero arbitrarias usando el Intermedio void*?

udata = static_cast<const uint32_t *>(static_cast<const void *>(data)) 

¿Echar a través de una unión?

union { 
    const uint32_t *udata; 
    const char *cdata; 
}; 
cdata = data; 
// now use udata 

Soy plenamente consciente de que esto no será una solución portátil 100%, pero sólo estoy esperando a usarlo en un pequeño conjunto de plataformas en el que sé que funciona (memoria a saber, no alineada accesos y supuestos compilador de puntero aliasing). ¿Qué recomendarías?

Respuesta

12

En lo que respecta al estándar C++, la respuesta de litb es completamente correcta y la más portátil. Casting const char *data a const uint3_t *, ya sea a través de un molde de estilo C, static_cast o reinterpret_cast, rompe las reglas de alias estrictas (ver Understanding Strict Aliasing). Si compila con optimización completa, hay una buena probabilidad de que el código no sea el correcto.

Fundir a través de una unión (como litb my_reint) es probablemente la mejor solución, aunque infringe técnicamente la regla de que si escribe en una unión a través de un miembro y lo lee en otra, se produce un comportamiento indefinido. Sin embargo, prácticamente todos los compiladores lo soportan y da como resultado el esperado. Si desea cumplir estrictamente con el estándar 100%, vaya con el método de cambio de bit. De lo contrario, recomiendo ir a través de un sindicato, lo que probablemente te proporcione un mejor rendimiento.

+0

las soluciones de litb son correctas según el estándar, pero como ya he dicho, ya estoy buscando plataformas específicas. – Tom

+0

no estoy seguro de por qué rechazaron esto :) pero tampoco estoy seguro de que mi uso de la unión sea un comportamiento indefinido. Soy consciente de que escribir a un miembro y leer de otro miembro es un comportamiento indefinido. Pero en mi caso, estoy apuntando a un miembro de él, que se supone que tiene un valor válido, y lo leí luego. –

+2

No creo que este ejemplo en particular rompa el alias estricto. char * es un caso especial bajo estrictas reglas de aliasing - un char * nunca se puede suponer, no un alias de un puntero a algún otro tipo. Pero en mi respuesta todavía juego seguro: simplemente no vale la pena hacer char * de manera diferente a otros casos similares. –

-4

Esto parece un ejemplo de libro de casos de cuándo usar reinterpret_cast, cualquier otra cosa le dará el mismo efecto sin la explicitud que obtiene al usar una construcción de lenguaje para su uso oficial.

+1

Como se dijo en MSDN (http://msdn.microsoft.com/en-us/library/e0w9f63b%28v=vs.80%29.aspx): "El resultado de un reinterpret_cast no se puede usar con seguridad para nada que no sea volver a su tipo original. Otros usos son, en el mejor de los casos, no portables ". –

+1

@ JensÅkerblom, cierto, pero todos los demás métodos también son UB (por eso dije en mi respuesta _le daré el mismo efecto_). – Motti

5

eficiencia Haciendo caso omiso, por simplicidad de código que haría:

#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); 
} 

También me gusta la última respuesta de litb, el que cambia cada Char, a su vez, excepto que ya carbón podría ser firmado, creo que es necesita una máscara extra:

checksum += ((data[i] && 0xFF) << shift[i % 4]); 

Cuando el tipo de juego de palabras es un problema potencial, prefiero no escribir un juego de palabras en lugar de tratar de hacerlo de manera segura. Si no crea ningún puntero alias de distintos tipos, no tiene que preocuparse de lo que el compilador podría hacer con los alias, y tampoco lo hace el programador de mantenimiento que ve sus múltiples static_casts a través de una unión.

Si no desea asignar tanta memoria extra, entonces:

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; 
} 

optimización Suficiente se librará del establecimiento de memoria y la variable uint32_t adicional por completo en gcc, y acaba de leer un valor entero no alineado , cualquiera que sea la forma más eficiente de hacerlo, está en su plataforma, directamente fuera de la matriz de origen. Espero que lo mismo sea cierto para otros compiladores "serios". Pero este código ahora es más grande que el de litb, así que no hay mucho que decir aparte del mío es más fácil convertirlo en una plantilla de función que funcionará igual de bien con uint64_t, y el mío funciona como endianidad nativa en lugar de elegir poco -Endian.

Esto, por supuesto, no es completamente portátil. Supone que la representación de almacenamiento de sizeof (uint32_t) chars corresponde a la representación de almacenamiento de uin32_t de la manera que queremos. Esto está implícito en la pregunta, ya que establece que uno puede ser "tratado como" el otro. Endian-ness, si un char es de 8 bits, y si uint32_t usa todos los bits en su representación de almacenamiento, obviamente puede entrometerse, pero la pregunta implica que no lo harán.

+1

Acabo de probar su último ejemplo. GCC se niega a vectorizarlo, quejándose de "referencias de datos no controladas". – Tom

+0

Justo lo suficiente, no es lo más rápido posible en hardware que admite operaciones vectoriales. Quizás GCC mejore en el futuro. Mi gurú de la programación extrema dice que no necesito perder el sueño por eso. Mi primera sugerencia no es tan rápida como sea posible en * any * hardware :-) –

+2

Pero estoy de acuerdo con que, como mencioné que la memcpy es de bajo costo, el hecho de que impida la vectorización podría ser un impedimento para algunas aplicaciones. –

-3

Sé que este mensaje ha estado inactivo durante un tiempo, pero pensé que había puesto una rutina de fundición genérica simple para este tipo de cosas:

// 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); 

espero que ayude a alguien!

+5

-1 para un comportamiento indefinido. – Puppy

+4

El tipo de juego de palabras no está definido en el estándar. Sin embargo, el tipo de juego de palabras mediante uniones está respaldado por al menos GCC (con iirc con aliasing estricto habilitado). –

+2

¿Es indefinido? Según entiendo, se trata de un comportamiento no especificado, solo si los tamaños de los tipos son diferentes, no están definidos. Asegúrate de que coincida con el tamaño, por seguridad. (http://stackoverflow.com/questions/11639947/is-type-punning-through-un-union-unspecified-in-c99-and-has-it-become-specified) – Hybrid

1

Hay mis cincuenta centavos, diferentes formas de hacerlo.

#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"; 
} 
+1

-1 para los modelos antiguos C y no indica que el tipo de juego a través de una "unión" es un comportamiento indefinido ya que solo el miembro de último acceso tiene un valor determinado por el estándar –

Cuestiones relacionadas