¿Por qué no hay un modificador de endianness en C++ como el que hay para la firma?
language-features static-typing (9)
(Creo que esta pregunta podría aplicarse a muchos lenguajes mecanografiados, pero elegí usar C ++ como ejemplo).
¿Por qué no hay forma de simplemente escribir:
struct foo {
little int x; // little-endian
big long int y; // big-endian
short z; // native endianness
};
especificar la endianness para miembros específicos, variables y parámetros?
Comparación con la firma
Entiendo que el tipo de una variable no solo determina cuántos bytes se usan para almacenar un valor, sino también cómo se interpretan esos bytes al realizar los cálculos.
Por ejemplo, estas dos declaraciones asignan cada una un byte y, para ambos bytes, cada posible secuencia de 8 bits es un valor válido:
signed char s;
unsigned char u;
pero la misma secuencia binaria podría interpretarse de manera diferente, por ejemplo,
11111111
significaría -1 cuando se asigna a
s
pero 255 cuando se asigna a
u
.
Cuando las variables con y sin signo están involucradas en el mismo cálculo, el compilador (principalmente) se encarga de las conversiones adecuadas.
En mi opinión, la endianidad es solo una variación del mismo principio: una interpretación diferente de un patrón binario basado en información en tiempo de compilación sobre la memoria en la que se almacenará.
Parece obvio tener esa característica en un lenguaje escrito que permite la programación de bajo nivel. Sin embargo, esto no es parte de C, C ++ ni de ningún otro lenguaje que conozca, y no encontré ninguna discusión sobre esto en línea.
Actualizar
Trataré de resumir algunas conclusiones de los muchos comentarios que recibí en la primera hora después de preguntar:
- la firma es estrictamente binaria (con o sin signo) y siempre lo será, en contraste con la endianidad, que también tiene dos variantes conocidas (grande y pequeña), pero también variantes menos conocidas, como endian mixta / media. Se podrían inventar nuevas variantes en el futuro.
- la endianidad es importante cuando se accede a valores de bytes múltiples por bytes. Hay muchos aspectos más allá de la simple endianness que afectan el diseño de la memoria de las estructuras de varios bytes, por lo que este tipo de acceso se desaconseja.
- C ++ tiene como objetivo apuntar a una máquina abstracta y minimizar el número de supuestos sobre la implementación. Esta máquina abstracta no tiene ninguna resistencia.
Además, ahora me doy cuenta de que la firma y la endianidad no son una analogía perfecta, porque:
-
endianness solo define
cómo
se representa algo como una secuencia binaria, pero ahora
qué se puede
representar.
Tanto
big int
comolittle int
tendrían exactamente el mismo rango de valores. -
la firma define
cómo los
bits y los valores reales se mapean entre sí, pero también afecta
lo que se puede
representar, por ejemplo, -3 no se puede representar con un
unsigned char
y (suponiendo que elchar
tiene 8 bits) 130 no se puede representar con unsigned char
.
De modo que cambiar el endianness de algunas variables nunca cambiaría el comportamiento del programa (excepto el acceso por bytes), mientras que un cambio de firma generalmente lo haría.
Lo que dice el estándar
[intro.abstract]/1
:Las descripciones semánticas en este documento definen una máquina abstracta no determinizada parametrizada. Este documento no exige requisitos en la estructura de implementaciones conformes. En particular, no necesitan copiar o emular la estructura de la máquina abstracta. Más bien, se requieren implementaciones conformes para emular (solo) el comportamiento observable de la máquina abstracta como se explica a continuación.
C ++ no pudo definir un calificador de endianness ya que no tiene un concepto de endianness.
Discusión
Sobre la diferencia entre significación y endianness, OP escribió
En mi opinión, la endianidad es solo una variación del mismo principio [(firma)]: una interpretación diferente de un patrón binario basado en información en tiempo de compilación sobre la memoria en la que se almacenará.
Yo diría que la firma tiene un aspecto semántico y otro representativo
1
.
Lo que
[intro.abstract]/1
implica es que C ++ solo se preocupa por la
semántica
, y nunca aborda la forma en que un número con signo debe representarse en la memoria
2
.
En realidad, el
"bit de signo"
solo aparece una vez en las especificaciones de C ++
y se refiere a un valor definido por la implementación.
Por otro lado, la endianidad solo tiene un aspecto representativo: la
endianness no transmite significado
.
Con C ++ 20, aparece
std::endian
.
Todavía está definido por la implementación, pero probemos el endian del host sin depender de
viejos trucos basados en un comportamiento indefinido
.
1)
Aspecto semántico: un entero con signo puede representar valores por debajo de cero;
aspecto representativo: uno necesita, por ejemplo, reservar un poco para transmitir el signo positivo / negativo.
2)
En la misma línea, C ++ nunca describe cómo se debe representar un número de coma flotante, a menudo se usa IEEE-754, pero esta es una elección hecha por la implementación, en cualquier caso aplicada por el estándar:
[basic.fundamental]/8
"La representación del valor de los tipos de punto flotante está definida por la implementación"
.
Además de la respuesta de YSC, tomemos su código de muestra y consideremos lo que podría alcanzar
struct foo {
little int x; // little-endian
big long int y; // big-endian
short z; // native endianness
};
Puede esperar que esto especifique exactamente el diseño para el intercambio de datos independiente de la arquitectura (archivo, red, lo que sea)
Pero esto no puede funcionar, porque todavía hay varias cosas sin especificar:
-
tamaño del tipo de datos: tendrías que usar
little int32_t
,big int64_t
eint16_t
respectivamente, si eso es lo que quieres -
relleno y alineación, que no se pueden controlar estrictamente dentro del lenguaje: use
#pragma
o__attribute__((packed))
o alguna otra extensión específica del compilador - formato real (firma de complemento 1s o 2s, diseño de tipo de punto flotante, representaciones de trampa)
Alternativamente, es posible que simplemente desee reflejar la resistencia de un hardware específico, pero
big
y
little
no cubren todas las posibilidades aquí (solo las dos más comunes).
Por lo tanto, la propuesta es incompleta (no distingue todos los arreglos razonables de orden de bytes), ineficaz (no logra lo que se propone) y tiene inconvenientes adicionales:
-
Actuación
Cambiar la endianness de una variable del orden de bytes nativo debería deshabilitar la aritmética, las comparaciones, etc. (ya que el hardware no puede realizarlas correctamente en este tipo), o debe inyectar silenciosamente más código, creando temporarios ordenados de forma nativa para trabajar.
El argumento aquí no es que la conversión manual a / desde el orden de bytes nativo es más rápida , es que controlarla explícitamente hace que sea más fácil minimizar el número de conversiones innecesarias y mucho más fácil razonar sobre cómo se comportará el código, que si las conversiones son implícito.
-
Complejidad
Todo lo sobrecargado o especializado para tipos enteros ahora necesita el doble de versiones, para hacer frente al raro evento de que se le pase un valor de endianness no nativo. Incluso si eso es solo un contenedor de reenvío (con un par de conversiones para traducir a / desde pedidos nativos), sigue siendo una gran cantidad de código sin beneficio perceptible.
El argumento final en contra de cambiar el idioma para admitir esto es que puede hacerlo fácilmente en código. Cambiar la sintaxis del idioma es un gran problema y no ofrece ningún beneficio obvio sobre algo como una envoltura de tipo:
// store T with reversed byte order
template <typename T>
class Reversed {
T val_;
static T reverse(T); // platform-specific implementation
public:
explicit Reversed(T t) : val_(reverse(t)) {}
Reversed(Reversed const &other) : val_(other.val_) {}
// assignment, move, arithmetic, comparison etc. etc.
operator T () const { return reverse(val_); }
};
Desde una perspectiva pragmática del programador que busca , vale la pena señalar que el espíritu de esta pregunta se puede responder con una biblioteca de utilidad. Boost tiene una biblioteca de este tipo:
http://www.boost.org/doc/libs/1_65_1/libs/endian/doc/index.html
La característica de la biblioteca más parecida a la característica del lenguaje en discusión es un conjunto de tipos aritméticos como
big_int16_t
.
Endianness es específico del compilador como resultado de ser específico de la máquina, no como un mecanismo de soporte para la independencia de la plataforma. El estándar, es una abstracción que no tiene en cuenta la imposición de reglas que hacen que las cosas sean "fáciles", su tarea es crear similitudes entre los compiladores que le permite al programador crear "independencia de plataforma" para su código, si así lo eligen. asi que.
Inicialmente, había mucha competencia entre las plataformas por la participación en el mercado y también: los compiladores a menudo eran escritos como herramientas patentadas por fabricantes de microprocesadores y para soportar sistemas operativos en plataformas de hardware específicas. Intel probablemente no estaba muy preocupado por escribir compiladores que admitieran microprocesadores Motorola.
C fue, después de todo, inventado por Bell Labs para reescribir Unix.
Endianness no es inherentemente una parte de un tipo de datos, sino más bien de su diseño de almacenamiento.
Como tal, no sería realmente similar a firmado / no firmado, sino más bien como anchos de campo de bits en estructuras. Similar a esos, podrían usarse para definir API binarias.
Entonces tendrías algo como
int ip : big 32;
que definiría tanto el diseño de almacenamiento como el tamaño entero, dejando que el compilador haga el mejor trabajo de hacer coincidir el uso del campo con su acceso. No es obvio para mí cuáles deberían ser las declaraciones permitidas.
Esa es una buena pregunta y a menudo he pensado que algo como esto sería útil. Sin embargo, debe recordar que C tiene como objetivo la independencia y la resistencia de la plataforma solo es importante cuando una estructura como esta se convierte en un diseño de memoria subyacente. Esta conversión puede ocurrir cuando se lanza un búfer uint8_t en un int, por ejemplo. Si bien un modificador de endianness se ve bien, el programador aún necesita considerar otras diferencias de plataforma, como los tamaños int y la alineación y empaque de la estructura. Para la programación defensiva cuando desea encontrar control de grano sobre cómo se representan algunas variables o estructuras en un búfer de memoria, lo mejor es codificar funciones de conversión explícitas y luego dejar que el optimizador del compilador genere el código más eficiente para cada plataforma compatible.
Los enteros (como concepto matemático) tienen el concepto de números positivos y negativos. Este concepto abstracto de signo tiene varias implementaciones diferentes en hardware.
Endianness no es un concepto matemático. Little-endian es un truco de implementación de hardware para mejorar el rendimiento de la aritmética de enteros de dos bytes de varios complementos en un microprocesador con registros de 16 o 32 bits y un bus de memoria de 8 bits. Su creación requirió el uso del término big-endian para describir todo lo demás que tenía el mismo orden de bytes en los registros y en la memoria.
La máquina abstracta C incluye el concepto de enteros con y sin signo, sin detalles, sin requerir aritmética de dos complementos, bytes de 8 bits o cómo almacenar un número binario en la memoria.
PD: Acepto que la compatibilidad de datos binarios en la red o en la memoria / almacenamiento es un PIA.
Porque nadie ha propuesto agregarlo al estándar, y / o porque el implementador del compilador nunca ha sentido la necesidad de hacerlo.
Quizás puedas proponerlo al comité. No creo que sea difícil implementarlo en un compilador: los compiladores ya proponen tipos fundamentales que no son tipos fundamentales para la máquina de destino.
El desarrollo de C ++ es un asunto de todos los codificadores de C ++.
@Schimmel.
¡No escuches a las personas que justifican el status quo!
Todos los argumentos citados para justificar esta ausencia son más que frágiles.
Un estudiante de lógica podría encontrar su inconsistencia sin saber nada sobre informática.
Simplemente proponlo, y no te preocupes por los conservadores patológicos.
(Consejo: proponga nuevos tipos en lugar de un calificador porque las palabras clave
unsigned
y
signed
se consideran errores).
Respuesta corta: si no fuera posible usar objetos en expresiones aritméticas (sin operadores sobrecargados) que involucren ints, entonces estos objetos no deberían ser tipos enteros. Y no tiene sentido permitir la suma y multiplicación de ints big-endian y little-endian en la misma expresión.
Respuesta larga:
Como alguien mencionó, el endianness es específico del procesador. Lo que realmente significa que así es como se representan los números cuando se usan como números en el lenguaje de máquina (como direcciones y como operandos / resultados de operaciones aritméticas).
Lo mismo es cierto para la señalización. Pero no en el mismo grado. La conversión de la señalización semántica del lenguaje a la señalización aceptada por el procesador es algo que debe hacerse para usar números como números. La conversión de big-endian a little-endian y viceversa es algo que debe hacerse para usar números como datos (enviarlos a través de la red o representar metadatos sobre datos enviados a través de la red, como las longitudes de carga útil).
Dicho esto, esta decisión parece estar impulsada principalmente por casos de uso. La otra cara es que hay una buena razón pragmática para ignorar ciertos casos de uso. El pragmatismo surge del hecho de que la conversión de endianness es más costosa que la mayoría de las operaciones aritméticas.
Si un lenguaje tuviera semántica para mantener los números como little-endian, permitiría a los desarrolladores dispararse en el pie al forzar little-endianness de los números en un programa que hace mucha aritmética. Si se desarrolla en una máquina little-endian, esta aplicación de endianness sería un no-op. Pero cuando se transporta a una máquina big endian, habría muchas ralentizaciones inesperadas. Y si las variables en cuestión se usaran tanto para aritmética como para datos de red, el código sería completamente no portátil.
No tener estas semánticas endianas o forzarlas a ser explícitamente específicas del compilador obliga a los desarrolladores a seguir el paso mental de pensar que los números están "leídos" o "escritos" en / desde el formato de red. Esto haría que el código que se convierte de un lado a otro entre la red y el orden de bytes del host, en medio de las operaciones aritméticas, sea engorroso y menos probable que sea la forma preferida de escribir por un desarrollador perezoso.
Y dado que el desarrollo es un esfuerzo humano, tomar malas decisiones incómodas es una buena cosa (TM).
Editar
: aquí hay un ejemplo de cómo esto puede ir mal:
Supongamos
que se introducen los tipos
little_endian_int32
y
big_endian_int32
.
Entonces
little_endian_int32(7) % big_endian_int32(5)
es una expresión constante.
¿Cuál es su resultado?
¿Los números se convierten implícitamente al formato nativo?
Si no, ¿cuál es el tipo de resultado?
Peor aún, ¿cuál es el valor del resultado (que en este caso probablemente debería ser el mismo en todas las máquinas)?
Nuevamente, si se usan números de varios bytes como datos simples, entonces las matrices de caracteres son igual de buenas. Incluso si son "puertos" (que realmente son valores de búsqueda en tablas o sus hashes), son solo secuencias de bytes en lugar de tipos enteros (en los que se puede hacer aritmética).
Ahora, si limita las operaciones aritméticas permitidas en números explícitamente endianos a solo aquellas operaciones permitidas para los tipos de puntero, entonces podría tener un mejor caso para la previsibilidad.
Entonces
myPort + 5
tiene sentido incluso si
myPort
se declara como algo así como
little_endian_int16
en una máquina endian grande.
Lo mismo para
lastPortInRange - firstPortInRange + 1
.
Si la aritmética funciona como lo hace para los tipos de puntero, esto haría lo que cabría esperar, pero
firstPort * 10000
sería ilegal.
Luego, por supuesto, entras en el argumento de si la característica hinchada está justificada por algún beneficio posible.