size_t ou int para dimensões, índice etc.

15

Em C ++, size_t(ou, mais corretamente, T::size_typeque é "geralmente" size_t; isto é, um unsignedtipo) é usado como o valor de retorno para size(), o argumento para operator[]etc. (consulte std::vectoret al.)

Por outro lado, as linguagens .NET usam int(e, opcionalmente long) para a mesma finalidade; de fato, os idiomas compatíveis com CLS não são necessários para suportar tipos não assinados .

Dado que o .NET é mais recente que o C ++, algo me diz que pode haver problemas usando unsigned intaté mesmo para coisas que "não podem" ser negativas como um índice ou comprimento de matriz. A abordagem do C ++ é "artefato histórico" para compatibilidade com versões anteriores? Ou existem trocas reais e significativas de design entre as duas abordagens?

Por que isso importa? Bem ... o que devo usar para uma nova classe multidimensional em C ++; size_tou int?

struct Foo final // e.g., image, matrix, etc.
{
    typedef int32_t /* or int64_t*/ dimension_type; // *OR* always "size_t" ?
    typedef size_t size_type; // c.f., std::vector<>

    dimension_type bar_; // maybe rows, or x
    dimension_type baz_; // e.g., columns, or y

    size_type size() const { ... } // STL-like interface
};
Como
fonte
6
Vale ressaltar: em vários locais do .NET Framework, -1é retornado de funções que retornam um índice, para indicar "não encontrado" ou "fora do intervalo". Também é retornado de Compare()funções (implementando IComparable). Um int de 32 bits é considerado o tipo digitar para um número geral, pelo que espero que sejam razões óbvias.
Robert Harvey

Respostas:

9

Dado que o .NET é mais recente que o C ++, algo me diz que pode haver problemas ao usar unsigned int, mesmo para coisas que "não podem" ser negativas, como comprimento ou índice de matriz.

Sim. Para certos tipos de aplicativos, como processamento de imagem ou matriz, geralmente é necessário acessar elementos relativos à posição atual:

sum = data[k - 2] + data[k - 1] + data[k] + data[k + 1] + ...

Nesses tipos de aplicativos, você não pode executar a verificação de intervalo com números inteiros sem sinal sem pensar com cuidado:

if (k - 2 < 0) {
    throw std::out_of_range("will never be thrown"); 
}

if (k < 2) {
    throw std::out_of_range("will be thrown"); 
}

if (k < 2uL) {
    throw std::out_of_range("will be thrown, without signedness ambiguity"); 
}

Em vez disso, você deve reorganizar sua expressão de verificação de intervalo. Essa é a principal diferença. Os programadores também devem se lembrar das regras de conversão de números inteiros. Em caso de dúvida, leia novamente http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

Muitos aplicativos não precisam usar índices de matriz muito grandes, mas precisam executar verificações de intervalo. Além disso, muitos programadores não são treinados para fazer essa expressão reorganizar a ginástica. Uma única oportunidade perdida abre a porta para uma exploração.

O C # foi projetado para os aplicativos que não precisarão de mais de 2 ^ 31 elementos por matriz. Por exemplo, um aplicativo de planilha não precisa lidar com tantas linhas, colunas ou células. O C # lida com o limite superior por ter aritmética marcada opcional que pode ser ativada para um bloco de código com uma palavra-chave sem mexer nas opções do compilador. Por esse motivo, o C # favorece o uso de número inteiro assinado. Quando essas decisões são consideradas completamente, faz sentido.

C ++ é simplesmente diferente e é mais difícil obter o código correto.

Com relação à importância prática de permitir que a aritmética assinada remova uma possível violação do "princípio do mínimo espanto", um exemplo é o OpenCV, que usa um inteiro assinado de 32 bits para o índice de elementos da matriz, tamanho da matriz, contagem de canais de pixels, etc. processing é um exemplo de domínio de programação que usa fortemente o índice de matriz relativa. O fluxo insuficiente de números inteiros não assinados (resultado negativo contornado) complicará severamente a implementação do algoritmo.

rwong
fonte
Esta é exatamente a minha situação; obrigado pelos exemplos específicos. (Sim, eu sei disso, mas pode ser útil ter "autoridades superiores" para citar.) #
181
1
@ Dan: se você precisar citar algo, este post seria melhor.
Rwong
1
@ Dan: John Regehr está pesquisando ativamente esse problema nas linguagens de programação. Veja blog.regehr.org/archives/1401
rwong
Existem opiniões contrárias: gustedt.wordpress.com/2013/07/15/…
rwong 15/12/16
14

