lunes, 6 de octubre de 2008

¿Qué pasa con atoll()?

Como programador de sistemas UNIX a veces me encuentro con algunas cosas curiosas con las APIs. Hoy, sin ir mas lejos, me he vuelto loco intentando hacer funcionar atoll().

En una parte de un viejo programa, se manejaba un identificador de menos de 32 bits. Ese identificador, que provenía de una máquina Cisco en formato cadena, estaba declarado como 'int' y se transformaba mediante atoi().

Por necesidades ligadas al crecimiento de la empresa, ahora el identificador debía poder almacenar hasta 56 bits.

En pocos segundos tenía el programa funcionando con un identificador declarado como 'long', pero ooops! no funcionaba. De hecho, se comportaba como un 'int'.

El sistema, un CentOS 5.2, el compilador, gcc 4.1.2, sin problemas conocidos. Así que me dediqué a hacer algunas pruebas:

$ cat test1.c

#include<stdio.h>
int main()
{
/* 2^31 = 2147483648 */

char *str="2147483648";
long long i;

i = atoll(str);

printf("%ld\n", i);
}


$ gcc test1.c
$ ./a.out
-2147483648

Overflow, exactamente igual que un 'int'.
Me dirijo al man de atoll() y me encuentro con esto:
"The atol() and atoll() functions behave the same as atoi()"

y efectivamente. La culpa es de la función de conversión, pues con una función my_atoll() personalizada, no existe tal problema:


$ cat test2.c

#includes<stdio.h>
long long my_atoll(char *str)
{
long long res;

res = 0;
for (; *str; str++)
res = 10*res + (*str - '0');

return res;
}


int main()
{
/* 2^31 = 2147483648 */

char *str="2147483648";
long long i;

i =my_atoll(str);


printf("%lld\n", i);
}


$ gcc test2.c
$ ./a.out
2147483648


Sorprendente ...

6 comentarios:

Anónimo dijo...

bueno para empezar _no_ se tiene que usar ninguna función
de la familia de atoi/l/ll, ya que estas funciones carecen
de un valor para chequear si ocurre algún error, por lo que
es _muy_ recomendable usar strtol/ll, te dejo un ejemplo de
uso usando justamente tu caso.

#include stdio.h
#include stdlib.h
#include limits.h
#include errno.h

#define ERANGE 34 /* Numerical result out of range */

int main(void) {

char *str = "2147483648";
long long int i; /* __int64_t */

i = strtoll(str, (char** NULL), 10);

/* chequeamos posibles errores, se puede chequear mas aun */
if ((errno == ERANGE) || (i == LONG_MAX || i == LONG_MIN)) {
perror("strtol");
exit(-1);
}

printf("%lld\n", i);

return 0;
}


aun se podría chequear unos pocos posibles errores si se desea,
pero para el ejemplo esta bien así ;-)

cya


PD: buen blog, seguíí así.

Anónimo dijo...

ups, se me escapo, la llamada a strtoll
strtoll(str, (char** NULL), 10);
hay que cerrar el paréntesis antes de NULL, quedaría así
strtoll(str, (char**) NULL, 10);

de paso aclaro que los #include's están así para que
no sean interpretados como tags html.

Daniel Lerch dijo...

Cierto, pero ¿sabes por qué atoll() no funciona como debería?

Fíjate en que en el estándar C99 dice:

"atof, atoi y atol se reemplazaron por strtod y strtol pero se conservaron por su extendida utilización en código existente. Son menos fiables pero pueden resultar más rápidas si se sabe que el argumento se encuentra en un intervalo válido".

En resumen, que debería funcionar, dado que se encuentra en un intervalo válido.

Por otra parte, gracias por el apunte, strtoll() funciona perfectamente.


Un saludo.

Anónimo dijo...

>Cierto, pero ¿sabes por qué atoll()
>no funciona como debería?

mmm, pues es difícil decirte ya que esta función si no puede
representar el numero su comportamiento no esta definido,
por estas razones creo que es un error usar cualquier
función de la familia atoi/f/l/ll

te dejo un extracto del estándar donde dice lo que dije:

7.20.1 Numeric conversion functions
The functions atof, atoi, atol, and atoll need not affect the value
of the integer expression errno on an error.
If the value of the result cannot be represented,
the behavior is undefined.

en el mismo estándar indican las equivalencias con strtol/ll

Except for the behavior on error, they are equivalent to
atoi: (int)strtol(nptr, (char **)NULL, 10)
atol: strtol(nptr, (char **)NULL, 10)
atoll: strtoll(nptr, (char **)NULL, 10)


aparte cabe destacar que strtol/ll ademas puede ignorar
tanto espacios en blanco como caracteres no numéricos,
ej. puedo tener 123abc y me devolvería 123 y el resto
abc apuntado por endptr, bueno, en fin la idea es
exponer la poca _seguridad_ que nos ofrecería hacer
las cosas con atoi/f/l/ll, y ya que la categoría
es Programación segura, me pareció correcto aclarar.

el estándar consultado fue:
WG14/N1124 Committee Draft -- May 6, 2005 ISO/IEC 9899:TC2

Daniel Lerch dijo...

He descargado las fuentes de libc6 y he buscado la función atoll(). Esto es lo que tenemos en stdlib/atoll.c

#undef atoll

/* Convert a string to a long long int. */
long long int
atoll (const char *nptr)
{
return strtoll (nptr, (char **) NULL, 10);
}


Así que me he limitado a añadir un include de stdlib.h y funciona!


# cat test3.c

#include stdio.h
#include stdlib.h

int main()
{
/* 2^31 = 2147483648 */
char *str="2147483648";
long long int i;

i = atoll(str);
printf("%ld\n", i);

}

$ gcc test3.c
$ ./a.out
2147483648


Al parecer en algun lugar se define atoll() de forma incorrecta y al incluir stdlib.h se elimina (undef) y se crea de nuevo como envoltorio de strtoll().

Vamos, que para que funcione correctamente hay que incluir stdlib.h.

Nos vamos acercando al final de la investigación ... Aun queda por saber donde esta definida la función atoll() que coge por defecto cuando no se incluye stdlib.h y ver por que no funciona.

Saludos.

Anónimo dijo...

Algunas cosas (como la comprobación de lo procesado por strtoll) están mal. Mirad http://linux.die.net/man/3/strtoll