Until today, in the few cases where I needed something like this, it had been in simple programs which only I used, and where I didn't care about security, so I used the simple atoi()
.
However, today I needed to do that for a some more serious program, and I researched about the many different forms that are out there to go from a string to a number: atoi vs atol vs strtol vs strtoul vs sscanf
None of those pleased me. strtol()
(and its family) is the safest standard one and also a very fast one, but it is incredibly difficult to use, so I decided to write a safe and simple interface to it. strtoi()
(libbsd) is easier to use than strtol()
, but still a bit complicated. I decided to use fixed-width integers, as I do in all my code. I also did an interface for strtof()
and company.
Requisites:
- libbsd (The following code can be written in terms of
strtol()
instead ofstrtoi()
if libbsd is not available, but it is more complex, and has a problem witherrno
whichstrtoi()
hasn't). - GNU C11 (not actually needed, but I use it for added safety/optimizations).
Signed integers:
strtoi_s.h
:
#pragma once /* libalx/base/stdlib/strto/strtoi_s.h */
#include <errno.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
__attribute__((nonnull, warn_unused_result))
inline
int strtoi8_s (int8_t *restrict num, const char *restrict str,
int base);
__attribute__((nonnull, warn_unused_result))
inline
int strtoi16_s (int16_t *restrict num, const char *restrict str,
int base);
__attribute__((nonnull, warn_unused_result))
inline
int strtoi32_s (int32_t *restrict num, const char *restrict str,
int base);
__attribute__((nonnull, warn_unused_result))
inline
int strtoi64_s (int64_t *restrict num, const char *restrict str,
int base);
inline
int strtoi8_s (int8_t *restrict num, const char *restrict str,
int base)
{
int rstatus;
*num = strtoi(str, NULL, base, INT8_MIN, INT8_MAX, &rstatus);
switch (rstatus) {
case 0:
return 0;
case ENOTSUP:
return rstatus;
case ECANCELED:
case EINVAL:
case ERANGE:
default:
return -rstatus;
}
}
inline
int strtoi16_s (int16_t *restrict num, const char *restrict str,
int base)
{
int rstatus;
*num = strtoi(str, NULL, base, INT16_MIN, INT16_MAX, &rstatus);
switch (rstatus) {
case 0:
return 0;
case ENOTSUP:
return rstatus;
case ECANCELED:
case EINVAL:
case ERANGE:
default:
return -rstatus;
}
}
inline
int strtoi32_s (int32_t *restrict num, const char *restrict str,
int base)
{
int rstatus;
*num = strtoi(str, NULL, base, INT32_MIN, INT32_MAX, &rstatus);
switch (rstatus) {
case 0:
return 0;
case ENOTSUP:
return rstatus;
case ECANCELED:
case EINVAL:
case ERANGE:
default:
return -rstatus;
}
}
inline
int strtoi64_s (int64_t *restrict num, const char *restrict str,
int base)
{
int rstatus;
*num = strtoi(str, NULL, base, INT64_MIN, INT64_MAX, &rstatus);
switch (rstatus) {
case 0:
return 0;
case ENOTSUP:
return rstatus;
case ECANCELED:
case EINVAL:
case ERANGE:
default:
return -rstatus;
}
}
Unsigned integers:
It's mostly the same as the previous one, so I'll post only a function
strtou_s.h
:
inline
int strtou8_s (uint8_t *restrict num, const char *restrict str,
int base)
{
int rstatus;
*num = strtou(str, NULL, base, 0, UINT8_MAX, &rstatus);
switch (rstatus) {
case 0:
return 0;
case ENOTSUP:
return rstatus;
case ECANCELED:
case EINVAL:
case ERANGE:
default:
return -rstatus;
}
}
Floating-point:
strtof_s.h
:
#pragma once /* libalx/base/stdlib/strto/strtof_s.h */
#include <errno.h>
#include <stdlib.h>
/*
* `errno` needs to be cleared before calling these functions. If not, false
* negatives could happen (the function succeds, but it reports error).
*/
__attribute__((nonnull, warn_unused_result))
inline
int strtod_s (double *restrict num, const char *restrict str);
__attribute__((nonnull, warn_unused_result))
inline
int strtof_s (float *restrict num, const char *restrict str);
__attribute__((nonnull, warn_unused_result))
inline
int strtold_s (long double *restrict num, const char *restrict str);
inline
int strtod_s (double *restrict num, const char *restrict str)
{
char *endptr;
*num = strtod(str, &endptr);
if (*endptr != '\0')
return ENOTSUP;
if (errno == ERANGE)
return ERANGE;
if (str == endptr)
return -ECANCELED;
return 0;
}
inline
int strtof_s (float *restrict num, const char *restrict str)
{
char *endptr;
*num = strtof(str, &endptr);
if (*endptr != '\0')
return ENOTSUP;
if (errno == ERANGE)
return ERANGE;
if (str == endptr)
return -ECANCELED;
return 0;
}
inline
int strtold_s (long double *restrict num, const char *restrict str)
{
char *endptr;
*num = strtold(str, &endptr);
if (*endptr != '\0')
return ENOTSUP;
if (errno == ERANGE)
return ERANGE;
if (str == endptr)
return -ECANCELED;
return 0;
}
The functions take two pointers: the first one to the variable where the number has to be stored; and the second one to the string to be read. The integer functions also require the base, which follows the same rules as in strtol()
.
The return value is simply an error code:
0
is OK as always,
> 0
means a valid conversion with some error (partial conversion, 0 or inf in floating-point, ...).
< 0
means an invalid conversion, or no conversion at all.
Example:
char buf[BUFSIZ];
int64_t num;
if (!fgets(buf, ARRAY_SIZE(buf), stdin))
goto err;
if (strtoi64_s(&num, buf, 0))
goto err;
/* num is safe to be used now*/
Do you think the interface can be improved in any way?
strtof()
and family is harder/more complex to use thanstrtol()
. If code can well handlestrofd()
with all its issues,strtol()
is relatively easy. \$\endgroup\$strtof()
, not extremely more complex. And it has the added problem that I already have withstrtof_s()
: iferrno
isn't reset, the return value is wrong, but it's also a minor problem. Not something very problematic, of course, but I prefer to avoid it even if it means using less portable code. \$\endgroup\$