Uma proibição 'longa' faz sentido?

105

No mundo atual de C ++ (ou C) de plataforma cruzada, 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 inteiro "comum" (assinado), int será suficiente e possivelmente ainda poderá ser usado como o tipo inteiro padrão ao gravar o código do aplicativo C ++. Ele também - para fins práticos atuais - tem um tamanho consistente em todas as plataformas.

Em um caso de uso requer pelo menos 64 bits, hoje podemos usar long long , embora possivelmente usando um dos tipos de especificação de bits ou o tipo __int64 pode fazer mais sentido.

Isso deixa long no meio e estamos considerando proibir o uso de long do nosso código de aplicativo .

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

Links de fundo possivelmente interessantes:

por Martin Ba 05.05.2016 / 22:14
fonte

4 respostas

17

A única razão pela qual eu usaria o long hoje é ao chamar ou implementar uma interface externa que o utiliza.

Como você diz em sua postagem, curta e int tem características razoavelmente estáveis em todas as principais plataformas de desktop / servidor / dispositivos móveis hoje e não vejo nenhuma razão para isso mudar no futuro previsível. Então, vejo poucos motivos para evitá-los em geral.

long , por outro lado, é 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 que a maior unidade de dados que poderia ser mantida em um registro normal e trabalhada com uma única instrução.

Grandes quantidades de código foram escritas 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. Plataformas Unix-like foram para LP64 que preservaram as características 2 e 3 ao custo da característica 1. Win64 foi para 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ê quiser um tipo que tenha exatamente 32 bits de tamanho, use int32_t .

Se você quiser um tipo que tenha o mesmo tamanho de um ponteiro, use intptr_t (ou melhor uintptr_t ).

Se você quer um tipo que é o maior item que pode ser trabalhado em um único registrador / instrução, infelizmente eu não acho que o padrão forneça um. size_t deve estar certo na maioria das plataformas comuns, mas não estaria em x32 .

P.S.

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

P.P.S

Um motivo para usar o unsigned long (mas não o% normallong) é porque ele tem o comportamento do módulo. Infelizmente, devido às regras de promoção erradas de C, tipos não assinados menores que int não possuem comportamento de módulo.

Em todas as principais plataformas atuais, uint32_t é do mesmo tamanho ou maior que int e, portanto, possui comportamento de módulo. No entanto, houve historicamente e teoricamente poderia haver nas plataformas futuras onde int é de 64 bits e, portanto, uint32_t não tem comportamento de módulo.

Pessoalmente, eu diria que é melhor entrar no habbit 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.

    
por 06.05.2016 / 03:08
fonte
201

