← Back to context

Comment by jandrewrogers

6 months ago

This assumes you are writing C++ in the most naive way possible. I’m sure some people do that but nothing requires it. The capabilities of a language are not defined by its worst programmers.

Modern C++ allows you to swap out most features and behaviors of the language with your own implementations that make different guarantees. C++ is commonly used in high-assurance environments with extremely high performance requirements, and it remains the most effective language for these purposes because you can completely replace most of the language with something that makes the safety guarantees you require. This is rather important. For example, userspace DMA is idiomatic in e.g. high-performance databases kernels; handling this is much safer in C++ than Rust. In C++, you can trivially write elegant primitives that completely hide the unusual safety model. In Rust, you have to write a lot of ugly unsafe code to make this work at all because userspace DMA isn’t compatible with a borrow checker. There can always be multiple mutable references to memory but it is not knowable at compile-time, safety of an operation can only be arbitrated at runtime.

Of course, it is still incumbent on the developer to use the language competently in all cases.

    The capabilities of a language are not defined by its worst programmers.

Is the implication here that Bjarne is a bad C++ developer? If the person in charge of the EWG fails "to use the language competently in all cases", what hope is there for the rest of us mere mortals?

For what it's worth, unsafe Rust is safer than C++. There's very little UB to explode your carefully crafted implementations. Safe rust of course has no UB except for what you write in unsafe blocks, so it's safer still and there's no real difference in the abstractions you can write with concepts vs traits.

I'm not actually arguing for rust here though, because this isn't a great showing for it. Trying to write the related add_wrap(T, T) function in rust is stupidly verbose compared to add_sat(T, T) thanks to bad decisions the num_traits authors made. What I am saying is C++ isn't a form of high level assembly like your original comment suggested. Understanding the relationship between the language and the hardware takes a lot of experience that most people don't use when writing code.

  • UB is a feature of the standard, not the implementation. Many of those behaviors can be defined. Modern C++ conveniently allows you to replace many of the bits that have UB, per standard, with your own bits with defined behavior with zero overhead. This was not always the case. You aren’t dependent on the compiler implementor. The ability to consistently do this transparently became practical around C++17 IMO. The C++ standard library is in many regards obsolete and many orgs treat it that way.

    I never suggested that C++ was “a form of high level assembly”. I’ve written enough assembly and C to know better; you lose a bit of precision with C++. But now I can define (or not) the behavior I want in a way that is largely transparent. This has been a brilliant change to the language.

    If you have a foundational library that makes different and/or explicit guarantees than std, it is pretty easy to police that in a code base with automation. Everyone doing high-performance and/or high-assurance systems is dragging in few if any dependencies, so this is practical. The kinds of things that C++ is really good at for new code are the kinds of things where this is what you would do regardless.

    Developers don’t even have to be hardware experts, they just have to not use std for most things. That is a pretty low barrier. And std is a mess with the albatross of legacy support. Reimagined C++20 native “standard” libraries are much, much cleaner and safer (and faster).

    Legacy C++ code bases aren’t going to be rewritten in a new language. New C++ code bases can take advantage of alternative foundations that ignore std and many do. Most things should not be written in C++, but for some things C++ is unmatched currently and safer in practice than is often suggested with basic hygiene.

    •     Modern C++ conveniently allows you to replace many of the bits that have UB, per standard, with your own bits with defined behavior with zero overhead.
      

      Okay, let's continue the example. Please demonstrate how to replace the addition operator on a primitive type. You can't within the confines of the language and that's a good thing in most cases. What you can do is pass -fwrapv, except that MSVC doesn't officially define a comparable flag.

          Developers don’t even have to be hardware experts, they just have to not use std for most things.
      

      Signed overflow isn't a problem with std, the solution to it is in std. Null pointers aren't a problem with std, but the recommended fixes are again in std. Etc.

          If you have a foundational library that makes different and/or explicit guarantees than std, it is pretty easy to police that in a code base with automation.
      

      As far as I'm aware, neither folly, absl, nor boost define custom integral types with defined overflow behavior. Please provide examples of anyone doing that.

          UB is a feature of the standard, not the implementation. 
      

      If you're writing "high assurance code", surely you're writing to the standard and not the implementation? The implementation's guarantees change with every upgrade, every new flag, and each time you build for different targets. I certainly try to avoid compiler assumptions as someone who writes safety critical code.

DMA being a problem appears to be mostly a problem with a lack of identification of the data. If the shape of the data could be verified by the language runtime, instead of being an arbitrary stream of bytes whose meaning must be known by the recipient without any negotiation, this form of unsafety would disappear, since the receiving code simply needs to assert the schema, which could be as simple as checking a 32 bit integer.

Then all you need to do is also verify that the sending code adheres to the schema it specified.

This has very little to do with borrow checking. From the perspective of the borrow checker, a DMA call is no different from RPC or writing to a very wide pointer.