6
\$\begingroup\$

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 of strtoi() if libbsd is not available, but it is more complex, and has a problem with errno which strtoi() 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?

\$\endgroup\$
2
  • \$\begingroup\$ "(strtol) is more complex, and has a problem with errno" and "I also did an interface for strtof() and company." is curious. strtof() and family is harder/more complex to use than strtol(). If code can well handle strofd() with all its issues, strtol() is relatively easy. \$\endgroup\$ Sep 2, 2019 at 23:42
  • \$\begingroup\$ @chux By more complex I meant that you have to handle more than one variable, as with strtof(), not extremely more complex. And it has the added problem that I already have with strtof_s(): if errno 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\$ Sep 3, 2019 at 10:57

2 Answers 2

2
\$\begingroup\$

Portability

To be clear, strtoi() and strtou() that OP's code relies on is not in the standard C library. OP's code is limited to the requisites.

strtol() may be more complex, yet it is portable throughout all compliant C implementations.

Bug - failure to clear errno

When strtod() succeeds, it does not change errno, so the tests on errno are testing the prior state. Add errno = 0; before calling strtod(), strtof(), strtold().

    errno = 0; // add
    *num    = strtod(str, &endptr);
    if (*endptr != '\0') return  ENOTSUP;
    if (errno == ERANGE) return  ERANGE;
    ...

Questionable error

With floating point conversions, input like "z", the functions indicate ENOTSUP. I'd expect ECANCELED

Rather than

    if (*endptr != '\0') return  ENOTSUP;
    if (errno == ERANGE) return  ERANGE;
    if (str == endptr)   return  -ECANCELED;

Consider

    if (str == endptr)   return  -ECANCELED;
    if (*endptr != '\0') return  ENOTSUP;
    if (errno == ERANGE) return  ERANGE;

Questionable cases

With "1.0e100000"? A floating point value with infinity with an ERANGE error?

With "INF"? A floating point value with infinity with an no error?

Careful about ERANGE on the small side

When the string indicates a small value like 1e-100000, this may or may not set errno = ERANGE.

C allows that. C also allows errno to not be set on underflow.

Linux man has "If the correct value would cause underflow, zero is returned and ERANGE is stored in errno."

It is unclear to me what libbsd or OP wants in this case.


There are additional issues anytime the string would convert to a value smaller in magnitude than DBL_MIN. This lack of crispness in strtod() specification renders string in the converted range of DBL_MIN and DBL_TRUE_MIN troublesome.

String to number design

Most string to number functions tolerate leading spaces. I find it curious that most such functions do not well tolerate trailing white-space.

IMO, such functions should - very convenient for reading and converting a line of input like "123\n". Perhaps as:

number = strto*(string, &endptr);

if (string == endptr) return fail_no_conversion;
while (isspace((unsigned char) *endptr)) {
  endptr++;
}
// Now test for null character
if (*endptr) return fail_junk_at_the_end;
...
\$\endgroup\$
9
  • \$\begingroup\$ Bug - failure to clear errno: I prefer to not clear errno in any of my library functions, just as standard library functions do. For that reason I wrote the note acknowledging the bug. Nevertheless, it's unlikely to find errno already with the value ERANGE without knowing just before calling this function. \$\endgroup\$ Sep 3, 2019 at 11:07
  • \$\begingroup\$ Questionable error: Very good point! \$\endgroup\$ Sep 3, 2019 at 11:10
  • \$\begingroup\$ Questionable cases: My point there is that is you wrote "INF" the variable stores exactly what you meant, but if you wrote "1.0e100000" it has lost some important information, so a positive return value indicates that. \$\endgroup\$ Sep 3, 2019 at 11:13
  • \$\begingroup\$ Trailing white-space: very good point! \$\endgroup\$ Sep 3, 2019 at 11:28
  • 1
    \$\begingroup\$ BTW, I did that of the trailing space, and for that it was easier to use good old strtol(), so as a side effect it is more portable now :) Also, now all of the functions share the same error handling function. \$\endgroup\$ Sep 4, 2019 at 10:55
3
\$\begingroup\$
  • The main advantage of strtol and family is that they compute (for free!) the point where the conversion ended. It is a very valuable information, because usually after extracting the number you want to continue parsing. Your wrappers throw it away.

  • DRY. The switches (which compute result) for integral types is identical. Factor it out into a function.

\$\endgroup\$
2
  • \$\begingroup\$ Good points. For the first point, I could add a parameter ptrdiff_t *read at the end and use nonnull(1, 2), so that the user can read it or ignore it passing NULL (a char ** would be another possibility, but I prefer the length, and they're easily interchangeable). \$\endgroup\$ Aug 28, 2019 at 19:44
  • \$\begingroup\$ For the second one, I will use a helper int strtoi_status(int rstatus);. BTW, for the floating point status computation I will also write a helper: int strtof_status(const char *restrict str, const char *restrict endptr, int _errno); \$\endgroup\$ Aug 29, 2019 at 0:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.