Uma proibição "longa" faz sentido?

109

No mundo C ++ (ou C) de plataforma cruzada de hoje, temos :

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
...

O que isso significa hoje é que, para qualquer número inteiro "comum" (assinado), intserá suficiente e ainda poderá ser usado como o tipo inteiro padrão ao escrever o código do aplicativo C ++. Também - para propósitos práticos atuais - terá um tamanho consistente entre as plataformas.

Se um caso de uso requer pelo menos 64 bits, podemos usar hoje long long, embora possivelmente use um dos tipos de especificação de testemunha ou o __int64tipo possa fazer mais sentido.

Isso deixa longno meio, e estamos considerando proibir completamente o uso de longnosso código de aplicativo .

Isso faria sentido ou existe um caso para usar longno código C ++ (ou C) moderno que precisa executar plataforma cruzada? (plataforma sendo desktop, dispositivos móveis, mas não itens como microcontroladores, DSPs etc.)


Links de fundo possivelmente interessantes:

Martin Ba
fonte
14
Como você lida com chamadas para bibliotecas que usam muito tempo?
Ángel
14
longé a única maneira de garantir 32 bits. intpode ter 16 bits, portanto, para algumas aplicações, não é suficiente. Sim, intàs vezes são 16 bits em compiladores modernos. Sim, as pessoas escrevem software em microcontroladores. Eu diria mais pessoas escrever software que tem mais usuários em microcontroladores do que no PC com o surgimento de dispositivos iPhone e Android para não mencionar o surgimento de Arduinos etc.
slebetman
53
Por que não banir char, short, int, long e long long e usar os tipos [u] intXX_t?
immibis
7
@slebetman Eu cavei um pouco mais fundo, parece que o requisito ainda está em vigor, embora oculto no §3.9.1.3, onde o padrão C ++ declara: "Os tipos inteiros assinados e não assinados devem atender às restrições dadas no padrão C, seção 5.2. 4.2.1 " E no padrão C, §5.2.4.2.1, afirma o intervalo mínimo, exatamente como você escreveu. Você estava absolutamente certo. :) Aparentemente, possuir uma cópia do padrão C ++ não é suficiente, é preciso encontrar a cópia do padrão C também.
Tommy Andersen
11
Você está sentindo falta do mundo DOSBox / Turbo C ++, no qual intainda existem 16 bits. Detesto dizer isso, mas se você escrever sobre o "mundo entre plataformas de hoje", não poderá ignorar todo o subcontinente indiano.
Lightness Races em órbita

Respostas:

17

O único motivo que eu usaria longhoje é ao chamar ou implementar uma interface externa que a utilize.

Como você diz em seu post, short e int têm características razoavelmente estáveis ​​em todas as principais plataformas de desktop / servidor / celular hoje e não vejo razão para isso mudar no futuro próximo. Então, vejo poucas razões para evitá-las em geral.

longpor outro lado, está uma bagunça. Em todos os sistemas de 32 bits, estou ciente de que tinha as seguintes características.

  1. Tinha exatamente 32 bits de tamanho.
  2. Era do mesmo tamanho que um endereço de memória.
  3. Era do mesmo tamanho da maior unidade de dados que podia ser mantida em um registro normal e trabalhar com uma única instrução.

Grandes quantidades de código foram gravadas com base em uma ou mais dessas características. No entanto, com a mudança para 64 bits, não foi possível preservar todos eles. As plataformas tipo Unix foram para o LP64, que preservou as características 2 e 3 ao custo da característica 1. O Win64 foi para o LLP64, que preservou a característica 1 ao custo das características 2 e 3. O resultado é que você não pode mais confiar em nenhuma dessas características e que a IMO deixa poucas razões para usar long.

Se você deseja um tipo com tamanho exatamente de 32 bits, use int32_t.

Se você deseja um tipo que seja do mesmo tamanho que um ponteiro, use intptr_t(ou melhor uintptr_t).

Se você deseja um tipo que é o maior item que pode ser trabalhado em um único registro / instrução, infelizmente não acho que o padrão forneça um. size_tdeve estar certo nas plataformas mais comuns, mas não no x32 .


PS

Eu não me incomodaria com os tipos "rápido" ou "menos". Os tipos "menos" são importantes apenas se você se preocupa com a portabilidade para realmente obscurecer arquiteturas onde CHAR_BIT != 8. O tamanho dos tipos "rápidos" na prática parece bastante arbitrário. O Linux parece torná-los pelo menos do mesmo tamanho que o ponteiro, o que é bobo em plataformas de 64 bits com suporte rápido de 32 bits, como x86-64 e arm64. O IIRC iOS os torna o menor possível. Não tenho certeza do que outros sistemas fazem.


