35 comments

[ 30.3 ms ] story [ 195 ms ] thread
C++ needs spatial memory safety. It can be added without greatly changing the semantics of the language. Temporal memory safety is much harder.
(comment deleted)
Libc++ for example is doing just that, it was on HN a while back and generated many many complaints.

It doesn’t help though that there’s no way to distinguish a pointer from an array, nor get the length of an array (because you aren’t guaranteed an array is a c++ array with a prefix header or a c style array with a separate size), and given a lot of c++ is still pointers - for example the semantics of iterators makes it hard to be both bounds safe and fast, which means c++ iterators can easily go out of bounds or into random memory in the face of mutation. Now obviously we generally agree mutating something while iterating it is bad, but the c++ iterators semantics makes detecting and stopping that extremely expensive because it’s fundamentally designed around reusing old code that was just incrementing pointers.

I’m not sure what you mean by “there’s no way to distinguish a pointer from an array”. Arrays and pointers in C and C++ are distinct types, although arrays will decay to a pointer in an expression context (and there is the whole problem with arrays as function parameters being rewritten to pointers which adds to the confusion).

C with VLAs can actually express the length of a pointed to array, with the following example:

    #include <stdio.h>
    // See the linux kernel for a version that verifies
    // `x` is actually an array.
    #define arrlen(x) (sizeof(x)/sizeof(x[0]))
    void foo(const int x, int(*y)[x]){
        printf("%zu\n", arrlen(*y));
    }

    int main(){
        int a[3];
        foo(arrlen(a), &a);
        return 0;
    }
If you compile and run the above code, you will see that it correctly prints the length of the array - 3. Unfortunately, the syntax is awkward and most C/C++ programmers would be stumped by the actual syntax for a pointer to an array. There is also no help that the correct length is passed.
First up, this is just invalid in C++, because C++'s type system has a bunch of the foot guns removed from it.

In C this will probably work, but only because VLAs in C are broken by design - VLAs are not part of the type system, which means there is no actual required relationship between the size of the array - even when statically known as in your example - and the parameters to the function. So yes your example will print 3, but that's because it will print whatever you pass in for x. So I can equally do:

   foo(500, &a);
and that's valid, and I would put money on not producing even a warning. Again because VLAs aren't part of the type system I could also do:

   foo(5, malloc(1));
and that should work fine as well, because VLAs are again not real.

Moreover this is also kind of missing the point: I said there is no way to distinguish a pointer from an array allocation, and your example is not using a pointer or an array, it is using a pointer and an additional length value - and yes, the correct thing to do is to have a struct containing both of those, or probably better a struct with trailing storage. Either way the C or C++ type system does not distinguish between a pointer to an object, or a pointer to an array of N (including potentially 1) objects, and you, the programmer need to do this yourself without language support.

>> While there might be 30+ years of divergence between C and C++, none of C++’s so-called “progress” involved removing memory-unsafe C features from C++, many of which are still in common use, and many of which still make memory safety in C++ near intractible

I think just the concept of classes, and new, help with memory safety. It's harder to screw with memory with new Foo() then doing a malloc and memcpying bits into a buffer. Sure it's far cry from real memory safety, but i do think c++ has a bunch of improvements over c in memory safety. Sure you can do all the nasty bits in c++ that you could do in c, but you are carrotted out of doing them most of the time.

Alas it doesn’t really help - new is just as bad as malloc, and the fact that there is no difference syntactically or in the type system between a pointer to an object and a pointer to an array means that c++s new introduces the joy of calling the wrong delete \o/
This is incorrect, but most C and C++ programmers would be stumped by `int(*y)[3]` (which is how you spell a pointer to an array of length 3).
Plenty of C++ developers still mix up delete versus delete[].
Which is my point: the type system doesn't distinguish T* (a single object) T* (an array of objects), so delete vs delete[] only shows up as runtime heap corruption that may or may not eagerly terminate or lead to RCE.
The type of `new T[N]` is T. The type of `new T[10]` is also T. The type of `new T` is also T.

Yes if you have a statically sized array you can make a pointer to an array of that length, but those are generally not relevant outside of specific cases (arrays in structs or on the stack where the size impacts offsets in codegen). More over the desire of C and C++ to drop those annotations is such that even using your proposal of a pointer to an array rather than just an array, is of questionable value. Take an extension of your example:

    int(*y)[3] = ...;
    int(*y2)[3] = ...;
You can't do

    *y2 = *y;
Because C doesn't let you assign arrays by value, even fixed size ones \o/
Super interesting article. Is there a word for the type of software architect who pathologically excuses immediate-term problems because there is a fix a long ways away? Dr. Stroustroup's comments sound familiar if you've ever encountered the type of person who won't engage with a problem you have now, because they have a system planned for next year that makes the problem go away.
I usually refer to this as "business oriented" - not in a derogatory sense, but because their design decisions are primarily determined by their financial impact, vs another measure (such as fault tolerance, error detection, system consistency, or projected support hours required).

