size_t o int para dimensiones, índice, etc.

15

En C ++, size_t(o, más correctamente, T::size_typeque es "generalmente" size_t; es decir, un unsignedtipo) se usa como el valor de retorno para size(), el argumento a operator[], etc. (ver std::vector, et. Al.)

Por otro lado, los lenguajes .NET usan int(y, opcionalmente long) para el mismo propósito; de hecho, los lenguajes compatibles con CLS no son necesarios para admitir tipos sin firmar .

Dado que .NET es más nuevo que C ++, algo me dice que puede haber problemas al usar unsigned intincluso cosas que "no pueden" ser negativas, como un índice de matriz o una longitud. ¿Es el enfoque de C ++ "artefacto histórico" para la compatibilidad con versiones anteriores? ¿O hay compensaciones de diseño reales y significativas entre los dos enfoques?

¿Por qué importa esto? Bueno ... ¿qué debo usar para una nueva clase multidimensional en C ++; size_to 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
};
Ðаn
fuente
66
Vale la pena señalar: en varios lugares en .NET Framework, -1se devuelve desde funciones que devuelven un índice, para indicar "no encontrado" o "fuera de rango". También se devuelve de las Compare()funciones (implementación IComparable). Un int de 32 bits se considera ir a escribir para un número general, por lo que espero sean razones obvias.
Robert Harvey

Respuestas:

9

Dado que .NET es más nuevo que C ++, algo me dice que puede haber problemas al usar int sin firmar incluso para cosas que "no pueden" ser negativas como un índice de matriz o longitud.

Si. Para ciertos tipos de aplicaciones, como el procesamiento de imágenes o el procesamiento de matrices, a menudo es necesario acceder a elementos relativos a la posición actual:

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

En este tipo de aplicaciones, no puede realizar la verificación de rango con enteros sin signo sin pensar detenidamente:

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"); 
}

En su lugar, debe reorganizar su expresión de verificación de rango. Esa es la principal diferencia. Los programadores también deben recordar las reglas de conversión de enteros. En caso de duda, vuelva a leer http://en.cppreference.com/w/cpp/language/operator_arithmetic#Conversions

Muchas aplicaciones no necesitan usar índices de matriz muy grandes, pero sí necesitan realizar comprobaciones de rango. Además, muchos programadores no están entrenados para hacer esta gimnasia de reordenamiento de expresiones. Una sola oportunidad perdida abre la puerta a una hazaña.

C # está diseñado para aquellas aplicaciones que no necesitarán más de 2 ^ 31 elementos por matriz. Por ejemplo, una aplicación de hoja de cálculo no necesita lidiar con tantas filas, columnas o celdas. C # se ocupa del límite superior al tener aritmética comprobada opcional que se puede habilitar para un bloque de código con una palabra clave sin alterar las opciones del compilador. Por esta razón, C # favorece el uso de enteros con signo. Cuando estas decisiones se consideran por completo, tiene sentido.

C ++ es simplemente diferente y es más difícil obtener el código correcto.

Con respecto a la importancia práctica de permitir que la aritmética firmada elimine una posible violación del "principio del menor asombro", un caso en cuestión es OpenCV, que utiliza un entero de 32 bits con signo para el índice de elementos de la matriz, el tamaño de la matriz, el recuento de canales de píxeles, etc. Imagen El procesamiento es un ejemplo de dominio de programación que utiliza mucho el índice de matriz relativa. El desbordamiento de enteros sin signo (resultado negativo envuelto) complicará gravemente la implementación del algoritmo.

rwong
fuente
Esta es exactamente mi situación; Gracias por los ejemplos específicos. (Sí, lo sé, pero puede ser útil tener "autoridades superiores" para citar).
El
1
@Dan: si necesitas citar algo, esta publicación sería mejor.
rwong
1
@Dan: John Regehr está investigando activamente este tema en lenguajes de programación. Ver blog.regehr.org/archives/1401
rwong
Hay opiniones contrarias: gustedt.wordpress.com/2013/07/15/…
rwong
14

Esta respuesta realmente depende de quién va a usar su código y qué estándares quieren ver.