PPS

Um motivo para usar unsigned long(mas não é claro long) é porque é garantido o comportamento do módulo. Infelizmente, devido às regras de promoção erradas de C, os tipos não assinados são menores que o intcomportamento do módulo.

Hoje, em todas as principais plataformas, uint32_té do mesmo tamanho ou maior que o int e, portanto, possui um comportamento de módulo. No entanto, tem havido historicamente e, teoricamente, pode haver nas plataformas futuras int64 bits e, portanto uint32_t, não possui comportamento de módulo.

Pessoalmente, eu diria que é melhor entrar no hábito de forçar o comportamento do módulo usando "1u *" ou "0u +" no início de suas equações, pois isso funcionará para qualquer tamanho de tipo não assinado.

Peter Green
fonte
1
Todos os tipos de "tamanho especificado" seriam muito mais úteis se pudessem especificar semânticas diferentes dos tipos internos. Por exemplo, seria útil ter um tipo que usaria mod 65536-aritmética, independentemente do tamanho de "int", juntamente com um tipo que seria capaz de manter números 0 a 65535, mas poderia arbitrariamente e não necessariamente consistentemente ser capaz de manter números maiores que isso. Qual o tipo de tamanho mais rápido, na maioria das máquinas, depende do contexto; portanto, permitir que o compilador escolha arbitrariamente seria ideal para velocidade.
Supercat 6/16
204

