back
98 comments
This works, although the downside compared to Rust is that soft pointer validity is checked at runtime, meaning that a program that compiles can still randomly fail at runtime and that performance is worse due to the checks.

The key idea and massive difference from standard C++ is that object destruction is delayed until a "quiescient state" happens in what is a reframing of RCU [https://en.wikipedia.org/wiki/Read-copy-update], allowing to freely use raw pointers as long as none survive across a quiescient state.

[note however that this system allows to take pointers to stack variables, so they have to restrict raw pointers to function arguments only - it would be better to also introduce a "heap-only" pointer that can be freely returned/stored on the heap/etc. but can't be stored in types that live across a quiescient state, from which stack-or-heap raw pointers can be derived]

This also results in the downside that things like mutexes can only be safe if they are kept locked until a quiescient state happens, since that's the only lifetime that the system understands.

Likewise, you can't do this like prevent updating a collection while iterating unless you are fine with freezing the collection until a quiescient state happens.

In general, you are much better off using Rust (or an equivalently expressive language, if it existed), since that allows to statically check for correctness, not have to delay freeing memory, and allows to use lifetimes and linear types to secure mutex locking, collection iteration, and other things where lifetimes are essential.

I don’t understand your comment about how rust pointers are safer than soft pointers. The article explains how to implement a wide variety of pointer semantics, all of which are memory safe (throw an exception on explicit use after free, use the type system to have the compiler statically check the pointers are live, use dynamic cast, etc). Looking online, I see that people implement all the same primitives in rust, with exactly the same safety caveats.

Also, the container and mutex tricks you mention sound interesting, but I don’t see why they can’t also be used in C++ (which has a turing complete type system / checker).

> use the type system to have the compiler statically check the pointers are live

It doesn't explain how it would statically ensure that a moved-from unique_ptr (or equivalent) can not be used. In fact the only mentions of moves are that owning pointers can only be moved and soft pointers can be moved or copied, but C++'s move does not remove any access, it just moves the content leaving the moved-from object in a "valid but unspecified state".

Note that valid != safe. Dereferencing a moved-from unique_ptr is unsafe for instance.

Rust's affine types solve this issue, a moved-from type (Box included) simply can't be used, its scope ends when it's moved.

> Looking online, I see that people implement all the same primitives in rust, with exactly the same safety caveats.

Rust's (safe) pointers and references don't throw exceptions on explicit use after free because such code doesn't compile at all, and its equivalent to dynamic_cast has to be very specifically opted in: https://doc.rust-lang.org/1.19.0/std/any/trait.Any.html#meth...

“Throw an exception on explicit use after free” is “memory safe” in exactly the same sense “throw an exception on explicit use of an operation on an argument of the wrong type” is “type safe”. In other words, not at all.
> that performance is worse due to the checks.

I'd argue that use cases for 'soft pointers' are about the same as that of Rust's RC<T>, which also incurs runtime costs (very briefly - there is no magic here, neither with Rust).

> The key idea and massive difference from standard C++ is that object destruction is delayed until a "quiescient state" happens

If you're speaking about OP - clarification: it is not "object destruction" which is delayed (destructor is still called synchronously when the variable goes out of scope, so all the crazy finalize()-like problems don't occur), it is memory deallocation which is delayed (and this is generally ok as deallocation is not observable, or at least garbage-collected languages tell us so <wink />).

> things like mutexes can only be safe

Whether C++ or Rust or whatever-else, mutexes at app-level are evil ;-) (it can lead to a very long discussion, but long story short - finally, by 2017, most of the opinion leaders started to converge to this IMO-very-obvious observation: ASYNC RULEZZ! <wink />).

> since that allows to statically check for correctness,

The idea behind OP is to have a tool which will do the same thing (where possible, see above re. 'soft pointers' and RC<T>). Whether such a tool materializes - is a different story, but well - first we have to agree that such a tool is a Good Thing(tm).

> have to delay freeing memory

In practice, it is never an observable problem in (Re)Actor-like contexts ((Re)Actor use cases are about highly interactive systems ranging from games to stock exchanges, where typical input is processed in milliseconds, and amount of allocated memory until the 'quiescient state' is reached, is single-digit kilos; in extreme cases, it goes up to single-digit megabytes, still nothing by modern standards).

> you are much better off using Rust