size_t es un tamaño entero con un propósito:

El tipo size_tes un tipo entero sin signo definido por la implementación que es lo suficientemente grande como para contener el tamaño en bytes de cualquier objeto. (C ++ 11 especificación 18.2.6)

Por lo tanto, cada vez que desee trabajar con el tamaño de los objetos en bytes, debe usar size_t. Ahora, en muchos casos, no está usando estas dimensiones / índices para contar bytes, pero la mayoría de los desarrolladores eligen usarsize_t para mantener la coherencia.

Tenga en cuenta que siempre debe usar size_tsi su clase está diseñada para tener el aspecto de una clase STL. Todas las clases de STL en la especificación usan size_t. Es válido para el compilador para typedef size_ta ser unsigned int, y también es válida para que pueda ser typedefed a unsigned long. Si usa into longdirectamente, eventualmente se encontrará con compiladores donde una persona que cree que su clase siguió el estilo de STL queda atrapada porque no siguió el estándar.

En cuanto al uso de tipos firmados, hay algunas ventajas:

  • Nombres más cortos: es muy fácil para las personas escribir int, pero es mucho más difícil saturar el código unsigned int.
  • Un entero para cada tamaño: solo hay un entero compatible con CLS de 32 bits, que es Int32. En C ++, hay dos ( int32_ty uint32_t). Esto puede simplificar la interoperabilidad API

La gran desventaja de los tipos firmados es la obvia: pierde la mitad de su dominio. Un número con signo no puede contar tan alto como un número sin signo. Cuando apareció C / C ++, esto fue muy importante. Uno debía ser capaz de abordar la capacidad total del procesador, y para hacerlo era necesario usar números sin firmar.

Para los tipos de aplicaciones dirigidas a .NET, no era tan necesaria la necesidad de un índice sin firmar de dominio completo. Muchos de los propósitos para tales números son simplemente inválidos en un lenguaje administrado (me viene a la mente la agrupación de memoria). Además, cuando salió .NET, las computadoras de 64 bits eran claramente el futuro. Estamos muy lejos de necesitar el rango completo de un entero de 64 bits, por lo que sacrificar un bit ya no es tan doloroso como antes. Si realmente necesita 4 mil millones de índices, simplemente cambie a usar enteros de 64 bits. En el peor de los casos, lo ejecuta en una máquina de 32 bits y es un poco lento.

Veo el comercio como uno de conveniencia. Si tiene suficiente potencia informática que no le importa desperdiciar un poco de su tipo de índice que nunca usará, entonces es conveniente simplemente escribir into longalejarse de él. Si crees que realmente querías ese último bit, entonces probablemente deberías haber prestado atención a la firma de tus números.

Cort Ammon - Restablece a Monica
fuente
Digamos que la implementación de size()was return bar_ * baz_;; ¿No crea eso ahora un problema potencial con el desbordamiento de enteros (envoltura) que no tendría si no lo usara size_t?
El
55
@Dan Puede construir casos como esos en los que sería importante tener entradas sin firmar, y en esos casos es mejor usar las funciones de lenguaje completo para resolverlo. Sin embargo, debo decir que sería una construcción interesante tener una clase donde se bar_ * baz_pueda desbordar un entero con signo pero no un entero sin signo. Limitándonos a C ++, vale la pena señalar que el desbordamiento sin signo está definido en la especificación, pero el desbordamiento con signo es un comportamiento indefinido, por lo que si el módulo aritmético de enteros sin signo es deseable, definitivamente úselos, ¡porque está realmente definido!
Cort Ammon - Restablece a Mónica el
1
@ Dan - si la size()desbordó el firmado multiplicación, estás en la tierra lenguaje UB. (y en el fwrapvmodo, ver a continuación :) Cuando entonces , con solo un poquito más, desbordó la multiplicación sin signo , en la tierra de error de código de usuario, devolvería un tamaño falso. Así que no creo que sin firmar compre mucho aquí.
Martin Ba
4

Creo que la respuesta de rwong anterior ya destaca de manera excelente los problemas.

