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.
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).
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...
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.
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.
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 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]
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
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.
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.
Deref'ing an empty (e.g. moved-from) unique_ptr is still UB.
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.
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...
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.
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 do like the ideas it builds on, and I will probably implement a simplified version for my needs...
More info in the value category cheat sheet: https://github.com/jeaye/value-category-cheatsheet/blob/mast...
> 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.
> 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()?
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.
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.
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.
(edit: Forgot about Rust, sorry!)
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.
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.
What could possibly go wrong?
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.