A lot of business oriented people may find my delineation here offensive, but it really isn't meant to be. While a business' job is to make money, and employees work for the business, they may still disagree on how to evaluate a given action - because the business making money and being financially optimised may not be the main reason they work there.

> Besides the laughably condescending matter of calling Java (which first appeared in 1995), C# (first appeared in 2000), and Ruby (first appeared in 1995) “novel,”

I think "novel" means that they were Greenfield languages. They certainly took concepts from preexisting languages (like Java's "interfaces" being inspired by Objective-C's "protocols"), but they didn't start their lives as "existing-language-with-X," the way C++ was "C, with classes."

I just don't think this is a remotely justifiable reading.
If user(programmer)-behavior is the main problem, and there is claim to a safe C++ subset, then wouldn't it make sense to implement an UNSAFE compiler attribute macro, to enable the unsafe features, while turning them off in default compilation mode for new versions of C++?
There's no unsafe keyword, but most modern C++ people know what is intended to be safe and what isn't, i.e. unique pointers, raii, and bounds-checking accessors. It isn't going to be as strict as Rust, but you get a lot of benefits.

Knowing some Rust and how it works, then going back to C++ can help you recognize where to use safer idioms and what's potentially dangerous.

I know there is no unsafe keyword, but I was suggesting one should be added.

I am a bit rusty (no pun intended) at the moment, but I have decades-long experience with C++, but not all people do, and all people ,even experienced developers make mistakes or get lazy and take the easy way out from time to time.

I think one lesson C++ could take from rust, if nothing else, is that clearly demarking unsafe code sections is beneficial in and of itself.

If Strostrup makes the claim that developers using old C features is the main security problem of modern C++ , then an unsafe keyword would guide behavior towards safe usage.

    #No "old unsafe C" here

    UNSAFE{
    #Full C++, including "old unsafe C" here
      ...
    }

    #No "old unsafe C" here


    >c++                / treat code as "New Safe C++ only", except for UNSAFE sections
    >c++ --no-unsafe    / treat all code as "New safe C++ only"  UNSAFE not allowed
    >c++ --allow-unsafe / current behavior, allow all C++ including "old unsafe C" in all sections
You can still have rogue references to deleted objects in such a situation, or invalidated iterators, or plenty of other undefined behaviors even using just modern C++ features.
Stroustrup doesn't even mention Rust though and neither does the NSA document he was talking about: "Some examples of memory safe languages are C#, Go, Java, RubyTM, and Swift®."
(The document he refers to has two sections like this, one which mentions Rust and one which does not.)
OK but the post is styled a reply to Straustrup, who doesn't mention it.
Therefore, what, exactly?

Do you think Stroustrup is not aware of Rust? Do you seriously think he wasn't thinking about Rust when he wrote this? Even if he wasn't, don't you think he should've been? Do you not think the existence of Rust is relevant to the conversation?

Is this discourse bound by some lawyer-like rules where if someone doesn't bring up something that everyone knows is relevant, it's off the table to mention? Should I have filed a form with you asking for permission to bring up a topic not mentioned in the document I was responding to, even though everyone thinking through it with good faith knows it's on topic?

>Do you seriously think he wasn't thinking about Rust when he wrote this?

If he was, then why didn't he mention it? You seem to know what Stroustrup's inner thoughts are, so tell me, why didn't he mention it?

Maybe he wants to avoid the ten million reply guy Rust evangelists out there (ahem), maybe he doesn't think it's relevant to the discussion for other reasons. Please enlighten me.

Obviously, anyone can blog about anything they want to but likewise I can ask questions too. You seem very upset that I asked such a simple question.

You might have heard of this phenomenon called "click bait" where people inject themselves into only very tangential discussions to get clicks. Or, in this case, I suspect to preach to the choir.

Now, let me ask you: do you have an opinion about what Stroustrup wrote or are you just making sure no one says anything bad about [your_favorite_lang].

(comment deleted)
> If he was, then why didn't he mention it? You seem to know what Stroustrup's inner thoughts are, so tell me, why didn't he mention it?

As I say immediately afterwards, if he wasn't, he should've been. If Stroustrup thinks Rust is irrelevant to this conversation, he's simply mistaken. It's relevant for bringing memory safety into C++'s last remaining niche, after it's been chased out of most domains already.

> Now, let me ask you: do you have an opinion about what Stroustrup wrote or are you just making sure no one says anything bad about [your_favorite_lang].

Let me answer you: Most of my post still works if you replace Rust with Java. That's why I conclude that the time for Stroustrup to start talking about adding memory safety to C++ is 1995, before Rust was even thought of. If you had read and understood my post, you would've realized it has way more to do with what Stroustrup wrote than Rust in particular. Rust didn't invent memory safety, and Rust didn't make C++ behind the times for not pursuing it.