Agregaré mi 002:

  • size_t, es decir, un tamaño que ...

    puede almacenar el tamaño máximo de un objeto teóricamente posible de cualquier tipo (incluida la matriz).

    ... solo se requiere para los índices de rango cuando sizeof(type)==1, es decir, si se trata de chartipos de byte ( ). (Pero, notamos, puede ser más pequeño que un tipo ptr :

  • Como tal, xxx::size_typepodría usarse en el 99.9% de los casos, incluso si fuera un tipo de tamaño con signo. (compararssize_t )
  • Algunos consideran que el hecho de que los std::vectoramigos elijan size_t, un tipo sin signo , para el tamaño y la indexación es un defecto de diseño. Estoy de acuerdo. (En serio, tómese 5 minutos y vea la charla relámpago CppCon 2016: Jon Kalb "sin firmar: Una directriz para un mejor código" .)
  • Cuando diseña una API de C ++ hoy, se encuentra en una situación difícil: use size_tpara ser coherente con la Biblioteca estándar, o use (con firma ) intptr_to ssize_tpara cálculos de indexación fáciles y menos propensos a errores.
  • No use int32 o int64: use intptr_tsi desea firmar y desea el tamaño de palabra de la máquina, o use ssize_t.

Para responder directamente a la pregunta, no se trata completamente de un "artefacto histórico", ya que el tema teórico de la necesidad de abordar más de la mitad del espacio de direcciones ("indexación" o) debe abordarse de alguna manera en un lenguaje de bajo nivel como C ++.

En retrospectiva, personalmente , creo que es un defecto de diseño que la Biblioteca estándar utiliza sin firmar en size_ttodo el lugar, incluso donde no representa un tamaño de memoria en bruto, sino una capacidad de datos escritos, como para las colecciones:

  • dadas las reglas de promoción de enteros de C ++ ->
  • los tipos sin signo simplemente no son buenos candidatos para tipos "semánticos" para algo como un tamaño semánticamente sin signo.

Repito el consejo de Jon aquí:

  • Seleccione tipos para las operaciones que admiten (no el rango de valores). (* 1)
  • No use tipos sin firmar en su API. Esto oculta errores sin beneficio al alza.
  • No use "unsigned" para cantidades. (* 2)

(* 1) es decir, unsigned == bitmask, nunca hagas cálculos matemáticos (aquí aparece la primera excepción; es posible que necesites un contador que se envuelva; este debe ser un tipo sin signo).

(* 2) cantidades que significan algo con lo que cuenta y / o hace matemáticas.

Martin Ba
fuente
¿Qué quiere decir con "memoria plana disponible completa"? Además, ¿seguro que no quiere ssize_t, definido como el colgante firmado en size_tlugar de intptr_t, que puede almacenar cualquier puntero (no miembro) y, por lo tanto, podría ser más grande?
Deduplicador
@Deduplicator - Bueno, supongo que es posible que haya confundido la size_tdefinición. Ver size_t vs. intptr y en.cppreference.com/w/cpp/types/size_t Aprendí algo nuevo hoy. :-) Creo que el resto de los argumentos se mantienen, veré si puedo arreglar los tipos utilizados.
Martin Ba
0

Solo agregaré eso por razones de rendimiento que normalmente uso size_t, para asegurar que los errores de cálculo causen un flujo inferior, lo que significa que ambas comprobaciones de rango (por debajo de cero y por encima del tamaño ()) se pueden reducir a uno:

usando int firmado:

int32_t i = GetRandomNumberFromRange(-1000, 1000);

if (i < 0)
{
    //error
}

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

usando unsigned int:

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
fuente
1
Usted realmente quiere explicar que uno más a fondo.
Martin Ba
Para que la respuesta sea más útil, quizás pueda describir cómo se ven los límites de la matriz entera o la comparación de desplazamiento (con y sin signo) en el código de máquina de varios proveedores de compiladores. Hay muchos compiladores de C ++ en línea y sitios de desensamblaje que pueden mostrar el código de máquina compilado correspondiente para el código de C ++ dado y los indicadores del compilador.
rwong
Traté de explicar esto un poco más.
asger