NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
No way to parse integers in C (2022) (blog.habets.se)
orthoxerox 9 hours ago [-]
I wasn't in this class myself, but one prof at my alma mater started his "Programming 201" class with the simplest assignment: write a C program that accepts two integers from the user and prints their sum. It actually was the only assignment for the rest of the semester, since he has a test suite that would humiliate the students gently at first, but would ultimately pipe a billion nines into stdin as the first argument.
dlcarrier 7 hours ago [-]
It's a little awkward, because you'd need to parse the strings in reverse, but if all you need to do is sum, you can do it one digit at a time, while at any given moment only handling only one character from each input string, a carry byte, and one output character.
ronsor 3 hours ago [-]
You don't need to parse the strings in reverse. That's for printing integers, not parsing. Roughly:

    int stdin_atoi() {
      int i = 0;
      while (1) {
        int c = getchar();
        if (c >= '0' && c <= '9') {
          i = i * 10 + (c - '0');
        } else { break; }
      }
      return i;
    }
addaon 3 hours ago [-]
That covers the ‘int’ case, but not the ‘integer’ case described. Unless you have unlimited memory, you’ll need to go least- to most-significant digit; but you’ll need to do that on both inputs, which doesn’t really work with the interface described unless at least the first argument first in memory all at once, so… well, I assume “I under specified this problem and it’s impossible” is the point of this sort of exercise.
pbalau 4 hours ago [-]
How do you know where the first string ends and the second starts? Did you miss the "stdin" part?

This is not

    ./program first_number second_number
jeffrallen 8 hours ago [-]
Would be fun to write a program that arranges to send the input into dc(1) and just outsource the whole problem to Ken or Rob or whoever wrote it. :)
Henchman21 5 hours ago [-]
It would be fun, but were I the teacher I'd commend you for your ingenuity, and then ask you to return to your desk to complete the assignment.
msie 8 hours ago [-]
Perfect is the enemy of good.
lanstin 5 hours ago [-]
That's true for product development, but it's not true for mathy libraries. Perfect is achievable. For a released software that humans will decide to use or not, rapid iteration is great. But also: https://randomascii.wordpress.com/2014/01/27/theres-only-fou...

Precision and exactitude and formally proven correct software can exist in some problem domains, and it's kind of silly to not achieve that when it's achievable.

chowells 7 hours ago [-]
But in this case, C is not "good". It is more like "abysmal". "Good" is just producing a correct result or error, with no ambiguity which case applied and no UB. "Perfect" is arguing over the most usable and elegant API for it.
pjc50 8 hours ago [-]
Once a program is available over the internet, hackers are the enemy of merely good programs that don't perfectly validate their input.

"You have to get lucky every time. We only have to get lucky once".

msie 6 hours ago [-]
Sure
6 hours ago [-]
clark_dent 8 hours ago [-]
Could you humor a coding noob--how do you deal with utterly insane inputs like that?
wwalexander 6 hours ago [-]
Arbitrary precision arithmetic (GMP, BigInteger, etc). Numbers can take arbitrary amounts of memory, instead of just a single machine word.
matthewkayin 7 hours ago [-]
You first ask if you really need to.
AnimalMuppet 7 hours ago [-]
Unless you're exposing it to the internet, ever, in the entire future history of the program. Then you kind of have to, in one form or another.
Someone 6 hours ago [-]
You have to, but you probably shouldn’t do it by trying to add the inputs. That opens a door for DDOS attacks.

Returning an error on inputs that are too long (for some definition of it) is the way to go.

doubled112 7 hours ago [-]
Crash and report an error.
chowells 7 hours ago [-]
You report an error and exit cleanly with a proper operating system error code. Crashing is a quick hack, acceptable for throwaway projects but not in software used long-term.
SoftTalker 6 hours ago [-]
Crashing (in the sense of "give up and exit with an error") on invalid inputs is valid (and often the best thing) in many cases.

Fix your inputs.

chowells 6 hours ago [-]
I think you're using "crash" to mean "exit early". I am using "crash" in the sense of "this program did something causing the OS to terminate it externally". I suppose that's a real point of difficulty in communication across different programming languages.

