3 ms·
OpenBSD added strtonum to <stdlib.h>. It's quite opinionated, but fits the usage patterns OpenBSD developers prefer. > 50 lines long, of which most is error ha
by wahern 1y ago
OpenBSD added strtonum to <stdlib.h>. It's quite opinionated, but fits the usage patterns OpenBSD developers prefer.
> 50 lines long, of which most is error handling
That's a little exaggerated considering the example is an entire C program, including main. It's more like 14 lines, ignoring the '\0' check (which isn't necessary if you don't want to parse additional items), and even that includes whitespace and stderr logging.
I agree the biggest headache is reliance on errno and the nuanced--albeit conventional, consistent[1], and documented--semantics of only setting errno on error. Some POSIX APIs, at least those defined from scratch (e.g. pthreads, as opposed to incorporated common extensions) prefer returning the error code directly as you recommend. But this would technically only save you a single line, though it might save alot of confusion about semantics.
However, in this case I might keep returning the value directly and take an error pointer, similar to OpenBSD's strtonum. Otherwise you would need separate routines for char, short, int, long, and long long. Though there's still the issue of unsigned integers, and using _Generic it might be possible to at least hide all the type-specific variants behind a single interface.
And there's still the issue of needing to check for trailing garbage, or dropping the ability to use the interface inline with other parsing code. There's alot of dimensions to the problem. Parsing integers may be conceptually simple[2], but designing an interface that's easy to integrate into applications across a variety of contexts is much less simple.
[1] Consistent across libc interfaces, excepting those that may invoke syscalls internally, like printf, where errno may be incidentally modified even on success.
[2] I personally often prefer to just write a simple loop to manually parse integers. It's sometimes easier to integrate the desired error checking inline, or even elide it altogether (garbage-in, garbage-out).
- roelschroeven 1y ago> conventional, consistent[1], and documented--semantics of only setting errno on error. From that man page: > The implementation may also set errno to EINVAL in case no conversion was performed (no digits seen, and 0 returned). There are error cases where the implementation may set errno to EINVAL. There's not even a guarantee. I did a quick test. errno is only set if you pass an invalid base, or if the string does contain a number but it is out of range. If you pass a string which doesn't even remotely look like a number, errno is not set. You have to check endptr. #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { const char *s = "this is not a number"; printf("strtol(\"%s\")\n", s); errno = 0; long a = strtol(s, NULL, 10); printf("errno: %d; strerror: %s\n", errno, strerror(errno)); printf("result: %d\n", a); return 0; } Output: strtol("this is not a number") errno: 0; strerror: Success result: 0 https://godbolt.org/z/TbEKss8zM https://godbolt.org/z/TbEKss8zM That's just rubbish design. In what is in my opinion the most common error case, namely that the string doesn't represent a number while you expect that it does, errno is not set.
- spyrja 1y agoPersonally I'd just go the wrapper approach and ignore errno entirely. #include <assert.h> #include <ctype.h> #include <limits.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> bool copy_str_to_long_long(const char* str, long long* ptr) { while (isspace(*str)) ++str; if (*str == 0) return false; char* parsed = NULL; long long value = strtoll(str, &parsed, 0); while (isspace(*parsed)) ++parsed; if (*parsed != 0) return false; *ptr = value; return true; } long long str_to_long_long(const char* str) { long long result; assert(copy_str_to_long_long(str, &result)); return result; } bool copy_str_to_long(const char* str, long* ptr) { long long buffer; if (!copy_str_to_long_long(str, &buffer) || buffer < LONG_MIN || buffer > LONG_MAX) return false; *ptr = buffer; return true; } long str_to_long(const char* str) { long result; assert(copy_str_to_long(str, &result)); return result; } /* ...define copy_str_to_short, copy_str_to_int32, etc... */