left - ¿Existe una alternativa portátil a los campos de bits de C++?
left shift c (5)
Hay muchas situaciones (especialmente en la programación de bajo nivel), donde el diseño binario de los datos es importante. Por ejemplo: manipulación de hardware / controladores, protocolos de red, etc.
En C ++, puedo leer / escribir estructuras binarias arbitrarias utilizando operaciones char*
y bitwise (máscaras y desplazamientos), pero eso es tedioso y propenso a errores. Obviamente, trato de limitar el alcance de estas operaciones y encapsularlas en API de nivel superior, pero sigue siendo un dolor.
Los campos de bits de C ++ parecen ofrecer una solución fácil para el desarrollador para este problema, pero desafortunadamente su almacenamiento es específico de la implementación .
NathanOliver mencionó std::bitset
que básicamente le permite acceder a bits individuales de un entero con un operator[]
agradable operator[]
pero carece de accesores para campos de múltiples bits.
Usando la meta-programación y / o las macros, es posible abstraer las operaciones bitwise en una biblioteca. Como no quiero reinventar la rueda, estoy buscando una biblioteca (preferiblemente STL o boost) que haga eso.
Para que quede constancia, estoy buscando en esto una resolución de DNS , pero el problema y su solución deberían ser genéricos.
Edición: respuesta corta : resulta que el almacenamiento del campo de bits es confiable en la práctica (incluso si no lo exige el estándar) ya que las bibliotecas de sistemas / redes los utilizan y desarrollan programas de buen comportamiento cuando se compilan con compiladores convencionales.
C está diseñado para la manipulación de bits de bajo nivel. Es bastante fácil declarar un búfer de caracteres sin firmar y establecerlo en cualquier patrón de bits que desee. Especialmente si sus cadenas de bits son muy cortas, por lo que se ajustan a uno de los tipos integrales.
Un problema potencial es la endiancia de bytes. C no puede "ver" esto en absoluto, pero al igual que los enteros tienen una endianness, también los bytes, cuando se serializan. Otra es la muy pequeña cantidad de máquinas que no usan octetos para bytes. C garantiza que un byte será al menos un octeto, pero 32 y 9 son implementaciones del mundo real. En esas circunstancias, debe decidir si simplemente ignorar los bits superiores (en cuyo caso el código ingenuo debería funcionar), o tratarlos como parte del flujo de bits (en cuyo caso debe tener cuidado de plegar CHAR_BIT). sus cálculos). También es difícil probar el código ya que es poco probable que le resulte fácil conseguir una máquina CHAR + BIT 32.
Del estándar C ++ 14 (borrador N3797), sección 9.6 [class.bit], párrafo 1:
La asignación de campos de bits dentro de un objeto de clase está definida por la implementación. La alineación de los campos de bits está definida por la implementación. Los campos de bits se empaquetan en alguna unidad de asignación direccionable. [Nota: los campos de bits dividen las unidades de asignación en algunas máquinas y no en otras. Los campos de bits se asignan de derecha a izquierda en algunas máquinas, de izquierda a derecha en otras máquinas. - nota final]
Aunque las notas no son normativas, cada implementación que conozco utiliza uno de los dos diseños: big-endian o little endian bit order.
Tenga en cuenta que:
- Debe especificar el relleno manualmente. Esto implica que debe conocer el tamaño de sus tipos (por ejemplo, utilizando
<cstdint>
). - Debes utilizar tipos sin firmar.
- Las macros del preprocesador para detectar el orden de los bits dependen de la implementación.
- Por lo general, el endianness de orden de bits es el mismo que el endianness de orden de bits. Sin embargo, creo que hay una marca de compilación para anularla, pero no puedo encontrarla.
Por ejemplo, busque en netinet/tcp.h
y otros encabezados cercanos.
Editar por OP: por ejemplo, tcp.h
define
struct
{
u_int16_t th_sport; /* source port */
u_int16_t th_dport; /* destination port */
tcp_seq th_seq; /* sequence number */
tcp_seq th_ack; /* acknowledgement number */
# if __BYTE_ORDER == __LITTLE_ENDIAN
u_int8_t th_x2:4; /* (unused) */
u_int8_t th_off:4; /* data offset */
# endif
# if __BYTE_ORDER == __BIG_ENDIAN
u_int8_t th_off:4; /* data offset */
u_int8_t th_x2:4; /* (unused) */
# endif
// ...
}
Y dado que funciona con compiladores convencionales, significa que el diseño de la memoria de bitset es confiable en la práctica.
Editar:
Esto es portátil dentro de un endianness:
struct Foo {
uint16_t x: 10;
uint16_t y: 6;
};
Pero esto puede no ser porque se extiende a una unidad de 16 bits:
struct Foo {
uint16_t x: 10;
uint16_t y: 12;
uint16_t z: 10;
};
Y esto puede no ser porque tiene relleno implícito:
struct Foo {
uint16_t x: 10;
};
Es sencillo implementar campos de bits con posiciones conocidas con C ++:
template<typename T, int POS, int SIZE>
struct BitField {
T *data;
BitField(T *data) : data(data) {}
operator int() const {
return ((*data) >> POS) & ((1ULL << SIZE)-1);
}
BitField& operator=(int x) {
T mask( ((1ULL << SIZE)-1) << POS );
*data = (*data & ~mask) | ((x << POS) & mask);
return *this;
}
};
La implementación de juguete anterior permite, por ejemplo, definir un campo de 12 bits en una variable unsigned long long
con
unsigned long long var;
BitField<unsigned long long, 7, 12> muxno(&var);
y el código generado para acceder al valor del campo es justo
0000000000000020 <_Z6getMuxv>:
20: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax ; Get &var
27: 48 8b 00 mov (%rax),%rax ; Get content
2a: 48 c1 e8 07 shr $0x7,%rax ; >> 7
2e: 25 ff 0f 00 00 and $0xfff,%eax ; keep 12 bits
33: c3 retq
Básicamente lo que tendrías que escribir a mano.
He escrito una implementación de campos de bits en C ++ como un archivo de encabezado de biblioteca. Un ejemplo que doy en la documentación es que, en lugar de escribir esto:
struct A
{
union
{
struct
{
unsigned x : 5;
unsigned a0 : 2;
unsigned a1 : 2;
unsigned a2 : 2;
}
u;
struct
{
unsigned x : 5;
unsigned all_a : 6;
}
v;
};
};
// …
A x;
x.v.all_a = 0x3f;
x.u.a1 = 0;
puedes escribir:
typedef Bitfield<Bitfield_traits_default<> > Bf;
struct A : private Bitfield_fmt
{
F<5> x;
F<2> a[3];
};
typedef Bitfield_w_fmt<Bf, A> Bwf;
// …
Bwf::Format::Define::T x;
BITF(Bwf, x, a) = 0x3f;
BITF(Bwf, x, a[1]) = 0;
Hay una interfaz alternativa, bajo la cual las dos últimas líneas de lo anterior cambiarían a:
#define BITF_U_X_BWF Bwf
#define BITF_U_X_BASE x
BITF(X, a) = 0x3f;
BITF(X, a[1]) = 0;
Al usar esta implementación de campos de bits, el parámetro de la plantilla de rasgos le da al programador mucha flexibilidad. La memoria es solo la memoria del procesador por defecto, o puede ser una abstracción, con el programador que proporciona funciones para realizar lecturas y escrituras en la "memoria". La memoria abstraída es una secuencia de elementos de cualquier tipo integral sin signo (elegido por el programador). Los campos se pueden establecer de menor o mayor importancia o de menor importancia. El diseño de los campos en la memoria puede ser lo contrario de lo que están en la estructura del formato.
La implementación se encuentra en: https://github.com/wkaras/C-plus-plus-library-bit-fields
(Como puede ver, desafortunadamente no pude evitar completamente el uso de macros).
Tenemos esto en el código de producción donde tuvimos que portar el código MIPS a x86-64
Funciona bien para nosotros.
Básicamente es una plantilla sin almacenamiento, los argumentos de la plantilla especifican la posición de los bits relevantes.
Si necesita varios campos, puede juntar varias especializaciones de la plantilla en una unión, junto con una matriz de bytes para proporcionar almacenamiento.
La plantilla tiene sobrecargas para la asignación de valor y un operador de conversión a unsigned
para leer el valor.
Además, si los campos son más grandes que un byte, se almacenan en el orden de bytes big-endian, que a veces es útil cuando se implementan protocolos multiplataforma.
Aquí hay un ejemplo de uso:
union header
{
unsigned char arr[2]; // space allocation, 2 bytes (16 bits)
BitFieldMember<0, 4> m1; // first 4 bits
BitFieldMember<4, 5> m2; // The following 5 bits
BitFieldMember<9, 6> m3; // The following 6 bits, total 16 bits
};
int main()
{
header a;
memset(a.arr, 0, sizeof(a.arr));
a.m1 = rand();
a.m3 = a.m1;
a.m2 = ~a.m1;
return 0;
}