We agree that the program should exit early. I think we agree it should do it cleanly and intentionally. I'm adding the constraint that "crash" doesn't necessarily mean "cleanly and intentionally", especially when talking about a C program.

derefr 4 hours ago [-]
There's a position in between "exit cleanly" and "general protection fault, core dumped" where the process essentially does the internal equivalent of SIGKILLing itself.

I.e. either intentionally (e.g. tripping an assertion failure), or accidentally due to some logic-failure in exception/error-handling, the process ends up calling the exit(3) syscall without first having run its libc at_exit finalizers that a clean exit(2) would run; or, at a slightly higher runtime abstraction level, the process calls exit(2) or returns from main(), without having run through the appropriate RAII destructors (in C++/Rust), or gracefully signalled will-shutdown to managed threads to allow them to run terminating-state code (in Java/Go/Erlang/Win32/etc), or etc.

This kind of "hard abort" often truncates logging output at the point of abort; leaves TCP connections hanging open; leaves lockfiles around on disk; and has the potential to corrupt any data files that were being written to. Basically, it results in the process not executing "should always execute" code to clean up after itself.

So, although the OS kernel/scheduler thinks everything went fine, and that it didn't have to step in to forcibly terminate the process's lifecycle (though it did very likely observe a nonzero process exit code), I think most people would still generally call this type of abort a "crash." The process's runtime got into an invalid/broken state and stopped cleaning up, even if the process itself didn't violate any protection rules / resource limits / etc.

bsenftner 11 hours ago [-]
One of the first homework assignments when I learned C back in '83 was after a long lecture on how the string functions are fundamentally broken, and the class introduction to writing C was fixing all of them.
psvv 9 hours ago [-]
My memory growing up is that making your own C library was basically an inevitable rite of passage for any aspiring programmer.
lanstin 2 hours ago [-]
And then your own custom allocator that would be fitted for your algorithms and vastly faster than malloc.
prerok 6 hours ago [-]
Yeah, it's a shame we never got something like boost for C. Every company I ever worked for had its own common C library solving these problems.
ndesaulniers 6 hours ago [-]
It's a shame we never got a package manager for C (or C++).

EDIT: perhaps I should have been clearer; by not having one early on, we now have multiple competing package managers, with no clear winner. Responses prove that point.

fintler 5 hours ago [-]
Although vcpkg is probably the most popular, I’m a fan of https://conan.io/center
bsenftner 5 hours ago [-]
Never used this? https://vcpkg.io/en/
bsenftner 5 hours ago [-]
I worked at a shop where we used Boost in a C++ code base that the only use of C++ was the harness to use Boost. After that, it was all C, object-styled C, as that code base started before C++ compilers were not a template overlay on C.
ramon156 10 hours ago [-]
Why not look at how other languages attack this? e.g. how does "42".parse() work in rust?

Edit: https://doc.rust-lang.org/src/core/num/mod.rs.html#1537

interesting! It boils down to this

pub const fn from_ascii_radix(src: &[u8], radix: u32) -> Result<u32, ParseIntError> {

    use self::IntErrorKind::*;

    use self::ParseIntError as PIE;

    // guard: radix must be 2..=36

    if 2 > radix || radix > 36 {

        from_ascii_radix_panic(radix);

    }

    if src.is_empty() {

        return Err(PIE { kind: Empty });

    }

    // Strip leading '+' or '-', detect sign

    // (a bare '+' or '-' with nothing after it is an error)

    // accumulate digits, checking for overflow

    Ok(result)

}
marcosdumay 8 hours ago [-]
It's not an overwhelming hard problem. There are some issues with radix signaling, exponent notation, decimal points being allowed or not, and group separators that make parsing numbers incredibly irritating. So you usually don't want to do it yourself.

But it's not hard at all. It's not even as full of small issues that you can't handle the load, like dates. It's just annoying as hell.

The problem is exclusive to C and C++. It's created by the several rounds of standardization of broken behavior.

