¿Tiene sentido una prohibición 'larga'?

109

En multiplataforma C ++ de hoy (o C) mundo en el que tenemos :

Data model  | short |   int |   long | long long | pointers/size_t  | Sample operating systems
... 
LLP64/IL32P64   16      32      32     64           64                Microsoft Windows (x86-64 and IA-64)
LP64/I32LP64    16      32      64     64           64                Most Unix and Unix-like systems, e.g. Solaris, Linux, BSD, and OS X; z/OS
...

Lo que esto significa hoy, es que para cualquier número entero "común" (con signo), intserá suficiente y posiblemente todavía se pueda usar como el tipo entero predeterminado al escribir código de aplicación C ++. También, para fines prácticos actuales , tendrá un tamaño constante en todas las plataformas.

Si un caso de uso requiere al menos 64 bits, hoy podemos usarlo long long, aunque posiblemente use uno de los tipos que especifican bitness o el __int64tipo podría tener más sentido.

Esto queda longen el medio, y estamos considerando prohibir por completo el uso de longnuestro código de aplicación .

¿Tendría sentido , o hay un caso para usar longen código C ++ (o C) moderno que tiene que correr multiplataforma? (la plataforma es de escritorio, dispositivos móviles, pero no cosas como microcontroladores, DSP, etc.)


Posiblemente interesantes enlaces de fondo:

Martin Ba
fuente
14
¿Cómo manejará las llamadas a bibliotecas que usan mucho tiempo?
Ángel
14
longes la única forma de garantizar 32 bits. intpuede ser de 16 bits, por lo que para algunas aplicaciones no es suficiente. Sí, a intveces es de 16 bits en los compiladores modernos. Sí, la gente escribe software en microcontroladores. Yo diría que más personas escriben software que tiene más usuarios en microcontroladores que en el PC con el auge de los dispositivos iPhone y Android no mencionar el aumento de Arduino, etc.
slebetman
53
¿Por qué no prohibir char, short, int, long y long long, y utilizar los tipos [u] intXX_t?
Immibis
77
@slebetman Cavé un poco más profundo, parece que el requisito todavía está en su lugar, aunque está oculto en §3.9.1.3 donde el estándar C ++ establece: "Los tipos enteros con y sin signo deben cumplir las restricciones dadas en el estándar C, sección 5.2. 4.2.1. " Y en el estándar C §5.2.4.2.1 establece el rango mínimo, exactamente como lo escribió. Tenías toda la razón. :) Aparentemente, poseer una copia del estándar C ++ no es suficiente, también es necesario encontrar la copia del estándar C ++.
Tommy Andersen
11
Te estás perdiendo el mundo DOSBox / Turbo C ++ en el que inttodavía hay 16 bits. Odio decirlo, pero si vas a escribir sobre el "mundo multiplataforma de hoy", no puedes ignorar todo el subcontinente indio.
Carreras de ligereza en órbita

Respuestas:

17

La única razón por la que usaría longhoy es cuando llamo o implemento una interfaz externa que lo usa.

Como dice en su publicación, short e int tienen características razonablemente estables en todas las principales plataformas de escritorio / servidor / móviles hoy en día y no veo ninguna razón para que eso cambie en el futuro previsible. Entonces veo pocas razones para evitarlos en general.

longPor otro lado es un desastre. En todos los sistemas de 32 bits, sé que tenía las siguientes características.

  1. Tenía exactamente 32 bits de tamaño.
  2. Era del mismo tamaño que una dirección de memoria.
  3. Era del mismo tamaño que la unidad de datos más grande que se podía mantener en un registro normal y trabajar con una sola instrucción.

Se escribieron grandes cantidades de código en función de una o más de estas características. Sin embargo, con el cambio a 64 bits no fue posible preservarlos a todos. Las plataformas tipo Unix eligieron LP64 que conservó las características 2 y 3 a costa de la característica 1. Win64 eligió LLP64 que conservó la característica 1 a costa de las características 2 y 3. El resultado es que ya no puede confiar en ninguna de esas características y que la OMI deja pocas razones para usar long.

Si desea un tipo que tenga exactamente 32 bits de tamaño, debe usarlo int32_t.

Si desea un tipo que tenga el mismo tamaño que un puntero, debe usar intptr_t(o mejor uintptr_t).

Si desea un tipo que sea el elemento más grande en el que se pueda trabajar en un solo registro / instrucción, desafortunadamente no creo que el estándar proporcione uno. size_tdebería estar en la mayoría de las plataformas comunes pero no en x32 .


PD