Como você mencionou na sua pergunta, o software moderno é sobre interoperar entre plataformas e sistemas na Internet. Os padrões C e C ++ fornecem intervalos para tamanhos de tipo inteiro, não tamanhos específicos (em contraste com linguagens como Java e C #).

Para garantir que seu software compilado em plataformas diferentes funcione com os mesmos dados da mesma maneira e para garantir que outro software possa interagir com o software usando os mesmos tamanhos, você deve usar números inteiros de tamanho fixo.

Digite o <cstdint>que fornece exatamente isso e é um cabeçalho padrão que todas as plataformas de compilador e biblioteca padrão devem fornecer. Nota: esse cabeçalho era necessário apenas a partir do C ++ 11, mas muitas implementações de bibliotecas mais antigas o forneciam de qualquer maneira.

Deseja um número inteiro não assinado de 64 bits? Use uint64_t. Número inteiro de 32 bits assinado? Use int32_t. Embora os tipos no cabeçalho sejam opcionais, as plataformas modernas devem suportar todos os tipos definidos nesse cabeçalho.

Às vezes, é necessária uma largura de bit específica, por exemplo, em uma estrutura de dados usada para comunicação com outros sistemas. Outras vezes não é. Para situações menos rigorosas, <cstdint>fornece tipos com largura mínima.

Existem menos variantes: int_leastXX_tserá um tipo inteiro com no mínimo XX bits. Ele usará o menor tipo que fornece XX bits, mas é permitido que o tipo seja maior que o número especificado de bits. Na prática, estes são tipicamente os mesmos que os tipos descritos acima que fornecem o número exato de bits.

Também existem variantes rápidas : int_fastXX_ttem pelo menos XX bits, mas deve usar um tipo que tenha desempenho rápido em uma plataforma específica. A definição de "rápido" neste contexto não é especificada. No entanto, na prática, isso normalmente significa que um tipo menor que o tamanho de registro de uma CPU pode ser alias para um tipo de tamanho de registro de CPU. Por exemplo, o cabeçalho do Visual C ++ 2015 especifica que int_fast16_té um número inteiro de 32 bits porque a aritmética de 32 bits é geralmente mais rápida em x86 do que a aritmética de 16 bits.

Isso tudo é importante porque você deve poder usar tipos que possam conter os resultados dos cálculos que seu programa executa independentemente da plataforma. Se um programa produz resultados corretos em uma plataforma, mas resultados incorretos em outra devido a diferenças no excesso de número inteiro, isso é ruim. Ao usar os tipos de número inteiro padrão, você garante que os resultados em diferentes plataformas serão os mesmos em relação ao tamanho dos números inteiros usados (é claro que pode haver outras diferenças entre as plataformas além da largura do número inteiro).

Então, sim, longdeve ser banido do código C ++ moderno. Assim se int, shorte long long.


fonte
20
Eu gostaria de ter mais cinco contas para votar mais um pouco.
Steven Burnap 5/05
4
+1, lidei com alguns erros de memória estranhos que só acontecem quando o tamanho de uma estrutura depende do computador em que você está compilando.
Joshua Snider
9
@Wildcard é um cabeçalho C que também faz parte do C ++: veja o prefixo "c". Também existe uma maneira de colocar os typedefs no stdnamespace quando #included em uma unidade de compilação C ++, mas a documentação que vinculei não menciona isso e o Visual Studio parece não se importar com o acesso a eles.
11
A proibição intpode ser ... excessiva? (Eu consideraria isso se o código precisar ser extremamente portátil em todas as plataformas obscuras (e não tão obscuras). A proibição de "código do aplicativo" pode não se encaixar muito bem com nossos desenvolvedores.
Martin Ba
5
O @Snowman #include <cstdint>é obrigado a inserir os tipos std::e (infelizmente) opcionalmente também é permitido colocá-los no espaço de nomes global. #include <stdint.h>é exatamente o contrário. O mesmo se aplica a qualquer outro par de cabeçalhos C. Veja: stackoverflow.com/a/13643019/2757035 Eu gostaria que o Padrão exigisse que cada um apenas afetasse seu respectivo espaço de nome necessário - em vez de aparentemente se curvar às convenções precárias estabelecidas por algumas implementações - mas tudo bem, aqui estamos.
Underscore_d
38

Não, banir os tipos inteiros internos seria absurdo. Eles também não devem ser abusados.

Se você precisar de um número inteiro com exatamente N bits de largura, use (ou se precisar de uma versão). Pensar como um número inteiro de 32 bits e como um número inteiro de 64 bits está errado. Pode acontecer que seja assim em suas plataformas atuais, mas isso depende do comportamento definido pela implementação.std::intN_tstd::uintN_tunsignedintlong long

O uso de tipos inteiros de largura fixa também é útil para interoperar com outras tecnologias. Por exemplo, se algumas partes do seu aplicativo são escritas em Java e outras em C ++, você provavelmente desejará corresponder os tipos inteiros para obter resultados consistentes. (Ainda esteja ciente de que o estouro em Java possui semântica bem definida, enquanto o signedestouro em C ++ é um comportamento indefinido, portanto a consistência é um objetivo alto.) Eles também serão inestimáveis ​​ao trocar dados entre diferentes hosts de computação.

Se você não precisa exatamente de N bits, mas apenas de um tipo amplo o suficiente , considere usar (otimizado para espaço) ou (otimizado para velocidade). Novamente, ambas as famílias também têm contrapartes.std::int_leastN_tstd::int_fastN_tunsigned

Então, quando usar os tipos incorporados? Bem, como o padrão não especifica sua largura com precisão, use-os quando não se importar com a largura real do bit, mas com outras características.

A charé o menor número inteiro endereçável pelo hardware. O idioma realmente obriga a usá-lo para aliasing de memória arbitrária. Também é o único tipo viável para representar cadeias de caracteres (estreitas).

Um intnormalmente será o tipo mais rápido a máquina pode manipular. Ele será amplo o suficiente para poder ser carregado e armazenado com uma única instrução (sem ter que mascarar ou trocar bits) e estreito o suficiente para poder ser operado com as instruções de hardware mais eficientes. Portanto, inté uma escolha perfeita para transmitir dados e fazer aritmética quando o estouro não é uma preocupação. Por exemplo, o tipo subjacente padrão de enumerações é int. Não altere para um número inteiro de 32 bits apenas porque você pode. Além disso, se você tiver um valor que possa ser apenas –1, 0 e 1, uminté uma escolha perfeita, a menos que você armazene grandes matrizes delas. Nesse caso, convém usar um tipo de dados mais compacto ao custo de pagar um preço mais alto pelo acesso a elementos individuais. Um cache mais eficiente provavelmente pagará por isso. Muitas funções do sistema operacional também são definidas em termos de int. Seria tolice converter seus argumentos e resultados para frente e para trás. Tudo o que isso poderia fazer é introduzir erros de estouro.

longgeralmente será o tipo mais amplo que pode ser manuseado com instruções de máquina única. Isso é especialmente unsigned longatraente para lidar com dados brutos e todo tipo de manipulação de bits. Por exemplo, eu esperaria ver unsigned longna implementação de um vetor de bits. Se o código for escrito com cuidado, não importa a largura do tipo (porque o código se adaptará automaticamente). Nas plataformas em que a palavra-máquina nativa é de 32 bits, a matriz de backup do vetor de bits é uma matriz deunsignedInteiros de 32 bits é o mais desejável, porque seria tolo usar um tipo de 64 bits que precisa ser carregado por instruções caras apenas para mudar e mascarar os bits desnecessários novamente. Por outro lado, se o tamanho da palavra nativa da plataforma for de 64 bits, desejo uma matriz desse tipo, porque isso significa que operações como "encontrar o primeiro conjunto" podem ser executadas duas vezes mais rápido. Portanto, o “problema” do longtipo de dados que você está descrevendo, que seu tamanho varia de plataforma para plataforma, na verdade é um recurso que pode ser utilizado de maneira adequada. Isso só se torna um problema se você pensar nos tipos incorporados como tipos de uma certa largura de bit, o que eles simplesmente não são.

char, intE longsão tipos muito úteis como descrito acima. shorte long longnão são tão úteis porque sua semântica é muito menos clara.

5gon12eder
fonte
4
O OP destacou em particular a diferença no tamanho longentre o Windows e o Unix. Eu posso estar entendendo mal, mas sua descrição da diferença no tamanho de longser um "recurso" em vez de um "problema" faz sentido para mim ao comparar modelos de dados de 32 e 64 bits, mas não para essa comparação específica. No caso específico desta pergunta, isso é realmente um recurso? Ou é um recurso em outras situações (isto é, em geral) e inofensivo neste caso?
Dan Getz 5/05
3
@ 5gon12eder: O problema é que tipos como uint32_t foram criados com o objetivo de permitir que o comportamento do código seja independente do tamanho de "int", mas a falta de um tipo cujo significado seria "se comporta como um uint32_t funciona em um 32- sistema de bits "torna muito mais difícil escrever código cujo comportamento é independente do tamanho de" int "do que escrever código quase correto.
Supercat
3
Sim, eu sei ... foi daí que a maldição veio. Os autores originais apenas seguiram o caminho da resistência à concessão porque, quando escreveram o código, os SOs de 32 bits estavam a mais de uma década.
Steven Burnap 5/05
8
@ 5gon12eder Infelizmente, supercat está correto. Todos os tipos exatos de largura são "apenas typedefs" e as regras da promoção inteiro não tomar conhecimento deles, o que significa que a aritmética em uint32_tvalores serão realizados como assinado , intaritmética -width em uma plataforma onde inté mais amplo do que uint32_t. (Com ABIs de hoje, isso é esmagadoramente mais provável que seja um problema para uint16_t.)
Zwol
9
1º, obrigado por uma resposta detalhada. Mas: Oh querida. Seu longo parágrafo: " longnormalmente será o tipo mais amplo que pode ser manuseado com instruções de máquina única. ..." - e isso é exatamente errado . Veja o modelo de dados do Windows. IMHO, todo o exemplo a seguir se divide, porque em x64 o Windows ainda é de 32 bits.
Martin Ba
6

Outra resposta já foi elaborada sobre os tipos cstdint e variações menos conhecidas.

Eu gostaria de acrescentar a isso:

use nomes de tipos específicos de domínio

Ou seja, não declare seus parâmetros e variáveis uint32_t(certamente não long!), Mas nomes como channel_id_type, room_count_typeetc.

sobre bibliotecas

Bibliotecas de terceiros que usam longou outros enfeites podem ser irritantes, especialmente se usadas como referências ou indicadores para elas.

A melhor coisa é fazer invólucros.

Minha estratégia, em geral, é criar um conjunto de funções semelhantes a cast que serão usadas. Eles estão sobrecarregados para aceitar apenas os tipos que correspondem exatamente aos tipos correspondentes, juntamente com as variações de ponteiro etc. necessárias. Eles são definidos especificamente para o os / compiler / settings. Isso permite remover avisos e garantir que apenas as conversões "corretas" sejam usadas.

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

Em particular, com diferentes tipos primitivos produzindo 32 bits, sua escolha de como int32_té definida pode não corresponder à chamada da biblioteca (por exemplo, int vs long no Windows).

A função de conversão de documentos documenta o conflito, fornece uma verificação em tempo de compilação do resultado correspondente ao parâmetro da função e remove qualquer aviso ou erro se e somente se o tipo real corresponder ao tamanho real envolvido. Ou seja, ele está sobrecarregado e definido se eu passar (no Windows) um int*ou long*ae fornecer um erro em tempo de compilação.

Portanto, se a biblioteca for atualizada ou alguém alterar o que channel_id_typeé, isso continuará sendo verificado.

JDługosz
fonte
por que o voto negativo (sem comentários)?
JDługosz
Porque a maioria dos downvotes nesta rede aparecer sem comentários ...
Ruslan