alexfoo 10 hours ago [-]
I remember an old project that ran into something like this. I think we just used atoi() or similar and the error check was a string comparison between the original input and a sprintf() of the converted value.

Ugly (and not performant if in a hot path) but it works.

zokier 11 hours ago [-]
I thought it was pretty well known that everything related to strings in C stdlib (including all str... functions) is bad. You just need to bring in your own string library.
bhk 8 hours ago [-]
Not just the string-related functions. If you want robust error checking, re-entrant code, and bounds checking performed in library functions (instead of performing bespoke validations all across your code base), you have some work to do. Yes, some improvements have been tacked on over the years, but many problems ("current locale", for one) remain endemic.

In my experience, the worst part of the C standard library is not its existence, but the fact that so many developers insist on slavishly using it directly, instead of safer wrappers.

6 hours ago [-]
voidUpdate 11 hours ago [-]
Cant you just:

  for(int i = 0; i < len(characters); i++)
  {
    if(characters[i]-48 <= 9 && characters[i]-48 >= 0)
    {
      ret = ret * 10 + characters[i] - 48;
    }
    else
    {
      return ERROR;
    }
  }
  return ret;
Adjust until it actually works, but you get the picture.
dlcarrier 5 hours ago [-]
Here's a readability tip for working with ASCII numbers: Treat adding and subtracting the ASCIIness as you would multiplying and dividing by a unit in physics. You can add '0' to convert a numeral to ASCII and subtract '0' to convert it back, and you can do direct comparisons between ASCII numerals.

    if(characters[i] <= '9' && characters[i] >= '0')
    {
      ret = ret * 10 + characters[i] - '0';
    }
3 hours ago [-]
knome 10 hours ago [-]
this wouldn't catch overflow or underflow errors, nor does it allow non-base-10 numbers, nor does it handle negative numbers. and writing your own parser is a failure case by op's logic. they are complaining about the builtin parsing functions.

the author admits you can parse signed integers in their second example, but for unsigned, they don't like seem to like that unsigned parsing will accept negative numbers and then automatically wrap them to their unsigned equivalents, nor do they like that C number parsing often bails with best effort on non-numeric trailing data rather than flagging it an error, nor do they like that ULONG_MAX is used as a sentinel value by sscanf.

I'm not sure what they mean by "output raw" vs "output"

    $ cat t.c
    
    #include <stdlib.h>
    #include <math.h>
    #include <stdio.h>
    
    int main(int argc, char \* argv){
    
      char * enda = NULL;
      unsigned long long a = strtoull("-18446744073709551614", &enda, 10);
      printf("in = -18446744073709551614, out = %llu\n", a);
      
      char * endb = NULL;
      unsigned long long b = strtoull("-18446744073709551615", &endb, 10);
      printf("in = -18446744073709551615, out = %llu\n", b);
      
      return 0;
    }
    $ gcc t.c
    $ ./a.out 
    in = -18446744073709551614, out = 2
    in = -18446744073709551615, out = 1
    $
I get their "output raw" value. I don't know what their "output" value is coming from.

I don't see anywhere they describe what they are representing in the raw vs not columns.

thomashabets2 8 hours ago [-]
> they don't like seem to like that unsigned parsing will accept negative numbers and then automatically wrap them to their unsigned equivalents, nor do they like that C number parsing often bails with best effort on non-numeric trailing data rather than flagging it an error, nor do they like that ULONG_MAX is used as a sentinel value by sscanf.

That's right. I don't like asking it to parse the number contained inside a string, and getting a different number as a result.

That's just simply not the right answer.

> I'm not sure what they mean by "output raw" vs "output"

I can see how that's very unclear. Changed now to "Readable".

