[HN Gopher] Generic Containers in C: Safe Division Using Maybe
___________________________________________________________________
Generic Containers in C: Safe Division Using Maybe
Author : uecker
Score : 101 points
Date : 2025-08-11 05:14 UTC (17 hours ago)
(HTM) web link (uecker.codeberg.page)
(TXT) w3m dump (uecker.codeberg.page)
| nly wrote:
| GCC generates essentially the same assembly for C++'s
| std::optional<int> with the exception that the result can't be
| returned in a register (a problem which will go away if the
| function is inlined)
|
| https://gcc.godbolt.org/z/vfzK9Toz4
| uecker wrote:
| Yeah it a amazing a far C++ has come, soon it will be almost as
| good as C.
| JonChesterfield wrote:
| In this instance C++ is far ahead of C, because what you want
| for maybe<> is typestate, and that's only available in C++
| because it's wedded to member functions. See
| https://awesomekling.github.io/Catching-use-after-move-
| bugs-... or similar
| uecker wrote:
| Sure.
| pjmlp wrote:
| Except that it is much more safer to use the type system than
| pre-processor glue based on text replacements, with more
| interesting error messages than templates.
| uecker wrote:
| I don't see how it safer. I think this is just a random
| claim C++ people like to make without any evidence. In
| terms of error message, the problem is that C++ often can
| not produce them because due to overloading it is entirely
| unclear to the compiler what the intention of the code
| actually way. A macro-based solution also does not have
| ideal error message, but I do not think it is worse than
| C++.
| pjmlp wrote:
| It isn't a random claim, is based on years of experience
| fixing pre-processing code gone wrong, because whoever
| wrote it in first place forgotten that it is nothing more
| than text expansion, and then someone else completly
| unaware that it is a macro, ends up giving a bad set of
| parameters.
| uecker wrote:
| I spent also countless amount of hours fixing template
| code, so no I do not let your anecdotes count. There is
| certainly a lot of problematic macro code in C, but I do
| not think it is worse than C++ templates, and one can
| also write robust macros.
| pjmlp wrote:
| One can also write robust template code, if that is the
| reasoning we're going for, enable_if, static_assert and
| concepts have been available for a couple of years now.
|
| At least that is something we can agree on, C and C++ are
| both languages where developers could write robust code,
| and the large majority seldom does it.
| uecker wrote:
| I can agree with this. The difference is that C++
| approach is to solve problems with features people need
| to understand while C solves problems by only providing
| basic building blocks and expects people to use them
| correctly. In both cases it needs expertise to do this.
| But Rust also has this problem.
| wahern wrote:
| Is this because std::optional isn't trivially constructible?
| pwdisswordfishz wrote:
| Destructible. Opportunistically making it trivially
| destructible does the trick.
| pjmlp wrote:
| Proving the point that better type system and abstractions
| don't necessarly produce bad Assembly, and if constexpr is
| used, there won't be anything on the final executable other
| than the actual value.
|
| https://gcc.godbolt.org/z/31o75W5xx
|
| https://gcc.godbolt.org/z/av6a43WeY
| uecker wrote:
| You do not need constexpr, just remove the "volatile" which I
| put into my example only to prevent the optimizer from
| specializing it: https://godbolt.org/z/fs8q3sdxK
|
| But what matters is run-time behavior for any input. And
| sorry, C++'s complexity still has the effect that the code is
| terrible: https://godbolt.org/z/vWfjjf8rP
|
| In C, the compiler can remove all error paths:
| https://godbolt.org/z/GGMcc6bxv
| nly wrote:
| Not sure what you're talking about here. The cruft the
| assembly is because you enabled UBSAN. If you disable UBSAN
| then the exception throwing code paths go away.
| uecker wrote:
| The point is that the error code paths should go avoid
| even with UBSAN because they are dead, but the optimizer
| is not able to see this anymore in C++.
| aw1621107 wrote:
| > But what matters is run-time behavior for any input. And
| sorry, C++'s complexity still has the effect that the code
| is terrible: https://godbolt.org/z/vWfjjf8rP
|
| > In C, the compiler can remove all error paths:
| https://godbolt.org/z/GGMcc6bxv
|
| For what it's worth, if you actually use the same flags for
| the C++ example (in particular, -fsanitize-trap) the C++
| compilers improve quite a bit, with Clang getting fairly
| close to the GCC C output (https://godbolt.org/z/M3z5EaGKj
| ; note that GCC doesn't seem to eliminate the
| std::optional::value() check unless you use -O3). But
| perhaps more interestingly, if you make a simplified
| std::optional that still uses C++-specific features both
| Clang and GCC produce output that is very close to that
| from C/GCC: https://godbolt.org/z/PhEW7eT8x
|
| Removing I/O-related stuff makes this clearer:
|
| C: https://godbolt.org/z/e71s9E63f
|
| C++: https://godbolt.org/z/YnhdxYxY4 (Clang seems unable to
| eliminate the UBSan overflow check for some reason?)
|
| Does make me wonder what exactly it is about std::optional
| that confuses the optimizers, and whether a similarly
| complex C maybe implementation can suffer from the same
| issues that std::optional appears to.
| account42 wrote:
| > with the exception that the result can't be returned in a
| register (a problem which will go away if the function is
| inlined)
|
| It's still a damned shame. Same for not being able to pass
| optional/unique_ptr/etc in a register.
|
| We really need a trivially_relocatable attribute.
| nextaccountic wrote:
| > Here, instead of handling the error condition, I create an
| lvalue that points nowhere in case of an error because it then
| corresponds to ( _({ (void_ )0; })), relying on the null
| sanitizer to transform it into a run-time trap for safety.
|
| Isn't this undefined behavior?
| lmm wrote:
| In standard C yes. But any decent C compiler will offer
| stronger guarantees than the minimum that the standard
| requires, and presumably the "null sanitizer" they're referring
| to is one of them.
| pjmlp wrote:
| As usual, the problem is not what they have been offering for
| the last decades in tooling, rather what developers actually
| make use of.
|
| Unfortunely many keep needing education on such matters.
| playforclaude wrote:
| Any decent C compiler will use loopholes in the standard to
| optimise your code :)
| shakna wrote:
| Maybe, but it is defined for GCC:
|
| > You can store a null pointer in any lvalue whose data type is
| a pointer type. [0]
|
| Though, I would expect a complaint from clang, and clang-tidy.
|
| [0] https://www.gnu.org/software/c-intro-and-
| ref/manual/html_nod...
| uecker wrote:
| It is only undefined behavior if it is dereferenced, in which
| case the null sanitizer can be used to define it to trap, so
| safely terminate the program. But the example then also shows
| how you can make sure that this case is not even possible in
| the final program.
| layer8 wrote:
| The null sanitizer is used to define the behavior, which is one
| of the ways a C implementation is allowed to handle situations
| whose behavior the C standard leaves undefined.
| rowanG077 wrote:
| At this point I always wonder why people who write stuff like
| this don't just move to a different language. You are introducing
| insane amounts of hidden complexity(see also the other posts on
| that blog). For something that just exists in other languages. Or
| maybe this is just a fun puzzle for the author, in which case
| it's totally fine.
| throw-qqqqq wrote:
| You don't always get to choose your language. Especially in the
| embedded/firmware area of software development, C is the most
| widely available option, if not the only option besides ASM
| _shrugs_
| rowanG077 wrote:
| Definitely. I still don't think you should swim against the
| stream. Just bite the bullet and write idiomatic C. The
| people who will have to debug your code in the future will
| thank you.
| throw-qqqqq wrote:
| I agree 100%
| windward wrote:
| It is, but the bar of what's considered too 'clever' in
| embedded/firmware is usually lower than this. In fact, even
| the ternary conditional operator is too much.
| fuhsnn wrote:
| The said library is a bit farther away from the C that is
| widely available. It relies on C23 features, GNU statement
| expression, GNU nested function, sanitizer runtimes, VLA
| types and a very niche pattern of sizeof executing its
| statement-expression argument; only platforms that provide
| latest GCC/Clang would be able to use this.
| uecker wrote:
| In the library I experiment with various things, and C23
| and nested functions are not really required. And for
| running the code, it only relies on GNU statement
| expression. For bounds checking, you need need the
| sanitizers.
|
| Overall, it is still far more portable than C++ or any
| other new language.
| throw-qqqqq wrote:
| Fair point. I did not notice that. Most C-only compilers
| only support C99 or a subset thereof
| pjmlp wrote:
| Unless you are talking about PIC and similar CPUs, there is
| hardly a modern 16 bit CPU that doesn't have a C++ compiler
| available as well, assuming that we still consider 16 bit
| modern for whatever reason.
|
| Heck I learned to program in C++, back when DR-DOS 5 was the
| latest version, and all I had available was 640 KB to play
| around, leaving aside MEMMAX.
|
| Nowadays the only reason many embedded developers keep using
| C is religous.
| uecker wrote:
| Religion and the garbage code C++ compilers produces,
| incomprehensible error message, unstable tooling, and very
| long compilation times.
| simonask wrote:
| What year is it, 1998? Am I going crazy?
|
| I think you're making some extraordinary claims. I'd love
| to see some receipts. :-)
| uecker wrote:
| What specific claim do you think is extraordinary: That
| embedded programmers still often use C, that compilation
| times for C++ are often very long, that the languages is
| less stable than C, or that code produced by C++ can be
| worse?
| throw-qqqqq wrote:
| Purely guessing, but the "unstable tooling" could perhaps
| refer to the fact that C++ as a language has evolved a
| lot.
|
| I have had trouble compiling older C++ code bases with
| newer compilers, even when specifying C++98 as source
| standard. I gave up trying to get Scott McPeak's Elkhound
| C++ parser to compile, last I had to attempt it.
|
| C is a bit more forgiving on that topic (it hasn't
| changed as much, for better or worse).
| pjmlp wrote:
| I assume you are not using GCC or clang for compiling C
| code, given that are garbage compilers written in C++.
|
| Still using tcc, or stuck in GCC 5 ?
| uecker wrote:
| GCC was written in C but changed to C++ later. A lot of
| the code still looks a lot like C. And as a contributor,
| I would much prefer it was purely C (and compilation
| times for GCC itself are a pain - although I think this
| is not because of C++ but because some files grew to big
| and some refactoring would be in order)
| throw-qqqqq wrote:
| > ... there is hardly a modern 16 bit CPU that doesn't have
| a C++ compiler
|
| There are quite a few besides various PICs AFAIK, how
| modern they are is subjective I guess, and it IS mostly the
| weaker chips. Keil, Renesas, NXP, STMicro (STM8 MCUs used
| to be C only, not sure today) all sell parts where C++ is
| unsupported.
|
| > Nowadays the only reason many embedded developers keep
| using C is religous.
|
| I don't completely agree, but I see where you are coming
| from.
|
| The simplest tool that gets the job done is often the best
| IMO.
|
| In my experience, it is much more difficult to learn C++
| well enough to code safely, compared to C.
|
| Many embedded developers are more EE than CS, so simpler
| software is often preferred.
|
| I know you don't have to use all the C++ features, all at
| once, but still :)
|
| Horses for courses. I prefer C for anything embedded.
| amiga386 wrote:
| Quite. There's standard POSIX behaviour for this. Divide by
| zero and execution continues, _safely_ , in your SIGFPE
| exception handler.
| uecker wrote:
| Because the different languages suck much more.
| munchler wrote:
| So this was inspired by Haskell, but you don't actually think
| Haskell is a good language?
| layer8 wrote:
| A language can provide useful features worth adopting, and
| suck at the same time.
| uecker wrote:
| I think Haskell is a cool language. It is still definitely
| very bad for what I use C for.
| dazzawazza wrote:
| Other languages internalise that complexity. C leaves it bare,
| human scale and understandable.
|
| The speed at which you can write great C code often far
| outstrips other languages which are applicable to the problem
| domain.
|
| All languages are a compromise, there are no silver bullets.
| yobbo wrote:
| It might be more useful with a signature like maybe_divide ->
| maybe(int) -> maybe(int) -> maybe(int) ... and then a set of
| operations over maybe, and functions/macros for and_then(),
| or_else(), etc. It would be interesting to see how ergonomic it
| could get.
| munchler wrote:
| I await the first "Monads in C" tutorial with mixed feelings.
| uecker wrote:
| Good idea. This is very easy, here is a preview.
| https://godbolt.org/z/dME1MMr19
| BiraIgnacio wrote:
| I love seeing the creative ways people implement these types of,
| if I can say, high level abstractions in C. Thanks for sharing
| teo_zero wrote:
| Clever, but it misses the very goal of option/maybe types:
| forcing the user to check the result. In this implementation
| nothing stops the user from omitting the "if (p.ok)" part and
| directly using "p.value".
|
| It could work if "maybe(T)" is completely opaque to the user;
| both checking and accessing its payload must happen through
| helper macros; the checking macro ticks an invisible flag if ok;
| the accessing macro returns the payload only if the invisible
| flag is ticked, otherwise it triggers a runtime error/exception.
|
| Not impossible. However, you would need to replace all "p.ok"
| with "maybe_check(p)", which is not unreasonable, and all
| "p.value" with "maybe_value(p)", which might be too much for the
| final user...
___________________________________________________________________
(page generated 2025-08-11 23:01 UTC)