> You seem very upset that I asked such a simple question.

You didn't ask a question. You made an objection. You started out with a false claim that the NSA document didn't mention Rust, and then you implied that since Stroustrup's document didn't mention Rust, it was inappropriate for me to mention Rust. That's absurd, because Rust is extremely relevant to this conversation, for reasons I explain in the post and here. It's also infuriating, because you're holding me to a standard that sounds like it should be reasonable, but in fact isn't -- this ludicrous idea that replies should be limited strictly to the original topics, even if they can explain why a new topic is relevant, as I do with Rust. So I'm explaining why it's not reasonable, so that the casual reader doesn't mistakenly get the impression that you had a good point.

> very tangential discussions

I've explained (1) why Rust is relevant and (2) why my post would be mostly relevant even if Rust didn't exist, so it's not tangential.

> Or, in this case, I suspect to preach to the choir.

I don't see why this is a bad thing, really. Sometimes, the choir wants someone to explain elegantly to them what they already knew, and help them solidify their thought processes.

Is the opt-in versus opt-out point at the end of the article really important? Even if the compiler is opt-in you could use a separate tool to enforce preventing unsafe memory idiom usages unless annotated (akin to linters from other languages), and C++ codebases don't tend to have the same sprawling uses of third-party libraries that other languages do.
Yes? An ounce of prevention is worth a pound of cure.

See Yaml safe load vs load. Since default method is unsafe due to Yaml bugs there were wide ranging consequence across most of ecosystem.

If default was safe load, and you could opt-in to features of Yaml that caused unsafety it would be a much lesser problem.

Bjorn is probably a nice enough guy. But his opinions as a language designer are negated by the enormous harm done to the industry by C++.
Great article, no notes. I think you are correct about Stroustrup and want to amplify on that a little bit.

I will add that I'm in my mid-50s and have been programming since childhood and so I've had a chance to observe the evolution of C++ since the not-quite-C++ implementations such as those in THINK C (Object Extensions to C) and TCL (Think Class Library). I've used quite a few C++ frameworks and libraries that existed pre-STL including Borland's OWL 2.0 and the old Turbo Vision framework, as well as the first version of MFC (Microsoft Foundation Classes - I still have nightmares), and later worked extensively with the massively useful and very nicely designed Metrowerks PowerPlant class library. I used to subscribe to the old C++ Report magazine and read them cover-to-cover. The best of these articles are still available if you can track down old copies of the books _C++ Gems_ and _More C++ Gems_. (One of the fascinating things to be learned from those articles is how practitioners discovered that the exception mechanism in C++ really could only _theoretically_ be made to ensure safe and easy error-handling and recovery, but in practice made it impossible, because the total state of the program and its resources had to be considered at every point in the program in which an exception could occur; at one job, I tried unsuccessfully to convince my co-workers not to use exceptions for error-handling in embedded multi-threaded client/server code in vehicle infotainment devices.)

I have owned every edition of Stroustrup's _The C++ Programming Language_ and read his _The Design and Evolution of C++_. I used to buy, print out, and bind, copies of the C and C++ standards so that I could try to answer hard questions about whether compilers were broken, or the code was broken. If I had kept every book I've ever owned on C++ programming, I estimate it would be about 12 feet of shelf space. It is probably a blessing that I have only kept about 2 feet, which now has books on C++17 and C++20 for those times when I have to read some modern C++. At one point (about 1999) I considered myself to be a pretty expert C++ programmer, but because of language churn, those days are gone. (How many times can one re-learn the proper way to write a managed pointer class, or what methods should go into a "standard" kind of class to make it useful and safe? For me, the turning point probably came when I bought a brand new book on STL programming and tried to run the chapter 1 examples. Every compiler I had access to at the time on several platforms crashed when trying to compile the programs, which was a really vivid illustration of just how far the cutting edge was from the reality of working programmers.) I also once went to a talk by Stroustrup on the campus of the University of Michigan, and left frustrated and bored.

Given all that, I don't believe Stroustrup's ever been quite the thinker and visionary that some might think he is. He will not be remembered as a great language designer but more a formalizer, implementer, and promoter of ideas mainly borrowed from other languages and other people. He's certainly not a great writer or a great speaker, as I learned at his talk.

That itself is not my real criticism of him and his work; there are a lot of great ideas in language design to borrow from. My real criticism of him and his work is that along the way he has not been able to maintain a coherent vision of how language features would work together in real programs. To be fair to him, I think a lot of the people behind incredibly useful and active real-world computer languages such as Guido van Rossum and Yukihiro Matsumoto find themselves in similar positions, but Stroustrup seems to have a particular lack of self-awareness of the limits of his contributions and role in the design and evolution of a language which is now an irredeemable mess filled with too many Ph.D. theses. C++'s metastatic growth began with the early incorporation of Stepanov's Standard Temp...