card_zero 8 hours ago [-]
I think "output" is just supposed to be a human-readable version of "output raw". So the line in the table where "output raw" is 2 but "output" is 1 looks like a mistake. It's repeated in the table for sscanf().
thomashabets2 8 hours ago [-]
Yup. Sorry about that.
Sharlin 10 hours ago [-]
And how does this avoid returning nonsense if the number is too large? (Wrapping if the accumulator is unsigned, straight to UB land if signed.) Not reporting overflows as errors is one of the major problems demonstrated by TFA.
voidUpdate 10 hours ago [-]
you could check if ret > ret * 10 + characters[i]-48, if so it has wrapped around and you return an error
thomashabets2 8 hours ago [-]
For unsigned that could work, but signed overflow is UB.
Thiez 9 hours ago [-]
[dead]
fhdkweig 9 hours ago [-]
What if the number you want to return just happens to be the value of ERROR? You need an error flag that can't be represented as an int, but then C wouldn't let you return it from a function that only returns "int". It is why some languages throw exceptions and why databases have the special "null" value.
voidUpdate 9 hours ago [-]
I don't use C enough to know what the convention is for throwing an error when the function can return a number anyway. You'd have to ask someone else
zbentley 8 hours ago [-]
In C, errors are usually indicated by a negative return value constant, crashing the program with abort, or setting the errno global (thread-local, but whatever) and expecting callers to check it. Sometimes multiple of those.
QuercusMax 8 hours ago [-]
One reasonably common pattern is to have the return value indicate success / error, and you pass in a pointer to the value which will be mutated if successful.
jerf 9 hours ago [-]
And why some very, very special languages have an effectively-global variable called "errno" that you have to check after the call manually, and worry about whether maybe it was populated from some previous error. Nothing says "production-quality language that an entire civilization's code base should be based on" like "sometimes (but only sometimes!) functions return additional information through global values".
aleph_minus_one 9 hours ago [-]
> And why some very, very special languages have an effectively-global variable called "errno" that you have to check after the call manually, and worry about whether maybe it was populated from some previous error.

As you can read at https://en.wikipedia.org/wiki/Errno.h errno is barely used by the C standard (though defined there). It is rather POSIX that uses errno very encompassingly. For example the WinAPI functions use a much more sensible way to report errors (and don't make use of errno).

bitwize 10 hours ago [-]
You cannot "just" anything in C without hitting a minefield of UB. It is, probably, more economical to convert your entire project to Rust than it is to do the pufferfish spine removal procedure of auditing the code base for UB and replacing the problem areas. With generative AI, the size of project for which this remains true may be as large as "the entire Linux kernel".
norir 5 hours ago [-]
This is not a hard thing to do without using a library. The code below is easily adapted to the unsigned case and/or arbitrary base rather than 10.

    #include <stdio.h>
    int main(int argc, char **argv) {
        if (argc != 2) {
            fprintf(stderr, "usage: require one numeric argument");
        }
        char *nump = argv[1];
        unsigned neg = 0;
        unsigned long long ures = 0;
        if (*nump == '-') {
            neg = 1;
            nump = nump + 1;
        }
        if (!*nump) {
            fprintf(stderr, "require non empty string\n");
            return 1;
        }
        char b;
        while (b = *nump++) {
            if (b >= '0' && b <= '9') {
                unsigned long long nres = (ures * 10) + (b - '0'); 
                if (nres < ures) {
                    fprintf(stderr, "overflow in '%s'\n", argv[1]);
                    return 1;
                }   
                ures = nres;
            } else {
                if (b >= ' ') {
                    fprintf(stderr, "invalid char '%c' in '%s'\n", b, argv[1]); 
                } else {
                    fprintf(stderr, "invalid byte '%d' in '%s'\n", b, argv[1]);
                }
                return 1;  
            }
        }
        long long res = (long long) ures;
        if (neg) {
            if (ures <= 0x8000000000000000ULL) {
                res = -res;
            } else {
                fprintf(stderr, "underflow in '%s'\n", argv[1]);
                return 1;
            }
        } else if (ures > 0x7FFFFFFFFFFFFFFFULL) {
            fprintf(stderr, "overflow in '%s'\n", argv[1]);
            return 1;
        }
        fprintf(stdout, "result: %lld\n", res);
        return 0;
    }