No me molestaría con los tipos "rápido" o "mínimo". Los tipos "menos importantes" solo importan si le importa la portabilidad para ocultar realmente las arquitecturas donde CHAR_BIT != 8. El tamaño de los tipos "rápidos" en la práctica parece ser bastante arbitrario. Linux parece hacerlos al menos del mismo tamaño que el puntero, lo cual es una tontería en plataformas de 64 bits con soporte rápido de 32 bits como x86-64 y arm64. IIRC iOS los hace lo más pequeños posible. No estoy seguro de lo que hacen otros sistemas.


PPS

Una razón para usar unsigned long(pero no es simple long) es porque se garantiza que tiene un comportamiento de módulo. Desafortunadamente, debido a las reglas de promoción arruinadas de C, los tipos sin signo más pequeños que intno tienen un comportamiento de módulo.

En todas las plataformas principales hoy en día uint32_tes del mismo tamaño o más grande que int y, por lo tanto, tiene un comportamiento de módulo. Sin embargo, ha habido históricamente y, en teoría, podría haber en el futuro plataformas donde intsea ​​de 64 bits y, por uint32_tlo tanto , no tenga un comportamiento de módulo.

Personalmente, diría que es mejor entrar en el hábito de forzar el comportamiento del módulo usando "1u *" o "0u +" al comienzo de sus ecuaciones, ya que esto funcionará para cualquier tamaño de tipo sin signo.

Peter Green
fuente
1
Todos los tipos de "tamaño especificado" serían mucho más útiles si pudieran especificar la semántica que difiere de los tipos incorporados. Por ejemplo, sería útil tener un tipo que usaría la aritmética mod-65536 independientemente del tamaño de "int", junto con un tipo que sería capaz de contener números del 0 al 65535 pero podría ser arbitraria y no necesariamente necesariamente capaz de tener números más grandes que eso. El tipo de tamaño más rápido en la mayoría de las máquinas dependerá del contexto, por lo que permitir que el compilador elija arbitrariamente sería óptimo para la velocidad.
supercat
204