Como você mencionou em sua pergunta, o software moderno tem tudo a ver com a interoperabilidade entre plataformas e sistemas na Internet. Os padrões C e C ++ fornecem intervalos para tamanhos de tipos inteiros, não tamanhos específicos (em contraste com idiomas como Java e C #).

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

Insira <cstdint> que fornece exatamente isso e é um cabeçalho padrão que todas as plataformas de biblioteca padrão e de compilador são necessárias fornecer. Nota: este cabeçalho só era necessário a partir do C ++ 11, mas muitas implementações de bibliotecas mais antigas o forneciam mesmo assim.

Deseja um inteiro sem sinal de 64 bits? Use uint64_t . Assinado 32 bits inteiro? 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, uma largura de bit específica é necessária, 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_t será um tipo inteiro de no mínimo XX bits. Ele usará o menor tipo que fornece bits XX, mas o tipo pode ser maior que o número especificado de bits. Na prática, estes são tipicamente os mesmos tipos descritos acima que fornecem o número exato de bits.

Também existem variantes rápidas : int_fastXX_t tem 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 um tipo de tamanho de registro da CPU. Por exemplo, o cabeçalho do Visual C ++ 2015 especifica que int_fast16_t é um inteiro de 32 bits porque a aritmética de 32 bits é, no geral, mais rápida em x86 que a aritmética de 16 bits.

Isso tudo é importante porque você deve ser capaz de usar tipos que podem conter os resultados de cálculos executados pelo seu programa, independentemente da plataforma. Se um programa produz resultados corretos em uma plataforma, mas resultados incorretos em outra devido a diferenças no estouro de inteiro, isso é ruim. Ao usar os tipos inteiros padrão, você garante que os resultados em diferentes plataformas serão os mesmos com relação ao tamanho dos inteiros usados (claro que pode haver outras diferenças entre as plataformas além da largura inteira). / p>

Então, sim, long deve ser banido do código C ++ moderno. Então deve int , short e long long .

    
por 05.05.2016 / 22:24
fonte
37

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

Se você precisar de um número inteiro exatamente N bits, use std::intN_t (ou std::uintN_t se precisar de uma versão unsigned ). Pensar em int como um inteiro de 32 bits e long long como um inteiro de 64 bits está errado. Pode acontecer de ser assim nas suas plataformas atuais, mas isso depende de um comportamento definido pela implementação.

O uso de tipos inteiros de largura fixa também é útil para interoperar com outras tecnologias. Por exemplo, se algumas partes de sua aplicação forem escritas em Java e outras em C ++, você provavelmente desejará combinar os tipos inteiros para obter resultados consistentes. (Ainda esteja ciente de que o estouro em Java tem semântica bem definida, enquanto signed estouro 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 grande o suficiente , considere usar std::int_leastN_t (otimizado para espaço) ou std::int_fastN_t (otimizado para velocidade). Novamente, ambas as famílias têm unsigned de contrapartes também.

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

Um char é o menor inteiro que é endereçável pelo hardware. A linguagem na verdade força você a usá-la para aliasing de memória arbitrária. Também é o único tipo viável para representar cadeias de caracteres (estreitas).

Um int geralmente será o tipo mais rápido que a máquina pode manipular. Ela será larga o suficiente para que possa ser carregada e armazenada com uma única instrução (sem ter que mascarar ou deslocar bits) e estreitar o suficiente para que possa ser operada com as instruções de hardware (mais) eficientes. Portanto, int é uma escolha perfeita para passar 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 mude para um inteiro de 32 bits só porque você pode. Além disso, se você tiver um valor que só pode ser –1, 0 e 1, um int é uma escolha perfeita, a menos que você esteja armazenando matrizes enormes deles, caso em que você pode querer usar dados mais compactos digite ao custo de ter que pagar um preço mais alto para acessar elementos individuais. Um armazenamento em cache mais eficiente provavelmente compensará isso. Muitas funções do sistema operacional também são definidas em termos de int . Seria tolice converter seus argumentos e resultados de um lado para outro. Tudo isso poderia fazer é introduzir erros de estouro.

long geralmente é o tipo mais amplo que pode ser manipulado com instruções de máquina única. Isso torna especialmente unsigned long muito atraente para lidar com dados brutos e todos os tipos de manipulação de bits. Por exemplo, eu esperaria ver unsigned long na implementação de um vetor de bits. Se o código for escrito com cuidado, não importa o tamanho do tipo (porque o código se adaptará automaticamente). Em plataformas onde a palavra-máquina nativa é de 32 bits, ter a matriz de apoio do vetor de bits como uma matriz de unsigned 32 bits inteiros é mais desejável porque seria bobagem usar um tipo de 64 bits que precisa ser carregado através de instruções caras apenas para mudar e mascarar as partes desnecessárias de novo de qualquer maneira. Por outro lado, se o tamanho da palavra nativa da plataforma for de 64 bits, eu quero uma matriz desse tipo, porque significa que operações como “encontrar primeiro conjunto” podem ser executadas até duas vezes mais rápido. Portanto, o "problema" do tipo de dados long que você está descrevendo, de que seu tamanho varia de plataforma para plataforma, na verdade, é um recurso que pode ser bem utilizado. Isso só se torna um problema se você pensar nos tipos internos como tipos de uma certa largura de bit, o que eles simplesmente não são.

char , int e long são tipos muito úteis, conforme descrito acima. short e long long não são tão úteis porque suas semânticas são muito menos claras.

    
por 05.05.2016 / 23:44
fonte
6

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

Gostaria de acrescentar isso:

usa nomes de tipos específicos de domínio

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

sobre bibliotecas

As bibliotecas de terceiros que usam long ou outras coisas podem ser irritantes, especialmente se usadas como referências ou apontadores para elas.

A coisa melhor é criar wrappers.

O que a minha estratégia é, em geral, é fazer um conjunto de funções de conversão 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. que você precisar. Eles são definidos especificamente para os / compilador / configurações. Isso permite remover avisos e, ainda assim, garantir que apenas as conversões "certas" sejam usadas.

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

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

A função cast-like documenta o conflito, fornece 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 corresponde ao tamanho real envolvido. Ou seja, ele está sobrecarregado e definido se eu passar (no Windows) um int* ou um long* e causar um erro em tempo de compilação.

Portanto, se a biblioteca for atualizada ou alguém alterar o que é channel_id_type , isso continua a ser verificado.

    
por 07.05.2016 / 03:30
fonte