Really really depends. It is still C++, and being C++ has its own virtues (alongside with its own quirks); just two things to illustrate this point - (i) recently it was revealed that modern GPUs are designed with ISO C++ standard in mind (specifically C++, not Rust or anything else); (ii) developer availability is also a major factor for real-world projects, and so on, and so forth. In an ideal world - well, probably Rust does look as a more to-the-point language (though even with Rust I'd create an own dialect, in particular, outlawing thread sync to simplify things), but given real-world considerations - the choice is certainly not that black-and-white.

> I'd argue that use cases for 'soft pointers' are about the same as that of Rust's RC<T>, which also incurs runtime costs (very briefly - there is no magic here, neither with Rust).

The article's "soft" pointer does not own its contents and depends on an owning pointer, so the semantics are much closer to Rust's references. In fact, the article draws an analogy between soft pointers and weak_ptr. And of course dereferencing "soft" pointers can fault.

> Whether C++ or Rust or whatever-else, mutexes at app-level are evil ;-) (it can lead to a very long discussion, but long story short - finally, by 2017, most of the opinion leaders started to converge to this IMO-very-obvious observation: ASYNC RULEZZ! <wink />).

Async does jack shit for concurrent safety. You can have either shared-memory concurrency or isolated concurrency.

And explicit asynchronous API (à la JS or C#) are dreadful.

Do you happen to have a citation for (i)? That's very interesting!
I have a question about this.

Articles like http://blog.llvm.org/2011/05/what-every-c-programmer-should-... have convinced me that even if C or C++ reads logically like it is safe, there is a possibility that the compiler can rewrite your code in an acceptable way according to the standards such that the checks that are clearly visible in your code disappear, opening up the very problems that you thought you were protected against.

Is there any possibility that after an aggressive compiler gets done with inlining and optimization that that could happen here in some way? Can it be proven that if the compiler works according to the standard that this won't happen..even if the programmer accidentally trips on undefined behavior?

The best thing I've ever encountered that encapsulated this was a Cap'N'Proto vulnerability, and the discussion here about it was enlightening as well.[1]

The highly condensed version is that the compiler optimized away an if block that was responsible for throwing an error as impossible to reach in correctly functioning code, when the whole purpose of that if block was to check that condition and error so that the program did not continue in an invalid state.

Specifically:

  word* target = segmentStart + farPointer.offset;
  if (target < segmentStart || target >= segmentEnd) {
    throwBoundsError();
  }
  doSomething(*target);
(a simplified version of the actual code) was used to detect if target had overflowed (and thus target < segmentStart). The bug report goes on to explain:

However, as it turns out, pointer arithmetic that overflows is undefined behavior under the C standard. As a result, the compiler is allowed to assume that the addition on the first line never overflows. Since farPointer.offset is an unsigned number, the compiler is able to conclude that target < segmentStart always evaluates false. Thus, the compiler removes this part of the check. Unfortunately, in the case of overflow, this is exactly the part of the check that we need.

The post that's from (the sumbmitted article to the HN discussion I linked) is fairly accessible in my view. I highly recommend reading it.

Another discussion that probably has good info is this one.[2]

1: https://news.ycombinator.com/item?id=14163111

2: https://news.ycombinator.com/item?id=14785867

-fwrapv should fix it (no warranties of any kind, batteries not included).
Perhaps someone better versed in those compilers can add to/correct me here, but I'm pretty sure that can only happen if you're invoking UB somewhere along the line.
Once you've entered the realm of undefined behavior, the compiler can really do whatever it likes. Before then all it can do is assume you're not doing anything undefined.
That's exactly the problem. C/C++ has undefined behavior as part of the language spec, so it can never be safe unless you use a compiler that promises to reject programs that invoke undefined behavior.
The generally recommended thing for catching & fixing this is: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html

CppCon has also had a lot of great talks on UB and why it is what it is, such as https://www.youtube.com/watch?v=yG1OZ69H_-o

This is only a problem if you are using threads or shared memory and making up your own misguided locking mechanisms or in embed code on a processor with interrupts but no locks. Normally, if you use the tools correctly you will never have to worry about compiler reordering messing anything up.

The article you linked discusses what happens when you do things you should not, like fail to initialize a variable before using it. Modern compilers, when used correctly will let you know when you do this. With the right compiler options, it won't allow you to make such mistakes.

The example in the case that I linked was single threaded code having a check for a null pointer unexpectedly removed.

So no threads, no shared memory, no locking mechanisms, no interrupts. Just a compiler making valid optimization and removing a necessary guard condition.

Try again.

As far as I'm aware, if you stay within the confines of smart pointers (and don't drop down to the raw pointer it owns) you will never encounter undefined behavior. You may have crashes if you try to double free something, but these are defined to crash rather than letting the compiler optimize out checks.
> As far as I'm aware, if you stay within the confines of smart pointers (and don't drop down to the raw pointer it owns) you will never encounter undefined behavior.

Deref'ing an empty (e.g. moved-from) unique_ptr is still UB.

> As far as I'm aware, if you stay within the confines of smart pointers (and don't drop down to the raw pointer it owns) you will never encounter undefined behavior.

No. Even with smart pointers it's possible (move out of unique_ptr, deref). But even with no pointers it's possible - index into an array without checking the bound, signed int overflow, etc.