Essa resposta realmente depende de quem usará seu código e de quais padrões eles desejam ver.

size_t é um tamanho inteiro com uma finalidade:

O tipo size_té um tipo de número inteiro não assinado definido pela implementação que é grande o suficiente para conter o tamanho em bytes de qualquer objeto. (Especificação C ++ 11 18.2.6)

Portanto, sempre que desejar trabalhar com o tamanho de objetos em bytes, você deve usar size_t. Agora, em muitos casos, você não está usando essas dimensões / índices para contar bytes, mas a maioria dos desenvolvedores prefere usá-los para fins size_tde consistência.

Observe que você sempre deve usar size_tse sua classe tem a aparência de uma classe STL. Todas as classes STL na especificação são usadas size_t. É válido para o compilador para typedef size_tpara ser unsigned int, e também é válido para que possa ser typedefed para unsigned long. Se você usar intou longdiretamente, você encontrará os compiladores onde uma pessoa que acha que sua classe seguiu o estilo da STL fica presa porque você não seguiu o padrão.

Quanto ao uso de tipos assinados, há algumas vantagens:

  • Nomes mais curtos - é realmente fácil para as pessoas digitarem int, mas é muito mais difícil confundir o código unsigned int.
  • Um número inteiro para cada tamanho - Há apenas um número inteiro compatível com CLS de 32 bits, que é Int32. Em C ++, há dois ( int32_te uint32_t). Isso pode simplificar a interoperabilidade da API

A grande desvantagem dos tipos assinados é a óbvia: você perde metade do seu domínio. Um número assinado não pode contar tão alto quanto um número não assinado. Quando o C / C ++ apareceu, isso foi muito importante. Era necessário ser capaz de abordar todos os recursos do processador e, para isso, era necessário usar números não assinados.

Para os tipos de aplicativos direcionados ao .NET, não havia uma necessidade tão forte de um índice não assinado de domínio completo. Muitos dos propósitos para esses números são simplesmente inválidos em um idioma gerenciado (o pool de memória vem à mente). Além disso, quando o .NET foi lançado, os computadores de 64 bits eram claramente o futuro. Estamos muito longe de precisar de todo o intervalo de um número inteiro de 64 bits, portanto, sacrificar um bit não é tão doloroso quanto antes. Se você realmente precisa de 4 bilhões de índices, basta mudar para o uso de números inteiros de 64 bits. Na pior das hipóteses, você o executa em uma máquina de 32 bits e é um pouco lento.

Eu vejo o comércio como uma conveniência. Se você possui poder computacional suficiente para não se importar em desperdiçar um pouco do seu tipo de índice que nunca usará, é conveniente apenas digitar intou se longafastar dele. Se você realmente quisesse esse último pedaço, provavelmente deveria ter prestado atenção à assinatura dos seus números.

Cort Ammon - Restabelecer Monica
fonte
digamos que a implementação do size()was return bar_ * baz_;; isso agora não cria um problema em potencial com excesso de número inteiro (wrap-around) que eu não teria se não usasse size_t?
Ðаn
5
@ Dan Você pode criar casos como aquele em que ter entradas não assinadas importaria e, nesses casos, é melhor usar os recursos completos do idioma para resolvê-lo. No entanto, devo dizer que seria uma construção interessante ter uma classe em que bar_ * baz_possa estourar um número inteiro assinado, mas não um número inteiro não assinado. Limitando-nos a C ++, vale a pena notar que o estouro não assinado é definido na especificação, mas o estouro assinado é um comportamento indefinido; portanto, se a aritmética do módulo de números inteiros não assinados é desejável, use-os definitivamente, porque é realmente definido!
Cort Ammon - Restabelece Monica
1
@ Dan - se o size()transbordou a multiplicação assinada , você está no idioma UB land. (e, no fwrapvmodo, veja a seguir :) Quando então , com apenas um pouquinho mais, ela transbordou a multiplicação não assinada , você em terra de bug de código de usuário - retornaria um tamanho falso. Portanto, acho que não assinado compra muito aqui.
Martin Ba
4

Penso que a resposta de rwong acima já destaca excelentemente as questões.

