I view move semantics as not a memory safety thing at all (in fact, use-after-move can be exactly as ugly as use-after-delete), and smart pointers as a “let’s just make things marginally better than raw raw new/delete” thing. Smart pointers in C++ can still be null, after all.
Calling C++ not serious about seems a reasonable take to me. Of course Rust didn’t invent all of its solutions from scratch.
I feel like you just defeated your point; those are bolted on instead of a natural part of the language.
(a) Smart pointers + static analysis + sanitizers make C++ pretty usable
(b) C++ usability requires brittle, half-baked add-ons such as smart pointers and non-deterministic/heuristic/unreliable/NP-hard development tools such as static analysis + sanitizers.
My personal opinions on C++ are not very positive, but it's a language I literally grew with. My experience is that C++ code very gradually became more stable and less buggy. The thing is, some of that is just improved discipline, some of it is static analysis, etc. But a lot of it, is genuinely C++0x/C++11 and features that built on top of this.
So the idea that Rust showed up and suddenly C++ cared about safety? I don't really see it. I think C++ developers started caring about safety and then Rust showed up because of that.
P.S.: While the borrow checker and ownership semantics is really cool, I think a programming language's ecosystem and the 'best practices' it lends itself well to have a greater impact that people completely miss. Rust, Go, Zig are all clearly trying to foster a culture of writing more correct and complete programs. Error handling doesn't just feel like a couple control flow mechanisms you can use, but a responsibility that you have. Modern C++ is getting better probably more because of this than any committee; although I really wish the Expected type would've gone somewhere, since I feel Rust's `Result<...>` type and Go's multiple-return with `error` are being proven right as ways to handle errors robustly even with their caveats. (I've heard Zig's error handling is really cool too, but I haven't had a chance to write a serious Zig program. I'll get to it soon.)
- C++ code where some method calls std::shared_from_this(), and that method ends up being called indirectly during the object's construction, leading to a 0-pointer dereference [1]
- accidentally creating two shared-pointers to the same object leading to an eventual double-free (that may just silently corrupt the heap while the program continues running)
- undetectable cyclic shared_ptr<> references somewhere deeply hidden in the code causing a memory leak
Modern C++ feels like a case of "but we can do that in C++, too" syndrome. Stuff that "just works" in Java or Lisp, can now be done in C++, too, however in C++ it will only work for 95% of all use cases, and break in a spectacular manner for the remaining 5%.
E.g. think about the template meta-programming madness (and efficiency WRT compile-time) and compare that with what Lisp offers (see also [2]).
[1] https://stackoverflow.com/questions/31924396/why-shared-from... standpoint
[2] http://people.cs.uchicago.edu/~jacobm/pubs/templates.html
OTOH, C++ needs the ability to support old code bases because of its age. Of course a new language can forego this.
As for cycles, for GC language using ref-counting under the hood, the same problem applies. I've also seen memory leak from accidental keeping of pointers in fully GC languages. I've seen it happens in Java multiple times. Of course, two wrongs don't make right, but in this case GC is only solving one case.
But the more fundamental issue at play is that pointer cycles usually means a lack of up-front design and thought about how a program will work. As such, it is bad, but only a symptom of a lack of proper initial design which can manifest itself in all kind of places.
auto self = shared_from_this();
document.onclick([self](){self->doit(); } );
I'm not sure there is a way to express that in C++ without use of enable_shared_from_this.It is very easy to accidentally run this kind of code from a constructor where shared_from_this() is a 0-ptr.
make_shared also only helps in 95% of cases. E.g. it does not allow specifying a custom deleter, which is a use-case that is allowed by the shared_ptr<> constructor. It also won't fix any of the problems WRT shared_from_this() being called in the constructor.
Note that move-destructors were also considered for C++11, and were not added for lack of time / implementation experience . And this is really too bad, because they are at least as important as move constructors if not more so; I've yet to encounter a case where you actually want to use a moved-from object after the move.
I think C++ could really use what Rust calls generations. Figure out the obviously bad ideas and deprecated them over 10 year cycles.
That would allow the language to shrink again, I don't think it ever did.
Even things like integer promotion rules or "trivial" things like "deprecate NULL and 0 for nullptr" are likely untouchable with any edition mechanism for C++ that doesn't end up Breaking the World.
Regardless - C++ will effectively has some deprecation, at least of the standard library, with the introduction of modules instead of included headers in C++20.
Everybody and their uncle had a lot of ideas about how they would fix "C++". Eventually all those contradicting ideas got merged into the C++ standard.
:)
So I'm quite curious, what problems with it you were referring to?
Rendezvous Facilities: Concurrent C and the Ada Language - https://www.computer.org/csdl/journal/ts/1988/11/e1546/13rRU...
> The concurrent programming facilities in both Concurrent C and the Ada language are based on the rendezvous concept. Although these facilities are similar, there are substantial differences.
> Facilities in Concurrent C were designed keeping in perspective the concurrent programming facilities in the Ada language and their limitations. Concurrent C facilities have also been modified as a result of experience with its initial implementations.
> The authors compare the concurrent programming facilities in Concurrent C and Ada and show that it is easier to write a variety of concurrent programs in Concurrent C than in Ada.
Using "scale" is also misleading, because to use again your example C++ is a mainstream programming language and one of the most popular in the world. It towers above Rust by any thinkable metric related to scale - popularity, projects, developers, libraries, etc.
I do agree that discipline isn't enough... in the sense that one can't rely on discipline to achieve safety. This is a well known principle of safety engineering, which applies to all systems, not just SW or SW/HW systems. Discipline remains nevertheless essential, because it's one of the main things influencing the "human factor".
Perhaps there are two camps here: (1) Reduce discipline by making developers no longer have to consider certain situations (garbage collection fits here) and (2) Reduce discipline by shifting that to the compiler (types fit in here)
My experience is completely different. I programmed a bit in rust recently. Nothing system level. Quite opposite.
I had multiple occasions when after succeeding to implement some part in Rust I thought to myself "wow, if I tried to do that in C++ I would have fallen in so many pit traps by now, that rust helped me avoid by restricting me in some ways".
And I'm very new to rust and I was just coding blindly just by powering through different approaches till one of those compiled.
"I can't use borrow to keep that? What's a borrow anyways? Let's just cram it in Rc whatever it is. I can't return this? Fiiine! I'll create it outside and pass it in."
And in the end it worked perfectly. No weird bugs to find. I felt like a caveman trying to bang rock into shape. And at no point rock cracked, split and or buried me under the rubble.
This is exactly what makes it so that the programmer doesn't have to be disciplined. The Rust compiler will correct the lazy Rust programmer, while the C++ compiler will blithely do its best.
In big multi-core systems, I model the handful of locks by the worst-case access frequency for a core to understand contention. In practice, this is often on the order of 100ms, which means the locks are effectively contention-free and the fast path almost always wins.
"Lock free" data structures are tied very closely to the data being locked, and depend heavily on certain CPU operations being atomic. This is more of an issue with ARM than with x86, because ARM needs more fence instructions. Really, "lock free" programming is just locking critical sections with lower level hardware primitives.
The opposite extreme is POSIX locks. Locks are the OS's problem, and nothing in the code says what's covered by the lock. That was once standard practice and is now recognized as being bad.
I have to disagree, as the more interesting lock free algorithms use atomic operations that can fail. Yes, a compare-and-swap is like having a critical section on modifying that particular address. But a compare-and-swap can fail if what is currently there is unexpected. The result of that failure generally means redoing a bunch of work, rather than just trying again. Interesting lock free algorithms tend to have the structure:
1. Read some data.
2. Perform computations on that data.
3. Trying to commit the result of that computation. If someone else committed data to the same location after you read it in step 1, go back to step 1.
That's less like a "critical section," and more like a transaction. For lock free algorithms that just rely on atomic operations that cannot fail (such as an atomic increment), then yes, those are just like having a lower-level critical section. (And, on some architectures, that's exactly what they are.)
(Also, not all architecture use hardware mechanisms to enforce actual mutual exclusion. For example, Power does not, as it uses load-linked, stored-conditional: https://en.wikipedia.org/wiki/Load-link/store-conditional. On Power, even things like atomic increments are implemented with LL/SC. Architectures basically have to decide: will they implement atomics through some kind of a lock mechanism, or some kind of LL/SC mechanism.)
What lock-free means in this case is that an OS level thread freezing doesn't prevent global progress. That's true on the abstraction level of typical programming, but grandparent is very much correct:
> Really, "lock free" programming is just locking critical sections with lower level hardware primitives.
Compare and swap is interesting. It's a tiny transaction. It can even operate across networks - FireWire had hardware compare and swap support. Compare and swap with retry may have to wait and retry, which is effectively waiting for someone else to stop using a resource you want. So it's a form of a critical section lock.
Can fence instructions really be seen as locking mechanism? While a lock generally is bound to some data, memory fences are bound to relationships between data or a piece of code. I don't know if Rust or any other language can help you with that.
Isn't the actual bad practice to expose locks at all? If the locking is done implicitly in API calls, one doesn't need to ask "What locks it".
See also, eg, the LOCK prefix on x86 instructions.
(disclaimer: I only have approximate knowledge of anything, I'm not an expert)
That is incorrect. Properly designed and implemented mutexes (like in the parking_lot crate [1]) are extremely fast in the uncontented case, and need just one byte of overhead (and no heap-allocated memory) per mutex. In the contented case, a lock-free algorithm still has to deal with arbitration between the different threads, so it's typically not faster than a lock-based approach. Lock-free algorithms do have certain advantages (like deadlock-freedom), but performance is typically not one of them.
> Locking is a [...] scalability destroying operation
Also incorrect. If you have N threads trying to append to a single queue, then you have a scalability problem whether you're using locked or lock-free data structures. The solution in such a case is fine-grained concurrency and a different approach, not lock-free algorithms.
A common train of thought seems to be "locks are slow, so lock free must be fast", while lock-free says nothing about performance -- only about forward progress.
The question is "What prevents me from making changes to this variable without breaking everything".
So the point is not "What locks guard this data" the point is, how do I determine whether I can currently modify (or even read) this data. This allows the compiler to check that, indeed, you never use the data in a way that would break things. Hence your compiler can then give guarantees about thread-safety.
Now it seems likely that, when compilers do this and you write wrong code, your conclusion might be "I need a mutex lock for this", or if performance matters, "I need to rewrite this to allow performant lockless code. But the main question remains "Can I currently modify this data without issues".
Possibly unpopular opinion: I am wary of relying on type inference for return types, other than for a function defined locally where you can easily see the implementation at the same time as the code calling it. Code like
auto x = calculate_some_complicated_thing()
...
do_something_with(x)
has a similar obscurity problem to code like do_something_impressive(true, false, false, true)
in that to understand what the code is doing at this level, you have to read the interface documentation for the function you’re calling anyway, and then you’re hoping that you get everything in the right place because your tools might not be able to warn you if you don’t.Making the locks public, I sort of get (like making any field public) -- you have multiple locks, you need to ensure they are accessed in a certain order and if you make them public, you cannot enforce that order.
Instance.this is implicitly public because something else could have a reference to instance.
synchronize(this){}
that will lock with the following if its inside instance. synchronize(instance){}And in your view, are the bases flawed because they are low level?
"Which is what? Still to be created, or does it exist?" - several different 'initial stakes'/bases in the space of programming languages exist/were made/can be created than the family of languages mentioned in OP. Exploration from those might yield better optima. The widespread adoption of the current industry 'standard' languages seems an accident of history, not guided by quality of language.
"Are the bases flawed because they are low level?" - yes. A significant part of the computing field is about abstractions. Improving the level of the technology is one of the arguments the article here makes too. Besides, it's the implicit admission of everyone who doesn't advocate/write assembly.
On top of that, locking is faster than most lock-free strategies unless you deal with high contention or deterimistic guarantees (90% of the latter is solved with bounded fifos).
That's a bad example: GPUs are all about shared memory, concurrently accessed by many threads. It's up to the programmer to make sure that there are no read/write or write/write data races.
Ideally, any shared data is immutable (and modifying it would then need to be done in a coordinated synchronised fashion) while most mutable state is only ever owned by one task/thread, with ownership explicitly passed between them as others need access, unique_ptr style. Reality doesn't always match with the ideal though.
For many applications, this is the way I prefer to structure code these days. If you know that the only part of your program that will touch certain state directly is always running in a single thread, a lot of problems just go away, because whatever you do in response to any given message or other signal is effectively an atomic transaction, regardless of any concurrency or asynchronous behaviour you have going on elsewhere in your system. You still need to figure out how your message passing or other signalling infrastructure is going to work and in particular what will block and what won’t, so it doesn’t answer all of the usual questions with concurrency, but it’s a widely useful strategy and it goes a long way toward keeping things manageable.
If you have a function in a standard library, types are obviously helpful, albeit it can be convoluted if they are templated.
On the other end of the scale, if you have a little helper function used to reduce duplication/increase readability in another function, explicit typing easily becomes busywork. This is where duck typing/templates shines.
I once sent a suggestion to the C++ committee people that they allowed easy templating of function parameters (something like "auto foo(auto bar)"), but I don't think people who are not used to duck typing appreciate how much it helps in the grunt work inside modules. At least, I didn't myself.
Similarly, you can use auto as a return type if the function implementation is immediately given and the compiler with infer the return type. The new starship operator (<=>) typical usage is pretty much declaring it with an auto return type and "= default" implementation.
Mutability is very different in C++ though - const is quite a complex topic, and isn't as simple as "this variable binding is/isn't mutable".
The encouraging thing today is that we're kind of converging on good practices in language design.
- You really want to declare types for function parameters. The compiler needs that information for checking, and the programmer, and the maintenance programmers who follow behind, need that information so they know how to call the thing. On the other hand, you can usually infer the result types of expressions by a simple forward process. So C++ gained "auto", Rust and Go started off with that arrangement, Javascript got TypeScript, and Python is getting there via a side trip through unchecked type declarations.
- The three big questions in C: "How big is it?", "Who can delete it?", and "What locks it?" C gives no help with any of those. C++ now tries to address the first two. All the garbage-collected languages address the first two. Rust addresses all three.
- The idea that locks should be connected by syntax with the data they are locking took far too long to become accepted. Partly because the Ada rendezvous and the Java synchronized object didn't work well. Even Go doesn't do that very well. But language design seems to be coming around. When and if thread-type concurrency comes to Javascript, the webcrap crowd will create race conditions everywhere without automation in locking.