UAF is still possible. Iterator invalidation, and such.
Wouldn't you have a problem with reference cycles in C++ smart pointers? Not sure if they do anything special to prevent this.
This technique is mostly a garbage collector, as I see it. Postponing memory destruction until the stack is empty is a special case of deferred reference counting [1], where sweep can only happen with an empty stack. If the "soft pointers" are implemented with reference counting, that's also a type of GC.

On the other hand, the tagged pointer implementation strategy for "soft pointers" isn't really garbage collection, but it does have much of the same overhead. Pointer reads must check the tag ID and throw, which is like a read barrier [2]. Writes through a pointer must do the same, similar to a write barrier [3]. And that's not getting into the overhead of multithreading; I see no reasonable way to implement this scheme in a multithreaded world. I expect that a fast GC without read barriers will significantly outperform this scheme. As much as everyone complains about the speed of GC, garbage collection is hard to beat!

[1]: http://www.memorymanagement.org/glossary/d.html#term-deferre...

[2]: http://www.memorymanagement.org/glossary/r.html#term-read-ba...

[3]: http://www.memorymanagement.org/glossary/w.html#term-write-b...

> Pointer reads must check the tag ID and throw, which is like a read barrier

Usually, "read barrier" is understood as a multithreaded stuff - and OP has nothing to do with MT. In other words, no "read fence" is necessary (simply because it lives in a perfect single-threaded world). And from this POV, it is extremely difficult to beat this schema with any popular-multithreaded-GC. As a side note, proposed schema DOES allow 'naked' pointers, so relatively-expensive (costing ~4CPU cycles, which is not much to start with) conversion from 'soft' into 'naked' has to be done only _very_ occasionally, and after the conversion, we're working with good old plain pointers, which just happen to be safe due to the way they're used.

If anyone is really interested in this sort of thing, I suggest you take a look at SaferCPlusPlus[1]. It is "A Usable C++ Dialect That Is Safe Against Memory Corruption" (including data races). And it already exists.

And I think it's better than this proposed dialect in that most of the (safety) restrictions are enforced without requiring extra tooling, and it's much less restrictive. Most existing C++ code can be converted directly. And the run-time overhead is kept to a minimum. Btw these advantages apply versus the Core Guidelines[2] as well.

[1] shameless plug: https://github.com/duneroadrunner/SaferCPlusPlus

[2] https://github.com/duneroadrunner/SaferCPlusPlus#safercplusp...

I happen to like quite a few things from it, but... there is a Big Fat Hairy Difference(tm) between "safe" and merely "safer". Make it "guaranteed to be safe" (which will most likely require tooling) rather than merely "safer" - and I will be the first one to promote it myself :-). Also - it would be gr8 to reduce the number of different concepts developer needs to remember about while programming. In OP (assuming that tooling does exist) it is quite simple: there are only 3 concepts, with 2 of them ('naked' and 'owning'=unique_ptr<>) being already very familiar; OTOH, current implementation of SaferCPlusPlus reminds me of ALGOL68 - where it was possible to specify _everything_, but choosing the right thing was so time-consuming that it never really flew.
SaferCPlusPlus has two big issues which prevent me from using it: confusing class naming and too many concepts.

I do like the ideas it builds on, and I will probably implement a simplified version for my needs...

Even when you use RAII, const-by-default, shared pointers, type-rich APIs, and the like, you're still using C++. That means you're still tied to C's legacy defaults (of UB) and that also means you're still using C++ value categories. If your "safe" C++ subset uses references, it can't be guaranteed to be safe (since plenty of valid code will lead to UB).

More info in the value category cheat sheet: https://github.com/jeaye/value-category-cheatsheet/blob/mast...

TL;DR; use smart pointers, RAII semantic and STL containers/iterators. Though, I'd have a few criticism...

> Rules to ensure memory safety

These "rules" only protect you against object's lifetime issues, not overflow / underflows, and other kind of memory issues.

> ‘owning’ pointers are obtained only from operator new

no, you shall be using std::make_{unique,shared}(...) which will protect you against leaking memory if exceptions are raised.

> Calling a function passing the pointer as a parameter, is ok.

Correct, but you can still shoot yourself in the foot. Best is to pass a [const] reference to the function called.

> This only leaves us with functions such as strchr()

Don't use C API. The STL should provide you with enough API to use the proper C++ types, either std::string or std::string_view in C++17 if possible.

> and also prohibits C-style cast and static_cast with respect to pointers

IIRC, you can't static_cast<> a pointer, you'd have to reinterpret_cast<> it, which the document does mention.

> For arrays, we can always store the size of the array within our array collection, and check the validity of our ‘safe iterator’ before dereferencing/indexing

use std::array.

Mostly agree, but:

> Don't use C API. The STL should provide you with enough API to use the proper C++ types, either std::string or std::string_view in C++17 if possible.

Sometimes you're working with C API that gives you back a char * that they've already allocated. AFAIK there isn't a way to create an std::string out of that without a copy.

> you can't static_cast<> a pointer

You can static_cast a void * into other kinds of pointers.

> use std::array

Do you mean std::vector and at()?

Well, the point of the OP goes further than that. Two Big Questions are (a) what to do with the non-owning back references (such as backref going up the owning tree) - for this 'soft' pointers are proposed (I _hate_ shared_ptr-like ref-counted stuff, in large projects they tend to cause much more trouble then they're worth, especially memory leaks due to shared_ptr loops are troublesome, causing both syntactic and semantic memory leaks, ouch!), and (b) how to formalize the use of those non-owning ('naked') pointers/references and how to prevent them from being dereferenced when they're pointing to already-deallocated memory locations (and saying "don't use naked pointers/refs, ever" is not really practical IMNSHO).
Using iterators and ranges(think a generalized string_view) handles the overflow/underflow issue in conjunction with algorithms(std ones).
Dialects are of limited uses, because they are dialects... New dialects are arguably of even more limited uses, because better languages now exist where the desirable characteristics are enforced not by using a dialect, but by the core languages, and safety checking is not optional. (Also, I'm somewhat curious about why the proposed dialect tells about think "similar to unique_ptr, and so over: just use the real think -- at least it would be less a dialect and more of modern standard C++). Dialects enforced by wishful thinking or at beast ad-hoc tools maintained by a too small community will perish in front of well architectured languages maintained by a real community.

They have even been used to ship some important code in big project made of tons of legacy code -- so I'm not even sure an interop argument could be made.

One point of a dialect (aka “coding standards”) is that you can evolve legacy code bases toward them with a series of simple refactorings instead of by rewriting from scratch.

For me this is the big advantage of C++: it is possible to backport virtually any language feature you want to it, thanks to the combination of modern template programming and low-level C-style bit twiddling.

Interesting article. I hope the author has a chance to take a look at the Pony language, which he's described the core of. Now all it needs is a capability system to statically ensure that the data in sent messages is safe without copying. (And to move those runtime checks into the type system.)
This is pretty close to the "autorelease pool" concept in objective C - an idea which I copied in C++ for a product around 10yrs ago to good effect.

You wrap an auto release pool around every turn of the event loop, which is the deferred memory release mentioned in the article (I admit I only scanned it). Within the "react" part, this gives you much cleaner way to code even if your code involves raw pointers to objects, as long as they are allocated on the pool and aren't being transferred to another thread or caught in an RC loop - both of which we managed using custom smart pointers.

I feel like stuff like this is why golang and rust were created.

(edit: Forgot about Rust, sorry!)

I feel like stuff like this is why golang was created.

Or more properly, why Rust was created.

I've grudgingly used C++ on some projects because of other constraints such as the target platform. Due to the compiler version, we're stuck on C++11, which is... OK. But keeping straight what we can use, and what we can't, and which kinds of pointers we should be using when is a considerable burden.

Still working through "Effective Modern C++" while learning the ins and outs of it in general.

Well, they were mostly created, because the alternatives to C and C++ ended up loosing their market share, so current generations aren't usually aware of what came before.

Go is anything hardly new versus what Algol 68, Pascal or Oberon derivative would offer.

Likewise the best part of Rust is their work on how to make affine types from Cyclone, ATS and others into more developer friendly and productive language features, while following the traditional rules of other safe systems languages.

Since it is easier to introduce new languages than bring back old ones, here we are.

"Now, we can extend our allocation model with a few additional guidelines, and as long as we’re following these rules/ guidelines, our C++ programs WILL become perfectly safe against memory corruptions."

What could possibly go wrong?

If you're not being facetious, then really, not much. You really can't go wrong with smart pointers unless you explicitly try to access the memory it handles rather than going through its normal interface (e.g., not using get()). Shared pointers are basically reference counted just like many other language handle memory management.
This is basically how I program C++. Except that I try to avoid the 'new' keyword too by std::make_unique and std::make_shared. This way there are literally zero 'new' and 'delete' or 'malloc' or 'free' calls in your program.
I would try to go further and wrap them in a class to hide the heap usage and expose the valid uses in the interface. Then it reads like a value and walks like one too. Unless I need virtual inheritance I guess.
Likewise, I avoid -> if possible.
There's a pretty good comment on this post from a shadow banned user named Kenji, which I'm reproducing below:

This is basically how I program C++. Except that I try to avoid the 'new' keyword too by std::make_unique and std::make_shared. This way there are literally zero 'new' and 'delete' or 'malloc' or 'free' calls in your program.

I just vouched for that comment, so it should show up now.