The empire of C++ strikes back with Safe C++ blueprint

(theregister.com)

82 points | by amichail 3 days ago ago

110 comments

  • pama 3 days ago
  • netbioserror 2 days ago

    It's becoming increasingly obvious why Apple and some newer languages like Nim went with RC by default. You get safety, the near-elimination of memory-semantic clutter, AND deterministic performance that's good enough for most use cases.

    • pjmlp 2 days ago

      The only deterministic thing about reference counting is call sites.

      There is nothing deterministic about how long those calls take, or stack size requirements, especially if they trigger cascade deletions, or are moved into a background thread to avoid such scenarios.

      Reference counting optimizations slowly turn into a tracing GC.

      • marssaxman 2 days ago

        Indeed, "A Unified Theory of Garbage Collection" shows that tracing and counting are duals: https://dl.acm.org/doi/10.1145/1028976.1028982

      • pornel 2 days ago

        It's possible, but that's an exaggeration of a degenerate case. RC doesn't mean all data must be tangled into an unknowably large randomly connected web of objects.

        The behavior is deterministic enough to be profiled and identified if it actually becomes an issue.

        Identifying causes of pressure on a mark-and-sweep style GC is much more difficult, and depends on specialized GC instrumentation, not just a regular profiler.

        In practice, you have predictable deallocation patterns for the vast majority of objects. The things that are "young generation" in a GC are the things that get deallocated right away in RC.

        Time required to deallocate is straightforwardly proportional to the dataset being freed. This can be a predictable bounded size if you're able to control what is referencing what. If you can't do that, you can't use more constant-time alternatives like memory pools either, because those surprise references would be UAFs.

        I've been there when Apple moved Cocoa from GC to ARC, and the UI stutters have disappeared. It's much more palatable to users to have RC cost happening in line with the work application is doing, than have it deferred to cause jank at unexpected times seemingly for no reason.

        • pjmlp a day ago

          Were you also there when GC failure was actually underlying C semantics from Objective-C, producing random crashes, especially with mixed code bases, instead of the marketing material "why RC?"?

          There is a reason why RC is considered the baby algorithm from automatic memory management algorithms.

          Anything that people point out as optimizations, and profiling tools, also exist for the better algorithms.

          Most languages with automatic memory management support, also offer primitives to deterministic call sites, if one so desires.

          Finally it isn't as if Apple is a genius that managed to revolutionized memory management algorithms, doing in Cocoa what Microsoft was already doing with COM, was the natural way out given Objective-C' GC unsound implementation.

          Swift's requirement to stay compatible with Objective-C memory management, naturally required the same approach, the alternative being something like CCW/RCW COM interop from .NET, which understandably they didn't want to go down to, given previous history.

          • pornel a day ago

            My point was that RC in practice is pretty deterministic in the vast majority of cases, and I think equating its unpredictable rare cases with natural unpredictability of a GC is scaremongering. The differences between their non-deterministic behaviors are significant enough that they can't be simply equated with a sweeping generalization.

            This is true regardless of which approach has higher-throughput implementations or lower overhead overall, and completely unrelated to Apple's marketing, or Microsoft being first at something.

        • neonsunset a day ago

          Generational GC designs do not "deallocate" objects. In fact, most GCs don't. It's an understandable but an unfortunate misconception that sometimes causes developers to write more GC-unfriendly code than necessary.

          When a collection in a generational GC occurs, the corresponding generation's heap is scanned for live objects, which are then usually relocated to an older generation. In a most common scenario under generational theory - only few objects survive, and most die in a young/nursery/ephemeral generation (.NET, JVM and other ecosystems have different names for the same thing).

          This means that upon finishing the object relocation, the memory region/segment/heap that was previously used can be immediately made available to subsequent allocations. Sometimes it is also zeroed as part of the process, but the cost of such on modern hardware is miniscule.

          As a result, the more accurate intuition that applies to most generational GC implementations is that pause time / CPU cost scales with live object count and inter-generational traffic. There is no individual cost for "object deallocation". This process is vastly more efficient than reference counting. The concern for overly high allocation traffic remains, which is why allocation and collection throughput are defining characteristics of GC implementations, alongside average pause duration, frequency, costs imposed by specific design elsewhere, etc.

          Allocating and deallocating a complex graph of reference-counted objects costs >10x times more than doing so with a modern GC implementation. I don't know which implementation was used back in the day in Cocoa, but I bet it was nowhere as advanced as what you'd see today in JVMs or .NET.

          • pornel a day ago

            You've misread my comment. All my uses of "deallocate" refer to RC.

          • pjmlp a day ago

            The issue is that Objective-C GC was a failure, but not for the reasons Apple marketed its pivot into RC.

            Thankfully the documentation is still available, so those that were there can dig it out from the archives,

            "Inapplicable Patterns"

            https://developer.apple.com/library/archive/documentation/Co...

            "Managing Opaque Pointers"

            https://developer.apple.com/library/archive/documentation/Co...

            "Compile GC-Only"

            https://developer.apple.com/library/archive/documentation/Co...

            "Memory Management Semantics"

            https://developer.apple.com/library/archive/documentation/Co...

            A few entry points into the documentation, where several ifs and buts are described on how to make use of Objective-C GC, without possibility having things go wrong.

            All of this contributed for Objective-C not being a sound implementation with lots of radar issues and forum discussions, as you might expect trying to make existing projects, or any random Objective-C or C library now work under Objective-C GC semantics and required changes, wasn't that easy.

            Naturally having the compiler automate retain/release calls, similar to how VC++ does with _com_ptr_t (which ended up being superseded by other ways) for COM, was a much better solution, without requiring a "rewrite the world" approach.

            Automate a pattern developers were already expected to do manually, and leave everything else as it is, without ifs and buts regarding code best practices, programming patterns, RC / GC interoperability issues with C semantics and so on.

            The existing retain/release calls wouldn't be manually written any longer, everything else stays the same.

            Naturally Apple being Apple, they had to sell this at WWDC as some kind of great achievement of how RC is much better than GC, which in a sense is correct but only from point of view of the underlying C semantics and the mess Objective-C GC turned out to be, not tracing GC algorithms in general.

  • pizlonator 3 days ago

    Or you could use Fil-C++ and get memory safety without any changes. Unlike this proposal, fil-C++ can run real C and C++ programs safely today (including interesting stuff like CPython, OpenSSH, and SQLite).

    I don’t buy that adding an extension that is safe if you use it will move the needle. But making the language safe wholesale is practical. We should do that instead.

    • felipefar 3 days ago

      Hard pass on the garbage collector. We don't need that, and the minimal GC support that was in the standard has been removed from C++23.

      • pizlonator 3 days ago

        > Hard pass on the garbage collector.

        Why?

        > We don't need that

        You do if you want comprehensive use-after-free protection.

        > and the minimal GC support that was in the standard has been removed from C++23.

        Not related to what I'm doing. The support you cite is for users of the language to write garbage collectors "on top" of the language. Fil-C++'s garbage collector is hidden in the implementation's guts, "below" the language. Fil-C++ is compliant to C++ whether C++ says that GC is allowed or not.

        • jandrewrogers 3 days ago

          Garbage collectors are directly in conflict the requirements of many high-performance software architectures. Some important types of optimization become ineffective. Also, GC overhead remains unacceptably high for many applications; performance-sensitive applications worry about context-switching overhead, and a GC is orders of magnitude worse than that.

          C++ is usually used when people care about performance, and a GC interferes with that.

          • pjmlp 2 days ago

            PTC and Aicas have a good customer base that cares about performance, while selling real time Java implementations, implementations that happen to be used in military scenarios where lack of performance costs lifes on the wrong side.

          • pizlonator 3 days ago

            Fil-C uses a concurrent garbage collector that never pauses the program. There is no pause in Fil-C that looks anything like the cost of a context switch. It’s a design that is suitable for real time systems.

            The GC is similar to what I used here, just much more optimized: http://www.filpizlo.com/papers/pizlo-eurosys2010-fijivm.pdf

            • jandrewrogers 3 days ago

              The GC must interrupt the software because otherwise it would have no resources with which to execute. If I am running a standard thread-per-core architecture under full load with tightly scheduled CPU cache locality for maximum throughput, where do you hide the GC and how do you keep it from polluting the CPU cache or creating unnecessary cache coherency traffic? People have made similar claims about Java GCs for years but performance has never been particularly close, which is generally in agreement with what you would expect from theory. A GC will always lack context that the software has.

              • pizlonator 3 days ago

                Malloc has its own overheads, and they are often worse than those created by GC.

                Something to consider is that Fil-C permits you to do all of the things you would normally do, as a C programmer, to reduce or eliminate allocation - like having structs embedded in structs and avoidance of allocation in core algorithms.

                This makes it quite different from Java or JS where the language forces you to allocate to basically do anything. I think folks conflate “GC overhead” with the overhead of languages that happen to use GC.

                • jandrewrogers 2 days ago

                  > I think folks conflate “GC overhead” with the overhead of languages that happen to use GC.

                  That is fair to a point. Some GC languages (like Java) are significantly more inefficient than they need to be regardless of the GC.

                  Nonetheless, for performance C++ code you don’t see much malloc anywhere, directly or indirectly, so the efficiency doesn’t matter that much. That’s pretty idiomatic. I think this is the real point. As a C++ programmer, if you are not serious about eliminating dynamic allocation then you aren’t serious about performance. Since C++ is used for performance, you don’t see much dynamic allocation that a GC could theoretically help with. Most objects in the hot path have explicitly and carefully managed lifetimes for performance.

                  I use GC languages for quite a few things, but it is always for things where performance doesn’t matter. When performance matters, I’ve always been able to beat a GC for performance, and I’ve done my fair share of performance engineering in GC languages.

                  • pizlonator 2 days ago

                    > Nonetheless, for performance C++ code you don’t see much malloc anywhere, directly or indirectly, so the efficiency doesn’t matter that much. That’s pretty idiomatic.

                    If you compile that code with Fil-C++, then you won't see much (or any) GC overhead. The GC kicks in for three things:

                    - malloc (this is the main client of the GC)

                    - stack allocations that aren't escape analyzed (these are rare, typically small, and generate minuscule GC load - it's common for code I've tested to have zero of these)

                    - Fil-C runtime internal allocations (these are super rare - for example if you call sigaction, pthread_create, setjmp, or dlopen then there's some GC object allocated behind the scenes - but you aren't going to be calling those a lot, and if you are then you've got bigger problems than GC).

                    I understand your perspective about GC languages. Most GC languages also come with other baggage, which makes it very difficult to avoid the GC. Most C++ code that avoids malloc ends up doing a bunch of `new`s when converted to Java. But C++ code that avoids malloc will avoid the GC if compiled with Fil-C++.

                  • eacnamn 2 days ago

                    You've lost me here a little, sorry. If you have little to no dynamic allocations, meaning all of your memory will be automatic stack memory, then memory management wouldn't be much of an issue to begin with. But the most common pattern to me seems to be memory that is allocated upfront, and then treated as-if it were automatic in a hot loop, so not reallocated or moved etc., and then deallocated after the hot part is over. How does GC interfere with these use-cases, because I'd imagine it would only kick in after, when you'd want to deallocate anyway, but do this automatically without you messing up.

                • neonsunset a day ago

                  At this point you could also use C# for vastly better user experience, that builds on top of compiler that is actually GC-aware and does not have a performance penalty, aside from being just weaker than GCC (although that is improving with each release);

                  • pizlonator a day ago

                    C# isn’t C though. The point of Fil-C is you can compile real C programs (like CPython or SQLite and many others) with it.

                    Fil-C’s compiler is GC aware.

        • felipefar 3 days ago

          They solve the use-after-free issue by keeping pointed objects alive, not by helping you think better about object lifetimes in your code. That means some objects will live for longer than you initially thought they would, and potentially even introduce circular references. Added to that, they also introduce random, unpredictable slowdowns in your application to run their algorithms.

          I'm not yet sold on Rust, but exploring alternatives for achieving memory safety without needing to put on a GC is commendable.

          • pizlonator 3 days ago

            > They solve the use-after-free issue by keeping pointed objects alive

            That's not at all what Fil-C's garbage collector does.

            If you free() an object in Fil-C, then the capability is marked free and the next GC cycle will:

            - Delete the object.

            - Repoint all capabilities that referred to that object to point to the free singleton capability instead.

            This ensures that:

            - Freeing an object really does free it, regardless of whether the GC sees it as reachable.

            - Using an object after freeing it is guaranteed to trap with a Fil-C panic, and that panic is guaranteed to report that the object has been freed so you know why you're trapping.

            Also, as a bonus, if you don't ever free your objects, then the Fil-C GC will delete them for you once they become unreachable. So, you can write Java-style code in C++, if you want.

            > That means some objects will live for longer than you initially thought they would, and potentially even introduce circular references.

            No idea what you mean there. Maybe you're thinking of reference counting, which isn't a form of garbage collection. (Reference counting is different precisely because it cannot handle cycles.)

            > unpredictable slowdowns in your application to run their algorithms.

            Fil-C's garbage collector is 100% concurrent. It'll never pause your shit.

            • pornel 2 days ago

              Fil-C sounds very similar to Google's MiraclePtr.

              However, Safe C++ (Circle) and Rust do much more than that. They are not limited to pointers on the heap, and the borrowing rules work for all references including the stack. They also work for references that need to be logically short even when the data is not freed, e.g. internal references to data protected by a mutex don't outlive unlocking of the mutex. And all of that is at zero runtime cost, and by guaranteeing the code correctly doesn't create dangling references in the first place, not merely by softening the blow of run-time failures of buggy code.

              • pizlonator 2 days ago

                Fil-C is nothing like MiraclePtr. Fil-C gives you comprehensive memory safety.

                Yes, it handles references to the stack. Misuse traps or leads to other safe outcomes.

                Fil-C makes it so races have memory safe outcomes (like Java).

                Circle and Rust are strictly less safe than Fil-C, since both have unsafe escape hatches. Fil-C doesn't even have an unsafe escape hatch.

                • pornel 2 days ago

                  Oh, I remember this project now. I see it still advertises 3x-10x overhead. To me this takes it out of being a contender in the systems programming space.

                  This can't be dismissed as a mere quality-of-implementation detail. C and C++ are used because they don't have such overheads, so it takes away the primary reason to use these languages. When non-negligible overhead is not a problem, there are plenty of nicer languages to choose from for writing new code.

                  This leaves Fil-C in a role of a sandbox for legacy code, when there's some unsafe C code that won't be rewritten, but still needs to be contained at any cost. But here you need to compete with WASM and RLBox which have lower-overhead implementations.

                  • pizlonator 2 days ago

                    Fil-C was 200x slower when I started and the latest overheads are lower than 2x in a lot of cases. It’s getting faster every month, though I don’t always update the docs to say the latest numbers (because I’m too busy actually making it faster).

                    I think the reason why folks end up using C is often because they have a gigantic amount of C code and for those folks, Fil-C could easily be good enough as is.

                    But dismissing it as a contender because the perf isn’t there today even as it’s getting steadily faster (on a super aggressive trajectory) is a bit unfair, I think.

                    • pornel 2 days ago

                      The success of this project is going to be very non-linear with speed, so it really hangs on where your speed improvements will plateau.

                      If you get below 2x, you can compete with WASM and ASAN. If you get it down to 1.1x-1.2x, you can compete with RLBox. If you get down to 1.05 you can call it software-emulated CHERI and kill the whole hardware line before it even comes out.

                      If you get it down to 1.01x, Rust will copy you, and then beat you by skipping checks on borrowed references ;)

                      • pizlonator 2 days ago

                        > If you get below 2x, you can compete with WASM and ASAN.

                        I'm at 1.5x for a lot of workloads already. I will be solidly below 2x for sure, once I implement all of the optimizations that are in my near-term plan.

                        Wasm and asan don't really give you memory safety. Wasm is a sandbox, but the code running within the sandbox can have memory safety bugs and those bugs can be exploited. Hackers are good at data-only attacks. Say you run a database in wasm - then a data-only attack will give the bad guy access to parts of the database they shouldn't have access to. Fil-C stops all that because Fil-C makes C memory safe rather than just sandboxing it.

                        Asan also isn't really memory safe; it just catches enough memory safety bugs to be a useful tool for finding them. But it can be sidestepped. Fil-C can't be sidestepped.

                        So, even if Fil-C was slower than wasm or asan, it would still be useful.

                        > If you get it down to 1.1x-1.2x, you can compete with RLBox.

                        RLBox is like wasm. It's a sandbox, not true memory safety. So, it's not a direct competitor.

                        That said, I think I'll probably land at about 1.2x overhead eventually.

                        > If you get down to 1.05 you can call it software-emulated CHERI and kill the whole hardware line before it even comes out.

                        I can already kill CHERI because Fil-C running on my X86-64 box is faster than anything running on any CHERI HW.

                        No, seriously.

                        The biggest problem with CHERI is that you need high volume production to achieve good perf in silicon, so a SW solution like Fil-C that is theoretically slower than a HW solution is going to be faster than that HW solution in practice, provided it runs on high volume silicon (Fil-C does).

                        I think I'm already there, honestly. If you wanted to run CHERI today, you'd be doing it in QEMU or some dorky and slow IoT thing. Either way, slower than what Fil-C gives you right now.

                        > If you get it down to 1.01x, Rust will copy you, and then beat you by skipping checks on borrowed references ;)

                        Rust is a different animal. Rust is highly restrictive due to its ownership model, in a way that Fil-C isn't. Plus, there's billions of lines of C code that ain't going to be rewritten in Rust, possibly ever. So, I don't have to be as fast as Rust.

                        I don't think there is going to be much copying going on between Rust and Fil-C, because the way that the two languages achieve memory safety is so different. Rust is using static techniques to prevent bad programs from compiling, while Fil-C allows pretty much any C code to compile and uses static techniques to emit only the minimal set of checks.

                      • neonsunset a day ago

                        1.05-1.7x is where C# places vs C. Except you also have an actual type system, rich set of tools to diagnose performance and memory issues and ability to mix and match memory management styles. It is rudimentary when compared to borrow checking and deterministic drop in Rust, but in the year of 2024 almost every language with low-level capabilities is an upgrade over C if it can be used in a particular environment.

        • 3836293648 3 days ago

          Because if you can afford GC you're not using C/++. We need memory safe systems stuff. Higher level memory safety has been solved for decades

          • pizlonator 3 days ago

            I don’t buy that at all.

            If I could use C++ with GC, I would - but I can’t because other than Fil-C++ no GC works soundly with the full C++ language, and those that work at all tend to be unreasonably slow and flaky due to conservatism-in-the-heap (Boehm, I’m looking at you). Reason: GC and memory safety are tightly coupled. GC makes memory safety easy, but GC also requires memory safety to be sound and performant.

            So there isn’t anything else out there quite like Fil-C++. Only Fil-C++ gives you accurate high perf GC and the full C++ language.

            Finally, “affording” GC isn’t really a thing. GC performs well when compared head to head against malloc. It’s a time-memory trade off (GC tends to use more memory but also tends to be faster). If you want to convince yourself, just try a simple test where you allocate objects in a loop. Even JS beats C++ at that benchmark, because GCs support higher allocation rates (just make sure to make both the C++ and the JS version complex enough to not trigger either compiler’s escape analysis, since then you’re not measuring the allocator).

            • 3836293648 2 days ago

              "affording" GC is absolutely a thing. You're measuring the wrong thing. It's primarily about latency, not throughput, and GC can only go head-to-head on throughput.

              Secondly you have places where you don't have dynamic memory at all which you're also conveniently ignoring.

              • pizlonator 2 days ago

                Fil-C has a concurrent GC. It doesn’t stop your program, ever.

                If your C code doesn’t dynamically allocate then it won’t create any GC load in Fil-C.

                • riku_iki 2 days ago

                  > Fil-C has a concurrent GC. It doesn’t stop your program, ever.

                  what about performance/throughput compared to when you allocate stuff on stack?

      • pjmlp 2 days ago

        Unreal C++, C++/CLI, and V8 C++ do need one.

        It should never have been there in first place, because it ignored their requirements, and thus it was never adopted by them or anyone else.

    • elliotpotts 3 days ago

      Wow, Fil-C++ looks very interesting! I wonder what % of programs make its pointer tracking fail due to stuffing things in the higher bits, doing integer conversions and so on. It reminds me of CHERI.

      • pizlonator 3 days ago

        You can put stuff in the high and low bits of pointers in Fil-C so long as you clear them before access, otherwise the access itself traps.

        Python does shit like that and it works fine in Fil-C, though I needed to make some minor adjustments a like a 50KB ish patch to CPython.

    • gmueckl 3 days ago

      From the github README:

      > On the other hand, Fil-C is quite slow. It's ~10x slower than legacy C right now (ranging from 3x slower in the best case, xz decompress, to 20x in the worst case, CPython).

      That performance loss is severe and makes the approaches totally uninteresting for a most serious use cases. Most applications written in C or C++ don't get to waste that many cycles.

      • pizlonator 3 days ago

        Those are old perf numbers. It’s sub-2x most of the time now, and I’m working on optimizations to make it even faster.

        Note that at the start of this year it was 200x slower. I land speed ups all the time but don’t always update the readme every time I land an optimization. Perf is the main focus of my work on Fil-C right now.

        • Krutonium 3 days ago

          Might I suggest having the CI benchmark it and then update the readme?

          • pizlonator 3 days ago

            If I had the time to set that up then yeah.

            Right now I’m spending all my time actually implementing optimizations and measuring them locally. I did spend the time to give myself a good benchmark suite with a nice harness (I call it PizBench, it includes workloads from xzutils, simdutf, Python, OpenSSL, and others).

    • Svoka 3 days ago

      sorry, what's Fil-C++?

  • gdiamos 3 days ago

    Glad to see Sean Baxter is working on this

  • rfmi 2 days ago

    The concept of a plan compared with a working language A long time C++ programmer that went rusty

  • 29athrowaway 3 days ago

    So everything is unsafe by default, until you turn it on. Great...

    • quotemstr 3 days ago

      Yes, but there's hope. Making safety opt-in is a necessary prerequisite of backwards compatibility. Enforcing the use of safety annotations, however, is something that linters can enforce, and every major C++ codebase uses one form of another of supererogatory checking. By enforcing safety via linter, we've transitioned a robustness problem to an ergonomic one. Specifying "safe" over and over is hideous, aesthetically.

      I think C++ needs a broader "resyntaxing" --- something like what Elixer is to Erlang and Reason is to OCaml. Such a resyntaxing wouldn't change language semantics, but it would allow us to adopt new defaults --- ones that benefit from decades of hindsight. A C++ Elixer wouldn't only mark functions safe by default, but would probably make variables const by default too. And it would be 100% compatible with today's C++, since it would be the same language, merely spelled differently.

    • pizlonator 3 days ago

      Weird this got downvoted since this is a big deal.

      The default matters. So long as the language makes it easy to write unsafe code, people will do it and there will be security bugs.

      • 112233 2 days ago

        Usability trumps safety every day.

        You can upgrade your compiler to version A. It produces safe code. It also produces 500kB of error messages for each of your source files. You will not ship any new builds until you fix all of them.

        Or you can pick version B. It compiles all your code with only few errors. It also supports opting-in to multiple safety features.

        If your salary depends on your software project, which will you pick?

        • croes 2 days ago

          > Usability trumps safety every day

          That‘s the reason for all the problems in the first place.

      • kreetx 2 days ago

        I think the reason is that you don't want existing codebases to start erroring with the default setting.

      • 29athrowaway 3 days ago

        Safety should be opt-out not opt-in.

        But this would break backwards compatibility and the C++people do not want that.

        • pizlonator 3 days ago

          Fil-C/C++ is backwards compatible and has mandatory safety.

      • blastonico 3 days ago

        There will always be security bugs. Why do you think otherwise?

        • pizlonator 3 days ago

          Yes but the point of safe languages is to eliminate the memory safety security bugs, which tend to be the worst and the most common.

          An unsafe-by-default language that has some safety features isn't enough to fix memory safety.

          • uecker 2 days ago

            I don't they are the worst (often hard or impossible to exploit) and also not the most common according to: https://www.cvedetails.com/vulnerabilities-by-types.php (despite likely easy to find).

            Memory safety is something which can be solved though, which is what memory safe languages do. Of course, Rust is only an incremental improvement because of unsafe.

            • steveklabnik 2 days ago

              The claim was scoped to the organizations that made the claim: Mozilla, Google, and Microsoft independently claimed that around 70% of their security issues were memory safety related, not that every CVE ever filed was that weighted towards one vulnerability class.

          • 3 days ago
            [deleted]
        • 29athrowaway 3 days ago

          There are bad ideas with known consequences. We know what happens if you aim at your foot with a shotgun and then fire, for example.

          Unless you are doing something obscure like testing shotgun-proof footwear, it is a good idea to have an automated system tell you: "ERROR: You are shooting your foot, do not aim at your foot with a shotgun and fire on line 123".

          The C and C++ philosophy starts with the premise "programmers know best and will never aim a shotgun at their foot and then fire, that makes no sense haha, why are you talking to me about that? you are silly!". Yet, there are millions of people getting shot in the foot every year and it took a new language to demonstrate that the entire class of nonsense can be prevented.

          • JohnFen 3 days ago

            > The C and C++ philosophy starts with the premise "programmers know best and will never aim a shotgun at their foot and then fire

            That's absolutely not the C/C++ philosophy. The C philosophy (I lost track of whatever the C++ philosophy is supposed to be several versions back) is that it's a mid-level language and lets you have pretty low-level access to the machine. With that comes the need for great caution, much like working in assembly.

            It's not premised on the idea that programmers are infallible at all, but on the idea that code-checking and correctness is to be done through process rather than the language itself.

            • eacnamn 2 days ago

              >That's absolutely not the C/C++ philosophy

              while it is not explicitly written out like this anymore, the standard charter used to say

              > Trust the programmer.

              > Don't prevent the programmer from doing what needs to be done.

              for 30 something years, which sounds similar to what the other poster said.

              The current charter[1] only says "Allow programming freedom" which I'd still interpret to mean something similar.

              1: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3280.htm

              (edited due to formatting messups)

              • 29athrowaway 21 hours ago

                Some freedom is productive, some freedom is not.

            • vacuity 3 days ago

              It isn't what is explicitly touted as standard, but it is what many developers have culturally established.

  • ChrisArchitect 3 days ago

    Earlier discussion in September: https://news.ycombinator.com/item?id=41528124

  • worik 3 days ago

    It is not just memory safety, it is thread safety to.

    When I was working with Swift that was not available. Swift wraps "fork" in multi syllable function names and half a dozen variations. But it is (was?) still just fork.

    I do not know about the other languages mentioned.

    Rust shines on that front. It takes a bit of getting used to, but once you are it is awesome.

    I loved C++ back in the day. I have left it there, where it belongs. It was a fantastically successful experiment, but move on now.

  • omoikane 3 days ago

    See also:

    https://news.ycombinator.com/item?id=41528124 - Safe C++ (2024-09-13, 68 comments)

  • bioneuralnet 3 days ago

    "Rust lacks function overloading, templates, inheritance and exceptions,"

    ...sounds good to me!

    • stackghost 3 days ago

      Exceptions are great. There's an argument to be made that one should handle errors where they occur, but that's often not desirable or even possible.

      If I call into a library and something goes awry I don't want to have to care about the inner workings of that library. Most of the time it's sufficient for me to try/catch that call and if it fails I'll handle it as gracefully as possible at my level.

      • kibwen 3 days ago

        The workflow you're describing is also how it works in languages without resumable exceptions, except that you're forced to acknowledge when a function call is capable of producing an error. Whether you want to ignore the error, handle the error, or propagate the error, it's all the same; you're just required to be explicit about which approach you're taking (as opposed to exceptions, where the the implicit default is to propagate).

        • vacuity 3 days ago

          Indeed. While it is painful for the people who know they have a simpler architecture, making errors and other cross-cutting effects explicit is necessary at some point. It's essential complexity that shouldn't be hidden; it should be addressed from the get-go. Although the industry largely has the wrong incentives and discourages robust, comprehensible programs.

          • elcritch 2 days ago

            My belief is that errors should be handled using effect systems. So in the signature but not muddying the actual return types.

            Useful effect systems allow the end user to decide where and when to have the compiler enforce errors are handled. Nim has had an effect system for a while but became much more useful when `forbids: [IOError]` was added. It makes it easy to ensure certain type of errors are handled at specific points.

            More languages should embrace effect systems. Ocaml's is even used to implement multithreading support, albeit effect systems vary widely in design and theory.

            • consteval 2 days ago

              I mean this is how a lot of exceptions are handled, even in C++. You can use noexcept and whatnot and you don't have to change types and propagate them out. In Rust, you do. Java has maybe the strongest system because not only do you have to declare what can throw but it checks it at compile-time. That's, to me, a full featured effect system.

              But errors-as-values are all the rage today. But modifying types, especially every type in the chain, is annoying and overly manual IMO.

              • elcritch 2 days ago

                Yeah, Java had some good ideas but just not quite there on the UX, like many things with Java sigh.

                Checked exceptions were annoying because you had to manually annotate the exceptions all the way up your chain. The list of effects should be generated by the compiler, and the IDE should show them when desired. Maybe manually annotated at external API boundaries which double as forcing the API dev to handle unlisted exceptions.

        • soulbadguy 3 days ago

          > except that you're forced to acknowledge when a function call is capable of producing an error

          Acknowledging the error is handling the error even if partially. The point of exceptions is to only acknowledge error that one can/knows how to properly handle

          • kibwen 3 days ago

            > The point of exceptions is to only acknowledge error that one can/knows how to properly handle

            In Rust this takes a single character. There's effectively no cost to having the programmer acknowledge the error, and there's a large benefit in that you now know that there's no such thing as an error that the programmer ought to have handled but was simply unaware of. That's a huge benefit for writing resilient software.

            • elcritch 2 days ago

              It's one character in the best case. In the worse case you need to convert the error types to your error type which just re-wraps or replicates the upstream error type. Then repeat this for every library type you use. Zigs error design seems saner in this aspect at least. Rust, IMHO, just makes errors require lots of unnecessary manual labor instead of being smart about it.

              Alternatively everything just gets put into a `dyn trait` and you're effectively just bubbling up errors just like with exceptions, but with way more programmer overhead. The performance overhead of constantly doing if/else branches for errors adds up as well in some situations.

              Of course a fair bit of Rust code just uses `unwrap` to deal with inconvenient errors.

        • binary132 3 days ago

          One thing I’ve wondered about is, isn’t the cost of checking for the failure case in the good case all the time actually worse (even if only slightly) than the cost of not throwing, which is nothing?

          • vacuity 3 days ago

            There's indeed a predictably present cost for checking for failure all the time. Exceptions, depending on the implementation, often do come with runtime overhead too. If the determining factor is a slight performance gain of exceptions over ubiquitous checking, that would be an exceptional (ha) case. I daresay there are almost always other more salient factors, if harder to rearchitect around.

        • otabdeveloper4 2 days ago

          > you're forced to acknowledge when a function call is capable of producing an error

          All functions are capable of producing an unbounded set of errors.

          (Yes, programming is hard.)

        • stackghost 3 days ago

          >you're just required to be explicit about which approach you're taking

          Yes and I'm opposed to such "if err != nil" boilerplate.

          • worik 3 days ago

            > Yes and I'm opposed to such "if err != nil" boilerplate.

            That is not what Rust boilerplate looks like.

            • stackghost 3 days ago

              I'm aware. One of the criticisms often leveled against Go is that it's needlessly verbose when handling errors, which is why I chose that example.

          • 6equj5 3 days ago

            And you're not opposed to try-catch boilerplate?

            • stackghost 3 days ago

              Boilerplate is code you have to write almost as a pro forma thing. If (in go lang, to continue my example) you're just going to keep copy pasting the same if statement to return `err` up to some higher caller, then why write all those lines when at the top level a single try/catch can remove potentially dozens of lines of code?

      • 112233 2 days ago

        Exceptions in C++ are the closest we have to the implementation of the COME FROM proposed in "A Linguistic Contribution to GOTO-less programming".

        It takes statically typed C++ and turns it into dynamically typed language. Throwspec is dead, anything can throw, except when noexcept, then nothing can throw.

        It is next to impossible to reason about control flow. Dynamic linking opens whole new dimension of this wormcan. Do you handle exceptions in your constructors? How about constructors of your function arguments?

        Not to mention the non-trivial cost in code size, that makes exceptions a non-option for embedded use.

        • jacobp100 2 days ago

          Noexcept is the biggest scam. It basically wraps every call to a noexcept function in a try/catch, and calls terminate in the catch. Actively harmful for performance

          • favorited 2 days ago

            [[noexcept]] was never intended to be a standalone performance boost for arbitrary functions – it was proposed & accepted so container types could make the "strong guarantee" (of the so-called Abrahams guarantees): operations can fail, but failed operations have no side-effects.

            It allows, for example, std::vector's resize operation to move its contents rather than copying them, iff its element's move constructor is [[noexcept]]. If the move constructor could throw, the items must be copied so the original buffer is unchanged until the entire copy transaction is complete.

            std::move_if_noexcept <https://en.cppreference.com/w/cpp/utility/move_if_noexcept>

            "N3050: Allowing Move Constructors to Throw (Rev. 1)" (2010) <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n30...>

            "P2861R0: The Lakos Rule – Narrow Contracts and noexcept Are Inherently Incompatible" (2023) <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p28...>

            "P2946R1: A Flexible Solution to the Problems of noexcept" (2024) <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p29...>

          • consteval 2 days ago

            My understanding of exceptions implementation in C++ is that there's zero performance cost if an exception isn't thrown. Try... catch isn't implemented as a branch, I believe.

            • 112233 a day ago

              There indeed is zero perfomance cost. Code size cost, however, can be astonishing.

    • blastonico 3 days ago

      A proper OO support makes difference for some use-cases. That Serenity OS guy is building a web browser and recently spoke about it. Game developers also complain about the lack if it in Rust.

      • vacuity 3 days ago

        As is typical in Rust-land, lots of talk but implmentations (let alone remotely complete ones) are harder to come by. Partly a procedural and social issue, not just technical queries, which is disappointing.

        At least delegation (IIUC closer to concatenation as defined in [1]) is currently being implemented, but I can't say how much weight it can actually carry for the OOP efforts.

        [1] "On the Notion of Inheritance": https://users.csc.calpoly.edu/~gfisher/classes/530/handouts/...

    • saghm 2 days ago

      I had the same reaction when reading that part, but it's worth noting that the article used this quote in the context of migration from C++ codebases to Rust, not as a critique of Rust in a vacuum. The next part of the quote clarifies this:

      > These discrepancies are responsible for an impedance mismatch when interfacing the two languages. Most code generators for inter-language bindings aren’t able to represent features of one language in terms of the features of another.

    • Sytten 3 days ago

      Rust has problems but certainly not those for sure!

      • jampekka 3 days ago

        Rust unwraps many such problems.

    • bfrog 3 days ago

      Those are all features of rust not bugs.

      Function overloads are evil evil evil. Requiring some mental gymnastics by the reader to pretend to be the compiler what function is actually called. That sucks.

      • nneonneo 3 days ago

        Rust does support a form of overloading via custom traits. It’s true that you can’t overload e.g. different numbers of arguments (and the lack of default/keyword arguments is especially annoying here), but you can overload a function by having it be generic over a trait argument and then implementing that trait for each overloaded type.

        • tialaramex 3 days ago

          I would argue that although this is mechanically equivalent it encourages a much healthier design approach.

          Take Pattern. One way to look at Pattern is to say that this way we can provide the ad hoc polymorphism of overloading, for functions like str::contains or str::split or str::trim_end_matches -- but as a trait we can see that actually Pattern has discernible semantic properties, clearly a compiled regular expression could be a Pattern for example (and with some feature flags that's exactly correct)

          In contrast in C++ there are often functions which use/ abuse overloading to deliver separate features in the same interface, expecting that you'll carefully read the documentation and use the correct feature by passing the right type and number of parameters. Constructors are the worst for this, Rust's Vec::with_capacity gets you a growable array with a certain capacity already allocated ready for use -- C++ does not have such a thing - you must make a std::vector and then separately reserve enough space, but it looks like it might have this feature in its constructor as an overload because the constructor has an overload which is the right shape - however that's actually a very different feature, it will fill the std::vector with default initialized objects, rather than reserving capacity for such objects.

        • bfrog 3 days ago

          This is generics.

          Calling x.some_func() in rust means there is either a type specific function or an impl trait. If more than one option is there rust requires more explicitly calling the types function with x as a parameter.

          E.g.

          Something::some_func(x)

          I’ve never read rust code where I’m entirely guessing which overloaded function is being called. C++ has an entire set of overload resolution rules around this! https://en.cppreference.com/w/cpp/language/overload_resoluti...

          • rty32 2 days ago

            Reminds me of the trivia given by the instructor when we had a training on "C++ STL" -- guess which method will be called! (Yes, bringing in classes and virtual functions makes it extra fun)

            Having learned Go and Rust in the last two years, it occurs to me that -- if this can be made into a trivia, and considering all the things in C++ like friend class, nested class, public/protected/private inheritance matrix and all the "possibilities" out there, it seems this language is seriously f*cked up.

    • _aavaa_ 3 days ago

      Won't somebody please think of the children?

    • kjrfghslkdjfl 2 days ago

      [dead]

  • aw1621107 3 days ago

    Submitted earlier at https://news.ycombinator.com/item?id=41566507 but seems it didn't get traction at the time (11 points, 2 comments)