wCxV8HzziQBb 5 hours ago [-]
The bound on ures <= 0x80[...] should be either ures < 0x80[...] or ures <= 0x7F[...]. Otherwise, parsing negative `0x8000000000000000` will run code to negate the signed integer INT64_MIN (-0x80[...]) to 0x80[...], which doesn't fit in an integer (INT*_MAX is 0x80[...]).

    $ clang parseint.c -fsanitize=undefined -O0 -g -o parseint
    $ ./parseint -9223372036854775808
    parseint.c:38:23: runtime error: negation of -9223372036854775808 cannot be represented in type 'long long'; cast to an unsigned type to negate this value to itself
    result: -9223372036854775808
edit: this is just to show that getting undefined behavior right is hard!
contubernio 8 hours ago [-]
One of the great virtues of C is that this sort of thing is not part of the language ...
thomashabets2 7 hours ago [-]
Only literally. 7.24.1 in the C programming language spec has these poor parsers.
rbanffy 7 hours ago [-]
Is their misbehavior part of the spec as well? If not, we can always add the correct behavior to the spec and let anyone who implemented a broken version deal with fixing every program compiled using it.
thomashabets2 6 hours ago [-]
Fair enough.

For strtoul and friends, maybe? 7.24.1 is pretty dense, but the key parts are "the expected form of the subject sequence is a sequence of letters and digits representing an integer with the radix specified by base, optionally preceded by a plus or minus sign […] If the correct value is outside the range of representable values […] ULONG_MAX […] is returned".

So the "expected form" allows a minus sign, but then it's clearly "outside the range of representable values" for strtoul to try parsing a negative value. So maybe it should return ULONG_MAX on those.

So arguably a minus sign present could already be treated as an error, and still be standard compliant. Unless I'm misreading.

rbanffy 5 hours ago [-]
Passing a negative value to a function that is specifically for converting strings into unsigned numbers is pretty much an error. In the case of functions that return an unsigned number, at least, negative return values can represent errors.

It’s more fun when the result can be signed though. Maybe strcmp with the representation of the LONG_MAX, and if it doesn’t match, call strtol and watch for a LONG_MAX indicating an error.

C is a bit messy. Would be nicer to return a struct with a possible error and the desired value, Golang style.

thomashabets2 2 hours ago [-]
If that's an error then so is passing in a non number.

So catch 22. You can only check for valid numbers if the number is valid?

CodesInChaos 9 hours ago [-]
Another case many integer parsing functions get wrong is that they interpret a leading 0 as an octal indicator.

That should be opt-in via a flag, if it needs to be supported at all. Unix file permissions are the only deliberate use of octal I've ever seen.

kevin_thibedeau 8 hours ago [-]
It used to be much more common. In the 70s there was a lot of collective hesitance to use hex with its strange letter digits. Octal was the compact representation of choice.
jervant 10 hours ago [-]
bmandale 9 hours ago [-]
Interestingly fails as well, in two ways. First:

> The string may begin with an arbitrary amount of whitespace (as determined by isspace(3))

Second is that it only applies to signed long long, not unsigned.

mike_hock 4 hours ago [-]
The problem is that float parsing is highly non-trivial if you want it to be correct for all edge cases.

For integers, you're faster (in both development time and runtime) to write your own parser than to try and assemble the pieces in this pile of shit into a half-working one.

C++17 from_chars excluded. Incidentally, 2022 seems about right for the year that ONE open source implementation finally actually implemented the float part of that. Or was it more like 2024?

eithed 10 hours ago [-]
Can't you regex that given string contains just numbers and then use any of the provided methods? Then check if the returning value is a number to cater for edge cases

Ok, having a method to do that for you would be nice, but the post reads like it's an issue that std library doesn't provide you with a method behaving as you exactly want

fastaguy88 5 hours ago [-]
And yet, thousands and thousands of 'C' programs parse integers every hour successfully.

Perhaps the right title should be "No way to parse pathological edge cases in 'C'"

And then see how other languages do.

lacewing 5 hours ago [-]
There's no one correct way to parse integers. Do you want to support 0x prefixes? Is a leading zero an indicator or octal, a zero-padded decimal, or a syntax error? Are you willing to accept a leading "+"? Are leading whitespaces OK? Trailing ones? Is 0x0c a whitespace? What about all the weird Unicode ones? Do you allow exponential notation (1e1)? Etc, etc.

