Presumably my code has been less than optimally efficient or something, because it seems that most people talking about "modern" C++ view it as absolutely central to the language.
Presumably my code has been less than optimally efficient or something, because it seems that most people talking about "modern" C++ view it as absolutely central to the language.
Yep, it was either less efficient than it could be, or more clunky, or incorrect.
I remember back when boost was still a thing, everyone using boost::shared_ptr indiscriminately. Refcounting has a very nontrivial cost to it if implemented correctly.
Things like rvalue refs and std::move allow to properly implement the concept of "ownership transfer", obviating the need for costly ref counting in the majority of cases.
1) your object graph is complex enough to warrant the ubiquitous use of shared pointers;
2) you can afford the performance hit
it's better to use an actual garbage collector, even if it's bolted-on unreal engine-style.
With shared_ptr-style ref counting, every dtor is potentially a bomb waiting to go off causing a cascading effect. It's actually less predictable than a GC that you can run manually at fixed intervals or whenever a convenient opportunity arises.
Not really. It's difficult to reason about, but it's entirely deterministic, unlike a tracing GC.
Plenty of peripheral inputs that can be used to induce true randomness.
"Non-deterministic" in this context means that the behavior of the program is not deducible from the state of the current thread of execution, or more generally, from the state of the system being analyzed. If another thread modifies the current thread's memory, that modification is non-deterministic because you couldn't have foreseen it by following any pointer that's reachable by the current thread. If a cosmic ray hits a computer chip and flips a bit, that bitflip is non-deterministic; you couldn't have predicted that that bit would be flipped by looking at any part of the computer.
In other words, an effect is non-determistic with respect to a causal chain if it's causally unrelated to it.
A tracing GC is non-deterministic for two reasons. First, it causes effects (namely, pauses) in all threads, even those that never allocate any memory. Second, it makes the lifetime of objects unpredictable. Thread A can allocate an object and then drop it, and when that object will be destroyed depends on the behavior of the entire system, not just thread A's. RAII doesn't exist in GC'd languages (except through using or try-finally hacks).
Reference counting is completely deterministic. If a thread holds the last reference to an object you can predict with certainty when that object will be released and which objects will also be released as a consequence, before actually releasing the object. You don't need to know what any other thread is doing to make this prediction.
Crucially, if you find that sometimes a reference counting program spends too long releasing an object graph, all you need to do to debug it is to run the program again under the same conditions and break when it reaches that point. Try doing the same with a garbage collector.
You're contradicting your own definition of determinism here. Other threads may be holding references to the same object, and unless you know what the other threads are doing, you _cannot_ predict when an object is going to be released just by looking at the execution of a single thread.
You need to look at the overall behavior of the application, with the complex object graph spanning multiple threads.
> all you need to do to debug it is to run the program again under the same conditions
this is much easier said than done.
Yes, other threads may be holding references to that object, except when the current thread holds the last one, which was the first thing I said in the statement you're responding to.
But that aside, you seem to be making the assumption that threads sharing state and object graphs between each other is a given, but that's completely false. Reference counting lets you design your application such that threads don't need to interact with each other to manage their memory. So you have the option of completely deterministic behavior, if your problem is amenable to such a solution. On the other hand, tracing GC doesn't have that option. Threads affecting each other is mandatory, even when no object is reachable from two threads simultaneously.
Between reference counting and tracing GC, only one permits deterministic object destruction.
>this is much easier said than done.
You're right, it's much better when your memory management system makes it completely impossible to debug stalls. Completely impossible is definitely better than difficult.
Refcounted pointers don't make that guarantee. They just guarantee that if they are incorrect they will not deallocate (== will leave garbage) and thus never result in a use-after-deletion.
Most of the time they are fine but they can't handle circular structures. This is why C++ has the weak_ptr.
The alternative is "no language-level references, any time you want object "Foo", go look it up in some table". We opted not to do that.
Same with performance. Where nowadays simple std::move() on a return value suffices you had previously explicitly to pass the object/vector with which you swapped as a parameter.
Life with move semantics is a tad easier.
> return std::move(ret);
it can interfere with return value optimisation (RVO). There might be cases where the move makes sense but to my understanding it's enough to just use return.
It's very much possible to take advantage of modern C++ move semantics without an explicit call to move. Maybe you return a unique_ptr created by make_unique in a factory method, or call swap() on a moveable object, etc. I rarely need to write std::move to take advantage of modern C++ although it took some self-education about rvalues etc to learn how to do it.
But if you have something like a builder object which builds a thing which it keeps as an instance variable; then you have to use `return std::move()` or an out-parameter-and-swap if you want to avoid copies.
The reason modern C++ has become what it has become (in both good and bad senses) is because template generic programming has led it down a path of built-in constructs needing to consider an ever-growing zoo of weird cases. For every simple answer to the question, "how should this thing work?" there's a counter-example, "what if the type doesn't have that property?"
That's where we get things like std::move, etc. By contrast when dealing with your own code you can use knowledge of what is actually going to be passed to ignore many of these concerns or change your abstraction-using code to have or not have some property.
Overall, simplifying the use-case or restricting the domain is more pragmatic than making things fully general. C++, in its standard library / template utility headers is painted into a corner in that respect since it doesn't control the use-case, and the effort to restrict the domain (i.e. concepts) was delayed as well as representing itself another layer of complexity rather than a pure simplification.
IMO, a large set of "Modern C++" fanboys have a serious case of "Featuritis"; Merely a focus on the trivial use without understanding the motivation behind it and its place in the larger scheme of building systems. It is like they believe more knowledge of keywords/buzzwords == more expertise.
Even Scott Meyers said this: https://news.ycombinator.com/item?id=27945383
No offense, but are you using modern C++? One of the most common uses of std::move is around ownership transfer of std::unique_ptr - most moderately big projects will have those. Other uses are around preventing copies, etc but those need more thought since you may actually cause regressions by getting in the compiler's way.
We do not use unique_ptr and to be honest I can find little to no use for it anywhere in our 600k lines of C++.