Como mencionas en tu pregunta, el software moderno tiene que ver con la interoperabilidad entre plataformas y sistemas en Internet. Los estándares C y C ++ proporcionan rangos para tamaños de tipo entero, no tamaños específicos (en contraste con lenguajes como Java y C #).

Para asegurarse de que su software compilado en diferentes plataformas funcione con los mismos datos de la misma manera y para garantizar que otro software pueda interactuar con su software usando los mismos tamaños, debe usar enteros de tamaño fijo.

Ingrese <cstdint>cuál proporciona exactamente eso y es un encabezado estándar que todas las plataformas de compilación y biblioteca estándar deben proporcionar. Nota: este encabezado solo se requería a partir de C ++ 11, pero muchas implementaciones de bibliotecas más antiguas lo proporcionaron de todos modos.

¿Quieres un entero sin signo de 64 bits? Utilizar uint64_t. Entero de 32 bits firmado Utilizar int32_t. Si bien los tipos en el encabezado son opcionales, las plataformas modernas deben admitir todos los tipos definidos en ese encabezado.

A veces se necesita un ancho de bit específico, por ejemplo, en una estructura de datos utilizada para comunicarse con otros sistemas. Otras veces no lo es. Para situaciones menos estrictas, <cstdint>proporciona tipos que tienen un ancho mínimo.

Hay menos variantes: int_leastXX_tserá un tipo entero de mínimo XX bits. Utilizará el tipo más pequeño que proporciona XX bits, pero se permite que el tipo sea mayor que el número especificado de bits. En la práctica, estos son típicamente los mismos que los tipos descritos anteriormente que dan un número exacto de bits.

También hay variantes rápidas : int_fastXX_tes de al menos XX bits, pero debe usar un tipo que funcione rápidamente en una plataforma en particular. La definición de "rápido" en este contexto no está especificada. Sin embargo, en la práctica, esto generalmente significa que un tipo más pequeño que el tamaño de registro de una CPU puede alias a un tipo del tamaño de registro de la CPU. Por ejemplo, el encabezado de Visual C ++ 2015 especifica que int_fast16_tes un número entero de 32 bits porque la aritmética de 32 bits es en general más rápida en x86 que la aritmética de 16 bits.

Todo esto es importante porque debe poder usar tipos que puedan contener los resultados de los cálculos que realiza su programa independientemente de la plataforma. Si un programa produce resultados correctos en una plataforma pero resultados incorrectos en otra debido a diferencias en el desbordamiento de enteros, eso es malo. Al usar los tipos enteros estándar, usted garantiza que los resultados en diferentes plataformas serán los mismos con respecto al tamaño de los enteros utilizados (por supuesto, podría haber otras diferencias entre las plataformas además del ancho del entero).

Entonces sí, longdebería ser prohibido del código C ++ moderno. Por lo que debe int, shorty long long.


fuente
20
Desearía tener otras cinco cuentas para votar esto un poco más.
Steven Burnap
44
+1, he tratado algunos errores de memoria extraños que solo ocurren cuando el tamaño de una estructura depende de la computadora en la que estás compilando.
Joshua Snider
99
@Wildcard es un encabezado C que también forma parte de C ++: vea el prefijo "c" en él. También hay alguna forma de poner los typedefs en el stdespacio de nombres cuando #included en una unidad de compilación C ++, pero la documentación que vinculé no lo menciona y a Visual Studio parece no importarle cómo accedo a ellos.
11
La prohibición intpuede ser ... ¿excesiva? (Lo consideraría si el código necesita ser extremadamente portátil en todas las plataformas oscuras (y no tan oscuras). Prohibirlo para "código de aplicación" puede no estar muy bien con nuestros desarrolladores.
Martin Ba
55
#include <cstdint>Se requiere @Snowman para poner los tipos std::y (lamentablemente) opcionalmente también se permite ponerlos en el espacio de nombres global. #include <stdint.h>Es exactamente lo contrario. Lo mismo se aplica a cualquier otro par de encabezados C. Ver: stackoverflow.com/a/13643019/2757035 Desearía que el estándar hubiera requerido que cada uno solo afectara su respectivo espacio de nombres requerido, en lugar de aparentemente ceñirse a las convenciones pobres establecidas por algunas implementaciones, pero bueno, aquí estamos.
underscore_d
38

No, prohibir los tipos enteros integrados sería absurdo. Sin embargo, tampoco deben ser abusados.

Si necesita un número entero que tenga exactamente N bits de ancho, use (o si necesita una versión). Pensar en un entero de 32 bits y como un entero de 64 bits es simplemente incorrecto. Puede ser que sea así en sus plataformas actuales, pero esto se basa en un comportamiento definido por la implementación.std::intN_tstd::uintN_tunsignedintlong long

El uso de tipos enteros de ancho fijo también es útil para interactuar con otras tecnologías. Por ejemplo, si algunas partes de su aplicación están escritas en Java y otras en C ++, es probable que desee hacer coincidir los tipos enteros para obtener resultados consistentes. (Tenga en cuenta que el desbordamiento en Java tiene una semántica bien definida, mientras que el signeddesbordamiento en C ++ es un comportamiento indefinido, por lo que la coherencia es un objetivo importante). También serán invaluables al intercambiar datos entre diferentes hosts informáticos.

Si no necesita exactamente N bits, pero solo un tipo que sea lo suficientemente ancho , considere usar (optimizado para el espacio) u (optimizado para la velocidad). Nuevamente, ambas familias también tienen contrapartes.std::int_leastN_tstd::int_fastN_tunsigned

Entonces, ¿cuándo usar los tipos incorporados? Bueno, dado que el estándar no especifica su ancho con precisión, úselos cuando no le interese el ancho de bits real sino otras características.

A chares el número entero más pequeño que el hardware puede direccionar. El lenguaje en realidad te obliga a usarlo para aliasing memoria arbitraria. También es el único tipo viable para representar cadenas de caracteres (estrechas).

Un intserá generalmente el tipo más rápido que la máquina puede manejar. Será lo suficientemente ancho como para que pueda cargarse y almacenarse con una sola instrucción (sin tener que enmascarar o cambiar bits) y lo suficientemente estrecho para que pueda operarse con (las) instrucciones de hardware más eficientes. Por lo tanto, intes una opción perfecta para pasar datos y hacer operaciones aritméticas cuando el desbordamiento no es una preocupación. Por ejemplo, el tipo subyacente predeterminado de enumeraciones es int. No lo cambie a un entero de 32 bits solo porque pueda. Además, si tiene un valor que solo puede ser –1, 0 y 1, unintes una opción perfecta, a menos que vaya a almacenar grandes matrices de ellos, en cuyo caso es posible que desee utilizar un tipo de datos más compacto a costa de tener que pagar un precio más alto para acceder a elementos individuales. El almacenamiento en caché más eficiente probablemente dará sus frutos. Muchas funciones del sistema operativo también se definen en términos de int. Sería una tontería convertir sus argumentos y resultados de un lado a otro. Todo lo que esto podría hacer es introducir errores de desbordamiento.

longgeneralmente será el tipo más ancho que puede manejarse con instrucciones de una sola máquina. Esto lo hace especialmente unsigned longatractivo para tratar datos sin procesar y todo tipo de cosas de manipulación de bits. Por ejemplo, esperaría ver unsigned longen la implementación de un vector de bits. Si el código se escribe con cuidado, no importa qué tan ancho sea realmente el tipo (porque el código se adaptará automáticamente). En plataformas donde la palabra máquina nativa es de 32 bits, la matriz de respaldo del vector de bits es una matriz deunsignedLos enteros de 32 bits son lo más deseable porque sería una tontería usar un tipo de 64 bits que debe cargarse mediante instrucciones costosas solo para cambiar y enmascarar los bits innecesarios nuevamente de todos modos. Por otro lado, si el tamaño de palabra nativo de la plataforma es de 64 bits, quiero una matriz de ese tipo porque significa que operaciones como "buscar el primer conjunto" pueden ejecutarse hasta el doble de rápido. Por lo tanto, el "problema" del longtipo de datos que está describiendo, que su tamaño varía de una plataforma a otra, en realidad es una característica que puede aprovecharse. Solo se convierte en un problema si piensa en los tipos incorporados como tipos de cierto ancho de bits, que simplemente no lo son.

char, inty longson tipos muy útiles como se describe anteriormente. shorty long longno son tan útiles porque su semántica es mucho menos clara.

5gon12eder
fuente
44
El OP destacó en particular la diferencia en el tamaño de longentre Windows y Unix. Podría estar malentendido, pero su descripción de la diferencia en el tamaño de longser una "característica" en lugar de un "problema" tiene sentido para mí al comparar modelos de datos de 32 y 64 bits, pero no para esta comparación en particular. En el caso particular de esta pregunta, ¿es realmente una característica? ¿O es una característica en otras situaciones (es decir, en general) e inofensiva en este caso?
Dan Getz
3
@ 5gon12eder: El problema es que tipos como uint32_t se crearon con el fin de permitir que el comportamiento del código sea independiente del tamaño de "int", pero la falta de un tipo cuyo significado sería "comportarse como un uint32_t funciona en un 32- bit system "hace que escribir código cuyo comportamiento sea correctamente independiente del tamaño de" int "sea mucho más difícil que escribir código que es casi correcto.
supercat
3
Sí, lo sé ... de ahí vino la maldición. Los autores originales simplemente tomaron el camino de la resistencia al arrendamiento porque cuando escribieron el código, los sistemas operativos de 32 bits estaban a más de una década de distancia.
Steven Burnap
8
@ 5gon12eder Lamentablemente, supercat es correcto. Todos los tipos exactos de ancho son "simplemente typedefs" y las reglas de la promoción número entero no se fijan de ellos, lo que significa que la aritmética de uint32_tlos valores se llevará a cabo como firmado , intla aritmética -width en una plataforma en la que intes más ancha que uint32_t. (Con ABIs de hoy, esto es muchísimo más probable que sea un problema para uint16_t.)
Zwol
99
Primero, gracias por una respuesta detallada. Pero: Oh querido. Su párrafo largo: " longgeneralmente será el tipo más amplio que puede manejarse con instrucciones de una sola máquina ...", y esto es exactamente incorrecto . Mira el modelo de datos de Windows. En mi humilde opinión, todo el siguiente ejemplo se descompone, porque en x64 Windows largo todavía es de 32 bits.
Martin Ba
6

Otra respuesta ya elabora los tipos de cstdint y las variaciones menos conocidas.

Me gustaría agregar a eso:

usar nombres de tipo específicos de dominio

Es decir, no declare que sus parámetros y variables son uint32_t(¡ciertamente no long!), Sino nombres como channel_id_type, room_count_typeetc.

sobre bibliotecas

Las bibliotecas de terceros que usan longo no pueden ser molestas, especialmente si se usan como referencias o punteros a ellas.

Lo mejor es hacer envoltorios.

En general, mi estrategia es crear un conjunto de funciones similares a las del molde que se utilizarán. Se sobrecargan para aceptar solo aquellos tipos que coinciden exactamente con los tipos correspondientes, junto con cualquier variación de puntero, etc. que necesite. Se definen específicamente para el os / compiler / settings. Esto le permite eliminar advertencias y, sin embargo, garantizar que solo se usen las conversiones "correctas".

channel_id_type cid_out;
...
SomeLibFoo (same_thing_really<int*>(&cid_out));

En particular, con diferentes tipos primitivos que producen 32 bits, su elección de cómo int32_tse define podría no coincidir con la llamada a la biblioteca (por ejemplo, int vs long en Windows).

La función similar a la conversión documenta el choque, proporciona una verificación en tiempo de compilación del resultado que coincide con el parámetro de la función y elimina cualquier advertencia o error si y solo si el tipo real coincide con el tamaño real involucrado. Es decir, está sobrecargado y definido si paso (en Windows) una int*o una long*y da un error en tiempo de compilación de lo contrario.

Entonces, si la biblioteca se actualiza o alguien cambia lo que channel_id_typees, esto continúa siendo verificado.

JDługosz
fuente
¿Por qué el voto negativo (sin comentarios)?
JDługosz
Porque la mayoría de los votos negativos en esta red aparecen sin comentarios ...
Ruslan