pizlonator 8 hours 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 7 hours 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.

    • pjmlp an hour 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.

    • pizlonator 7 hours 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.

      • felipefar 7 hours 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 7 hours 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.

      • jandrewrogers 7 hours 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 an hour 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 6 hours 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 6 hours 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 5 hours 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 an hour 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.

      • 3836293648 6 hours 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 6 hours 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).

  • gmueckl 7 hours 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 6 hours 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 6 hours ago

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

        • pizlonator 5 hours 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).

  • elliotpotts 6 hours 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 6 hours 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.

gdiamos 8 hours ago

Glad to see Sean Baxter is working on this

29athrowaway 8 hours ago

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

  • quotemstr 7 hours 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 8 hours 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 23 minutes 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?

    • 29athrowaway 7 hours ago

      Safety should be opt-out not opt-in.

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

      • pizlonator 5 hours ago

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

    • blastonico 7 hours ago

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

      • pizlonator 7 hours 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.

      • 29athrowaway 7 hours 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 7 hours 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.

          • vacuity 6 hours ago

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

worik 8 hours 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.

bioneuralnet 9 hours ago

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

...sounds good to me!

  • stackghost 9 hours 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 9 hours 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 8 hours 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.

      • soulbadguy 8 hours 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 4 hours 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.

      • binary132 6 hours 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 6 hours 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.

      • stackghost 8 hours 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.

        • 6equj5 8 hours ago

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

          • stackghost 7 hours 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?

        • worik 8 hours ago

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

          That is not what Rust boilerplate looks like.

          • stackghost 7 hours 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.

  • blastonico 7 hours 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 6 hours 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/...

  • Sytten 9 hours ago

    Rust has problems but certainly not those for sure!

    • jampekka 8 hours ago

      Rust unwraps many such problems.

  • bfrog 8 hours 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 8 hours 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 6 hours 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 5 hours 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...

  • _aavaa_ 9 hours ago

    Won't somebody please think of the children?