In every language, the standard library makes some assumptions about this. In JavaScript, an empty string parses to zero.

The standard C library, which dates back to the stone age, does the simplest thing you can do without range checking, because, well, that's kinda the C paradigm. If you want parsing that handles edge cases in a specific way, you do it yourself. It's just digits.

mike_hock 4 hours ago [-]
> There's no one correct way to parse integers.

No, but there are a myriad of incorrect ways and the C library's way is one of them.

It's perfectly fine to make reasonable choices for all those options and then implement them correctly.

derefr 4 hours ago [-]
> It is not OK to stop at the first sign of trouble, and return whatever maybe is right. “123timmy” is not a number, nor is the empty string.

None of the C functions referenced (atol, strtol, sscanf) are number-parsing functions per se. Rather, they're numeric-lexeme scanning+extraction functions.

These functions are all designed to avoid making any assumptions about the syntax of the larger document the numeric lexeme might be embedded in. You might, after all, be using a syntax where numbers can come with units on the end. Or you might be reading numbers as comma-separated values.

And, as a key point the author might be missing: C, in being co-designed with UNIX, offers primitives tuned for the context of:

- writing UNIX CLI tools that work with unbounded streams of input (i.e. piped output from other UNIX CLI tools),

- where, crucially, the stream is just text, and so carries no TLV-esque framing protocol to tell you the definitive length of a thing;

- and nor (especially in early memory-constrained systems) are you able to perform allocations of heap memory in order to employ an unbounded growable buffer for retaining the current lexeme until you do reach the end of it (which, if you could, would let you use a scanner state-machine that doubles as a parser/validator, returning either a parsed value or an error)

- but instead, to deal with the 1. unbounded input, 2. of textual encoding, 3. in constant memory, you must eagerly scan the input stream (i.e. synchronously reduce over each received byte, or at most each fixed-length N-byte chunk using a static or stack-allocated fixed-length buffer, discarding the original string bytes once reduced-over) to produce lexically-decoded (but not parsed/validated) lexemes; and then do this again, on a higher level, feeding your stream of lexemes into a fixed-sized sum-typed ring-buffer (i.e. an array-of-union-typed-lexeme-struct-type-entries), where you can then invoke a function that attempts to scan over + consume them (but unlike the original stream-parsing function, doesn't consume the buffer unless successful, and so isn't functioning as a scanner per se, but rather as an LR parser.)

If you're not writing UNIX CLI tools, direct use of the C-stdlib numeric-lexeme scan functions is operating on the wrong abstraction layer. What you want, if you have pre-framed strings that are "either valid numbers or parse errors", is to implement an actual parsing function... that can then invoke these numeric-lexer functions to do the majority of its work.

And if you're writing C, and yet you're not in UNIX-pipeline unbounded-text-stream land, but rather are parsing well-defined bounded-length "documents" (like, say, C source files)... then you probably want to use a real lexer-generator (like flex) to feed a parser-generator (like yacc/bison). Where:

- you'd validate the token in context, in the parsing phaase;

- and your lexing rules would make certain classes of input invalid at lexing time. (E.g. you can write your lexeme matching rules such that multi-digit numbers with leading zeroes, or floating-point values with no digits before/after the decimal place, simply aren't "numbers" from your lexer's perspective.)

...which means that, once again, you can "get away with" invokeing the regular C numeric-lexeme scanner functions; i.e. `yylval = atoi(yytext);` in bison terms. (And you'd want to, since doing so saves memory vs. keeping the numbers around as strings.)

chadgpt3 10 hours ago [-]
... say users of only language with no way to parse integers.

:)

stephc_int13 11 hours ago [-]
As a C programmer, I find this kind of bad faith article very irritating.

Yes, the standard library is bad. This is by far the worst part of the C legacy. But it is not that hard to write your own.

String functions like this are not difficult at all, and you can use better naming and semantics, write faster code etc.

C is not the C standard library, ffs.

