c++ - descargar - ¿Cómo interpreto la carga de un mensaje sin violar las reglas de alias de tipo?
c++ manual (4)
Mi programa recibe mensajes en la red. Estos mensajes son deserializados por algún middleware (es decir, código de otra persona que no puedo cambiar). Mi programa recibe objetos que se parecen a esto:
struct Message {
int msg_type;
std::vector<uint8_t> payload;
};
Al examinar msg_type
puedo determinar que la carga útil del mensaje es en realidad, por ejemplo, una matriz de valores uint16_t
. Me gustaría leer esa matriz sin una copia innecesaria.
Mi primer pensamiento fue hacer esto:
const uint16_t* a = reinterpret_cast<uint16_t*>(msg.payload.data());
Pero luego leer de a
parecería violar el estándar. Aquí está la cláusula 3.10.10:
Si un programa intenta acceder al valor almacenado de un objeto a través de un glvalue distinto de uno de los siguientes tipos, el comportamiento no está definido:
- el tipo dinámico del objeto,
- una versión cv-calificada del tipo dinámico del objeto,
- un tipo similar (como se define en 4.4) al tipo dinámico del objeto,
- un tipo que es el tipo firmado o no firmado correspondiente al tipo dinámico del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión cv-calificada del tipo dinámico del objeto,
- un agregado o tipo de unión que incluye uno de los tipos mencionados anteriormente entre sus elementos o miembros de datos no estáticos (incluido, recursivamente, un elemento o un elemento de datos no estático de un subaggregado o unión contenida),
- un tipo que es un tipo de clase base (posiblemente cv calificado) del tipo dinámico del objeto,
- un
char
o un tipo deunsigned char
.
En este caso, a
sería glvalue y uint16_t*
no parece cumplir ninguno de los criterios enumerados.
Entonces, ¿cómo trato la carga útil como una matriz de valores uint16_t
sin invocar un comportamiento indefinido o realizar una copia innecesaria?
Si quiere seguir estrictamente el estándar C ++ sin UB y no emplea extensiones de compilador no estándar, puede intentar:
uint16_t getMessageAt(const Message& msg, size_t i) {
uint16_t tmp;
memcpy(&tmp, msg.payload.data() + 2 * i, 2);
return tmp;
}
Las optimizaciones del compilador deberían evitar la copia de memcpy
aquí en el código máquina generado; ver, por ej., Tipo Punning, Alias estricto y Optimización .
De hecho, está copiando en el valor de retorno, pero dependiendo de lo que haga con él, esta copia también puede optimizarse (por ejemplo, este valor puede cargarse en un registro y usarse solo allí).
Si quiere ser estrictamente correcto, como dice el estándar que citó, no puede. Si desea que el comportamiento esté bien definido, tendrá que hacer la copia.
Si el código está destinado a ser portátil, tendrá que manejar endianness de cualquier manera y reconstruir sus valores de uint16_t a partir de uint8_t bytes individuales, y esto, por definición, requiere una copia.
Si realmente sabe lo que está haciendo, puede ignorar el estándar, y simplemente hacer la reinterpretación_cast que describió.
Soporte de GCC y clang -fno-strict-aliasing
para evitar que la optimización genere código roto. Por lo que sé, en el momento de escribir esto, el compilador de Visual Studio no tiene un indicador, y nunca realiza este tipo de optimizaciones, a menos que use declspec(restrict)
o __restrict
.
Si va a consumir los valores uno por uno, puede memcpy
a uint16_t
, o escribir payload[0] + 0x100 * payload[1]
etc., en cuanto a qué comportamiento desea. Esto no será "ineficiente".
Si tiene que llamar a una función que solo toma una matriz de uint16_t
, y no puede cambiar la estructura que entrega el Message
, entonces no tiene suerte. En Standard C ++ tendrás que hacer la copia.
Si está utilizando gcc o clang, otra opción es establecer -fno-strict-aliasing
mientras compila el código en cuestión.
Su código puede no ser UB (o línea de borde que depende de la sensibilidad del lector) si, por ejemplo, los datos vector
se han construido de esta manera:
Message make_array_message(uint16_t* x, size_t n){
Message m;
m.type = types::uint16_t_array;
m.payload.reserve(sizeof(uint16_t)*n);
std::copy(x,x+n,reinterpret_cast<uint16_t*>(m.payload.data()));
return m;
}
En este código, los datos del vector contienen una secuencia de uint16_t
incluso si se declara como uint8_t
. Para acceder a los datos con este puntero:
const uint16_t* a = reinterpret_cast<uint16_t*>(msg.payload.data());
Está perfectamente bien. Pero acceder a los datos del vector
como uint8_t
sería UB. El acceso a[1]
funcionaría en todos los compiladores, pero es UB en el estándar actual. Esto es posiblemente un defecto en el estándar, y el comité de estandarización de c ++ está trabajando para solucionarlo, ver P0593 Creación de objetos implícitos para la manipulación de objetos de bajo nivel .
A partir de ahora, en mi propio código, no me ocupo de los defectos en el estándar, prefiero seguir el comportamiento del compilador porque para este tema, este es un codificador y compilador que crea reglas y ¡el estándar seguirá!