Vou adicionar meu 002:

  • size_t, ou seja, um tamanho que ...

    pode armazenar o tamanho máximo de um objeto teoricamente possível de qualquer tipo (incluindo matriz).

    ... é necessário apenas para índices de intervalo quando sizeof(type)==1, ou seja, se você estiver lidando com chartipos de byte ( ). (Mas, observamos, ele pode ser menor que um tipo ptr :

  • Como tal, xxx::size_typepoderia ser usado em 99,9% dos casos, mesmo que fosse um tipo de tamanho assinado. (comparar ssize_t)
  • O fato de os std::vectoramigos escolherem size_t, um tipo não assinado , para o tamanho e a indexação é considerado por alguns como uma falha de design. Eu concordo. (Sério, tire 5 minutos e assista à palestra relâmpago CppCon 2016: Jon Kalb "sem assinatura: Uma Diretriz para um Código Melhor" .)
  • Quando você cria uma API C ++ hoje, está em uma situação difícil: use size_tpara ser consistente com a Biblioteca Padrão, ou use (um assinado ) intptr_tou ssize_tpara cálculos de indexação fáceis e menos propensos a erros.
  • Não use int32 ou int64 - use intptr_tse você deseja assinar e deseja o tamanho da palavra da máquina ou use ssize_t.

Para responder diretamente à pergunta, não é inteiramente um "artefato histórico", pois a questão teórica de precisar abordar mais da metade do espaço de endereço ("indexação" ou) deve ser, aehm, abordada de alguma forma em uma linguagem de baixo nível como C ++.

Em retrospecto, eu pessoalmente acho que é uma falha de design que a Biblioteca Padrão usa sem assinatura em size_ttodo o lugar, mesmo onde não representa um tamanho de memória bruto, mas uma capacidade de dados digitados, como nas coleções:

  • dadas as regras de promoção inteira de C ++ ->
  • tipos não assinados simplesmente não são bons candidatos para tipos "semânticos" para algo como um tamanho que é semanticamente não assinado.

Vou repetir o conselho de Jon aqui:

  • Selecione os tipos para as operações que eles suportam (não o intervalo de valores). (* 1)
  • Não use tipos não assinados na sua API. Isso oculta erros sem nenhum benefício positivo.
  • Não use "não assinado" para quantidades. (* 2)

(* 1) isto é, não assinado == máscara de bits, nunca faça contas nele (aqui ocorre a primeira exceção - você pode precisar de um contador que o envolva - este deve ser um tipo não assinado).

(* 2) quantidades que significam algo em que você conta e / ou faz contas.

Martin Ba
fonte
O que você quer dizer com "memória plana completa e disponível"? Além disso, com certeza, você não deseja ssize_t, definido como o pendente assinado em size_tvez de intptr_t, que pode armazenar qualquer ponteiro (não membro) e, portanto, pode ser maior?
Deduplicator
@ Reduplicator - Bem, eu acho que posso ter começado a size_tdefinição um pouco confusa. Consulte size_t vs. intptr e en.cppreference.com/w/cpp/types/size_t Aprendeu algo novo hoje. :-) Acho que o restante dos argumentos permanece, vou ver se consigo consertar os tipos usados.
Martin Ba
0

Vou acrescentar que, por razões de desempenho, normalmente uso size_t, para garantir que erros de cálculo causem um subfluxo, o que significa que as duas verificações de intervalo (abaixo de zero e acima de size ()) podem ser reduzidas a uma:

usando int assinado:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

if (i > size())
{
    //error
}

usando int não assinado:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

/// This will underflow any number below zero, so that it becomes a very big *positive* number instead.
uint32_t asUnsigned = static_cast<uint32_t>(i);

/// We now don't need to check for below zero, since an unsigned integer can only be positive.
if (asUnsigned > size())
{
    //error
}
Asger
fonte
1
Você realmente quer explicar isso mais detalhadamente.
Martin Ba
Para tornar a resposta mais útil, talvez você possa descrever como a matriz inteira limita ou compara a comparação de offset (assinada e não assinada) no código de máquina de vários fornecedores de compiladores. Existem muitos sites online de compilação e desmontagem de C ++ que podem mostrar o código de máquina compilado correspondente para os códigos e sinalizadores de compilador C ++ fornecidos.
rwong
Eu tentei explicar isso um pouco mais.
Asger