konmok 10 hours ago [-]
I don't think it's in bad faith.

The distinction between a language and its standard library gets blurry even in theory, and in practice they're nearly inseparable. If a language's standard library has four ways of doing almost the same thing, and they're all fundamentally broken, that's a problem.

stephc_int13 9 hours ago [-]
If you read the other articles by the same author on his blog, you'll see that he has some strong and weird opinions about C and UB.

Complete BS in my opinion.

dosisking 10 hours ago [-]
[flagged]
alexfoo 10 hours ago [-]
Exactly. A wrapper that handles all of the edge cases properly and gives proper reporting just gets added to your own library of functions and the devs get used to using it. Much like the code for abstract data types like lists/hashmaps/etc which neither C nor the standard libraries provide.

Bonus points for having bespoke linting rules to point out the use of known “bad” functions.

In one old project we went through and replaced all instances of sprintf() with snprintf() or equivalent. Once we were happy that we’d got every occurrence we could then add lint rules to flag up any new use of sprintf() so that devs didn’t introduce new possible problems into the code.

(Obviously you can still introduce plenty of problems with snprintf() but we learned to give that more scrutiny.)

1718627440 10 hours ago [-]
> like lists/hashmaps/etc which neither C nor the standard libraries provide

There is a hashmap implementation though: https://man7.org/linux/man-pages/man3/hsearch.3.html

steveklabnik 8 hours ago [-]
“One hashmap for your entire program” is not generally what people mean when they want a hashmap.
alexfoo 7 hours ago [-]
> The three functions hcreate_r(), hsearch_r(), hdestroy_r() are reentrant versions that allow a program to use more than one hash search table at the same time.
alexfoo 9 hours ago [-]
Sure there's an implementation, but like the integer comparison functions that sparked this thread there are some severe limitations with the implementation.

(In fact, looking at it again, I assume I'd purposely purged it from my memory given how terrible it is.)

The non-extensible nature is the biggest one. There are plenty of times when the maximum number of elements needed to be stored will be known in advance. (See the note about hcreate().)

Secondly the hserach() implementation requires the keys to be NUL terminated strings since "the same key" is determined using strcmp(). Good luck if you want to use a number, pointer, arbitrary structure or anything else as a key.

Any reasonable hash table implementation would not have either of these limitations.

Maybe I needed to say:

> > like lists/hashmaps/etc which neither C nor the standard libraries provide

... reasonable implementations of.

thomashabets2 8 hours ago [-]
While snprintf() is better than sprintf(), I find that it's easy for people to not check if the return value is bigger than the provided size. Sure, it prevents a buffer overflow, but there could still be a string truncation problem.

Similar to how strlcpy() is not a slam dunk fix to the strcpy() problem.

alexfoo 7 hours ago [-]
That's partly the point.

If someone uses sprintf() you have to go faffing around to check whether they've thought about the destination buffer size. The size of the structure may be buried far away through several layers of other APIs/etc.

Using snprintf() doesn't solve this in any way, but checking whether the new use of snprintf() checks the return value is relatively simple. Again, there's still no guarantee that there aren't other problems with snprintf() but, in our experience, we found that once people were forced to use it over sprintf() and had things checked in PR reviews we found that the number of instances of misuse dropped dramatically.

It wasn't the switch of functions that reduced the number of problems we saw, but the outright banning of the known footgun `sprintf()` and the careful auditing and replacement of it with `snprintf()` that served as a whole load of reference copies for how to use it. We spread the work of replacing `sprintf()` around the team so that everyone got to do some of the switches and everyone got to review the changes. And we found a whole load of possible problems (most of which were very unlikely to ever lead to a crash or corruption.)

The same would apply if you picked any other known footgun and did similar refactoring/rewrites/auditing/etc.

Anyway, I haven't done C commercially/professionally for about 5 years now. I do miss it though.

wang_li 10 hours ago [-]
The thing I find irritating is all the folks who say C is broken because it’s not a write once run anywhere language like JavaScript or python. Part of the deal has always been that the programmer needs to understand the target platform and the target compiler’s behavior.
DowsingSpoon 7 hours ago [-]
Write once run anywhere? But C already is a "write once run anywhere" language! Though, you usually have to recompile first :)

The criticisms related to UB are not about understanding the target platform and the target compiler's behavior. Undefined Behavior is not the same thing as Implementation-defined Behavior, and lots of folks (including me) would be satisfied with reclassifying chunks of UB as the latter.

The behavior of the target platform isn't really the issue. C23 mandates two's complement for signed integers. Most hardware wraps on overflow, but that literally doesn't matter. The standard says a program exhibiting signed overflow is undefined, period.

In practice, UB rules mean the compiler is free to remove checks for signed overflow/underflow, checks for null pointers, etc. This can and does happen. Man, just a few weeks ago, I just had to deal with a crash in a C program that turned out to be due to the compiler removing a null check. That was a painful one.

prerok 6 hours ago [-]
> crash in a C program that turned out to be due to the compiler removing a null check.

The what now? Though not lately, I did program in C for 15 years and never seen something like this. I did see some compiler bugs on obscure platforms (SINIX, IRIX, HPUX on Itanium64, etc.) with proprietary compilers, this kind of thing would make really get me shouting.

Were you able to determine why the compiler did this? Is it a bug in the compiler?

thomashabets2 7 hours ago [-]
The point of this post, though, is even something as simple as "give me this string as an integer" doesn't have an answer that doesn't come with "are you OK with this best effort parse under these edge cases? Oh and we use this number as error, so you can't parse that".

Like… edge cases? It's parsing a number! We're not talking about I/O on hard vs soft intr NFS mounts, here. There's a right answer.

strlen(), on valid null terminated strings, doesn't come with caveats like "oh we can't measure strings of length 99".

But sure, C is turing complete. It is possible to solve any problem a turing machine can solve.

> understand the target platform and the target compiler’s behavior.

This is neither. This is purely the language.

prerok 5 hours ago [-]
Somewhat true, but C is pretty close to translating directly to machine code, even if most compilers now do so many complex things the assembly can be pretty far off. My point being is that if you have a type int in your program, it's specifically tied to the byte size of an integer on the target platform. While it can be 8, 16, 32, 64 bits, it's defined based on what the target platform supports efficiently.

So, when you say, "it's purely the language", I have to disagree. The language means different things on different platforms but it's still defined exactly on the target platform. And it's efficient on that platform.

Nowadays, we prefer correct vs. efficient, which I do agree with, of course. But, I also understand why C is like it is. It is possible to claim it's a problem of the language but I would argue that it is not. C gives us barebones and working with it we have to know this. If that's not needed then sure, other languages will be easier to work with.

mswphd 8 hours ago [-]
isn't the whole point of C that it's portable assembly though? needing to understand the target platform/compiler's behavior to write correct code seems to cut against that claim quite a bit.
wang_li 7 hours ago [-]
No. What gives that idea? The language doesn't even fix the data size of its primary numerical type. No way anyone thought that was portable.
konmok 7 hours ago [-]
Is this sarcasm? I thought C didn't fix the size of int because they were trying to make C programs "portable" between architectures with different natural word sizes. It was a mistake, but I remember that as being the stated reason. I'm happy to be corrected if I'm misremembering my history though.
prerok 5 hours ago [-]
Why would it be a mistake? It's efficient for the target platform.

The same code can be compiled for different platforms, yes, but the assembly and machine code will vary significantly, so it could behave differently. Porting to a new platform was usually a very complex process, but the code produced was efficient. Nobody seems to care about this nowadays, though, it seems.

wang_li 5 hours ago [-]
I suppose one could say that they didn't fix the data size so the language would be portable. But I can't see how the intent was that programs would be portable, if you define portable to mean 1:1 functioning across differing platforms.
msie 8 hours ago [-]
The people downvoting you are probably not C programmers and love to hate C.
card_zero 6 hours ago [-]
I guess trying to write in Rust makes them irritable.
Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 23:38:13 GMT+0000 (Coordinated Universal Time) with Vercel.