However I'm excited for the future of system-level programming: we were stuck with C/C++ for a long, long time and maybe that's finally changing thanks to Rust!
However I'm excited for the future of system-level programming: we were stuck with C/C++ for a long, long time and maybe that's finally changing thanks to Rust!
In the system programming world, "simple" means having the program do what you tell it and no more, so that there are no surprises when you come to run it.
Rust has a very complicated compiler, and a moderately complicated syntax, but the semantics of the language are amongst the simplest out there - far simpler than C (purely because of the UB), Nim or Kotlin Native IMO, and that really helps when writing low-level code, or even just when contributing to an unfamiliar code-base.
("Modern" JavaScript comes close, but that's not because of semantics, it's more because everything is incredibly overengineered...
More complicated than Haskell, etc.?
The patronizing attitude of rust defenders here on HN, on the other hand, man... it just pushes me further away.
That's not the case for Rust. It takes a few solid weeks of feeling stupid until you really start to grasp borrowing, and that experience may feel offensive to people who are used to knowing what they're doing, but a few weeks really isn't that much time in the grand scheme of things.
What Rust does differently is making ownership and unique/shared references a central feature of the language in a way that is amenable to static analysis. Obviously this is often a struggle for programmers to get used to, there's no denying that, but I don't think it's the concepts themselves that are unfamiliar. Unless of course the programmer is new to low level languages.
The big borrow checker hurdle comes from developers not being used to thinking in/about lifetimes. It takes some getting used to because it enforces constraints that should be there for a non-GC language, but are not enforced anywhere else. But it is a hurdle you can get over after a few weeks of feeling like an idiot. At one point it just clicks. You start to design your types and code with lifetimes in mind, and the borrow checker is not a problem anymore and feels natural - and a helpful safeguard even.
The other complex part is the lack of inheritance and the trait system, which is also foreign for many. I feel like if you had contact with ML languages, and Haskell in particular, you will have a much easier time. It takes a different approach for structuring code.
Sadly Rust is in fact getting mroe more complex all the time though, with things like async/await added and other complex features in the pipeline (like specialization, higher kinded types, generators, ...).
All of those make sense and make the language more powerful, but at the expense of simplicity.
Some of those have fairly simple semantics though.
Generators are really just very a very big enum, with a function moving through the different states of the enum. Async/await is just a fancy name for a generator that returns a Poll.
Specialization is very constrained - it only applies to trait methods qualified with "default"), but I agree the current rules around it are rather complex, and they aren't even sound so they'll probably get even more complex.
HKT has no concrete plan so it's hard to know how much mental overhead it will add. But it's likely to be very non-trivial too.
I'd disagree with that. Yes technically, a generator is just a enum representing a state machine that can be stepped through. And async/await is just a generator with some extra compiler utility.
But they completely change how code is executed, to the point that you don't really think about it as a state machine and have to work hard (mentally) to reconstruct what is happening under the hood. Debugging gets much more complex. And it also adds another part that you need to understand and work with.
Async/await is particularly bad because it's not a standalone feature: it comes with a large ecosystem of crates that provide streams, executors, reactors, timers, ....
It's a huge strain on the complexity budget.
Complexity doesn't bother me (obviously, since I use C++ regularly) but lack of expressiveness is a big issue.
Additionally, the goal of unsafe is not to throw away all safety; it's to prove to the compiler something that you know, but it does not know. This means that you can contain the unsafety, and still gain the benefits as a user. The goal is to isolate and contain it, making it easier to write correctly.
Others say that they love it because it enforces how they write their code anyway, but now with compiler checks that prevent them from messing up.
I think it's a fair criticism that the borrow checker is sometimes not powerful enough to understand a perfectly valid access pattern, but unsafe is always there if you know better.
More often than not (in my experience), the access pattern is not actually valid though, or risky and easy to mess up. And the compiler rubs your nose in a suboptimal design.
I've found that when I needed to reach for `unsafe`, I often could restructure the code instead to achieve a safe result with minimal performance loss.
This!
I agree that the trait system is less familiar to most programmers than inheritance, but I'm not convinced it's more complex; inheritance itself isn't really something I'd describe as simple, just something that people are more familiar with.
The syntax is strict, but this avoids some bugs that would have taken time to spot at runtime.
For some use cases, this is a time saver. For everything else, you may spend more time fighting the compiler than you would have done debugging these issues afterwards.
But yeah, it's a language that constantly makes you feel stupid. The compiler is barfing errors that are hard to understand, for code that looks trivial.
That's a typical experience, but it's not because Rust is that complicated, but because it's different.
People approach Rust with their intuition from other languages, such as C's or Java's where you can have a web of objects referencing each other willy-nilly, and are stumped by Rust's insistence on clear, single ownership, and simpler tree/DAG-like structure of program's data.
Single ownership itself is simple, you just need to unlearn multiple-shared-ownership habits.
A data structure isn't something you import from a module. A good program is composed of layers of bespoke data structures that culminate in a giant data structure tailored for a particular task. So the fact that you can just import a hash table or tree implementation (friction-free) is irrelevant; useful, but irrelevant because it's combining such data structures that is the primary task at hand when writing a non-trivial piece of software, and combining them necessarily results in complicated ownership dependencies.
And this is all especially true for applications that do more than transform some input into some output in one shot.
I'm not trying to criticize Rust. They took a good idea and ran with it, and expressing more complex ownership semantics is still an open problem (perhaps with no satisfying answer, ever). But minimizing or dismissing the sore points isn't constructive.
That is not a difficult relationship to capture, IMHO. The problem is that it's too easy to conflate "address" and "name" in most languages. An address is the physical location of a thing. A name is a location-independent way to look up a thing. Only one thing can live at an address, but you can use a name to look up multiple addresses depending on what question you want answered. (There is a parallel here with domain names and IP addresses.)
Rust associates ownership with every address. This causes serious problems when you've been using addresses as names. The solution is to use different names.
For instance, let's represent a graph as a vector of nodes. Every node has an index within that vector. Given this index, we can look up any node -- but critically, having the index does not confer ownership of the node. It's a completely orthogonal concern. So a node can quite happily possess a vector of the indices of its neighbors.
You might complain that this is a poor naming scheme for a graph structure, since it complicates deletion of nodes from the vector. Great! We have these exact same problems when using pointers (observe that the index plus the vector base address is the node address), but now that naming is a first-class concept, we can consider it on its own terms. There are alternative index spaces we can apply.
I've used this kind of design in a compiler hobby project. The intermediate representation is a set of tables representing different information about the program. With each pass, I can easily add new tables or transform existing ones, so long as I keep the names consistent. I can easily represent relationships between entities by using their indices. And I can easily attach more information to certain entities in compiler passes by adding a new table keyed on the same index.
And of course the extra indirection is costly, especially for a systems language. And you're also doing much more memory management. Hacks like these are why people say handling OOM is too difficult, because such hacks turn a simple logical operation into a series of multiple memory allocations, each of which could individually fail, which explodes the number of possible failure points, creating significantly more tension than necessary between memory management and functional clarity.
[1] The analog in the land of pointers is simply to NULL node member pointers and fail if a member is non-NULL at destruction or insertion, which is effectively the same as with the index tables hack. And for type safety you can of course use container-specific member types, which is exactly how the canonical linked-list and tree implementations work in BSD. (See https://man.openbsd.org/queue#LIST_EXAMPLE and https://man.openbsd.org/tree#EXAMPLES.) And like with many things in Rust, but-for the single ownership limitation Rust would otherwise make doing all of this much easier because it makes it easier to maintain the NULL invariants.
As someone who works with Rust often, your assertions don't match my experience. I want to engage honestly and understand where Rust itself falters, versus where we need to invest in educating newcomers about what kinds of models are better represented in Rust than others. I have found the model I'm describing to be valuable in many ways -- more than a "hack" to get around Rust.
> And of course the extra indirection is costly, especially for a systems language.
To my knowledge, base-offset addressing is natively present on at least x86. With a vector-based index space like I was describing, a[b] == * (a + b) is no more costly than * b. If you would need a more complex index space, you were probably doing more than bare pointers already.
> And you're also doing much more memory management.
> because such hacks turn a simple logical operation into a series of multiple memory allocations
No, no more than you would have needed with pointer-based naming. The top-level arena (a vector in the graph example) owns everything and can be deallocated all at once when you're done with it. Arenas were already a well-known pattern before Rust, and they can reduce the amount of manual memory management over tracking a bunch of small, separate heap allocations.
Relatedly, Catherine West gave an excellent talk at RustConf 2018 on how the Entity-Component-System architecture maps nicely into Rust [2]. The address/name distinction comes from ECS, but it becomes even more useful in Rust.
> The analog in the land of pointers is simply to NULL node member pointers and fail if a member is non-NULL at destruction or insertion
I don't see how this is hindered by Rust. We use an Option type -- with Some(T) and None variants -- to model something that might not be present, so the logic can be exactly the same. Rust can even optimize away the "extra" space you would need for the Some/None variant flag in common situations [3].
[1] https://news.ycombinator.com/item?id=20822003
The indexing scheme is a hack to subvert Rust's typing system. It's even admitted in the talk you mentioned,
> One more criticism of this style is that you might say that using indexes instead of pointers is “safe” in the strict sense, but possibly only technically, you’re trading UB and potential crashes with pointers for “random but unspecified” behavior if you access the wrong or outdated index, and potentially panics. You’d be right, btw!
The generational indexing discussed therein is just a runtime invariant assertion, just as-if you asserted on internal pointer members being NULL at destruction (which implies but doesn't guarantee that the node has been properly unlinked).
This is all just more memory games.[1] In some respects they may be marginally safer, but they're also marginally more complicated. And that's friction. And even if on the whole it makes programs safer, the cost exists. ATS is an even safer language than Rust. If we're gonna pretend that the friction doesn't matter why wouldn't we just pretend we should all use ATS?
I'm not arguing that C or C++ is better than Rust. It just bugs me the way people rationalize some of the difficulty with arguments about why it makes things easier. Yeah, single-ownership does make things easier. Good C programs and good programmers generally learn to strive for single ownership semantics. Without an argument, Rust's enforcement of this invariant makes programs safer. But arguing that the enforcement of the invariant makes it easier to program? Come on! I know single-ownership is safer and simpler, which means when I choose to have a more complex ownership pattern it's for a darned good reason. Having to jump through hoops to accomplish it, even if it's "good" for me, is still jumping through hoops.
[1] And game makers love memory hacks; it's why their apps crash all the time, and why the thought of handling allocation failure would never cross their minds. To understand why people stopped using arenas and ad hoc invariant assertions for systems programming, see Heartbleed.
Of course you're doing more than bare pointers. Which is why your example isn't applicable. (a + b) is fast when you're accessing record fields, because you already have a in a register and b is usually a constant. In the vector scheme you're doing (v + a + b), where v needs to be fetched (often through another level of indirection), and unless you're doing a long series of operation to the same structure, you're constantly reloading v and recalculating a.[1]
It doesn't follow that technique A is equivalent to B simply because when you reduce B to a microbenchmark the difference to A is de minimis. Microbenchmarks obscure and often elide the very reasons B would be slower than A. That said, I'm usually dubious of such performance arguments. This level of performance rarely matters, though when you do it ubiquitously it's more likely to matter, which is why it crossed my mind.
[1] It's often pointed out that loops like `for (size_t i = 0; i < n; i++) {}` are as fast as `for (; p < pe; p++) { ... }`, and that the former is preferable for various dubious reasons related to the ease of implementing boundary checks. But the performance is often the same because (and to the extent that) the compiler can reduce the former to the latter. Even today I regularly encounter situations where using pointers is cleaner and faster than indexing. Though that fact that it's faster is more a novelty; what matters most to me is clarity and correctness, which is highly context dependent.
We agree.
> You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
The clarity and simplicity of a single ownership model trumps just about everything else for me.
Yes, it is possible to awkwardly and inefficiently shoehorn these cases into an N == 1 model but it comes at a substantial cost.
There are awkward cases (self-referential structs, ugh), but let's not forget that in many many cases there are straightforward, zero-cost solutions. E.g. parent incorrectly stated you can't put one object in multiple containers. You can: one will own it, the rest will borrow it. And if you can't explain in your program which one is the owner, chances are you have the same problem in other languages:
• Many C programs with very complex and performance-sensitive ownership just use arenas. This pattern works in Rust, too (at zero cost).
• If you're certain you can get the custom ownership right with C pointers, Rust has C pointers, too (at zero runtime cost).
• When ownership is truly dynamic, C/C++ programs also have to use some kind of refcount or a flag to track it (e.g. setting freed pointer to null == Option.take() in Rust, bool free_me = borrow::Cow in Rust). Rust's refcounting is very efficient (intrusive, atomics not required), equal or better than `shared_ptr`.
With practice the frequency of "awkwardness" when writing code drops. You may still need to be more precise/restrained (especially around mutability and global state), but that pays off as soon as you want to make your code multi-threaded.
I mostly work on high-end database engines, which appear to violate assumptions of Rust about what a sensible memory model can look like. In a modern database engine, almost all of your working memory is unavoidably paged, opaque, and directly DMA-ed. The concept of a lifetime cannot be attached to a memory address and virtually all of your data structures are "unsafe" in a Rust sense. You have to consider implications like destructors being effectively non-deterministic.
Fortunately, schedule-based safety models are transparent to developers, do not require "borrowing" so it works well with references not visible at compile-time, and all references can be mutable. To be clear, these models don't generalize to all software either, they are just well-suited to the requirements of high-throughput server engines. High-scale database engines are effectively single-threaded, so the only cost is a relatively trivial number of non-atomic ref counts. The underlying C++ to make this be handled automagically from a developer perspective is not trivial though.
Rust's memory safety model looks like it was designed for more traditional applications, like web browsers. Modern server software that is I/O intensive is going to look pretty similar to the above if performance matters, and that is a rough fit for Rust's memory model.
First, loose coupling through the layers of the stack probably gives up an order of magnitude in throughput on the same hardware versus a state-of-the-art tightly coupled architecture for many workloads. Many optimizations are enabled when high level orchestration and operation semantics (like a SQL join) are understood at every level of the stack down to DMA scheduling. Most open source databases use loose coupling because (1) it is much simpler to design and (2) developers can reuse third-party code for parts they lack the expertise or resources to implement themselves.
Second, RocksDB is a modern implementation of an obsolete design. As a practical matter, this has a high cost in terms of storage scalability and throughput. Many open source projects use it because they don't have a clear idea of how to design something better. Applications where storage engine performance and scalability are critical, e.g. real-time sensor data models, is an area where open source is not remotely competitive at the moment.
Unfortunately, these criticisms could be leveled at most open source database engine designs, which tend to optimize for expediency and ease of implementation rather than performance, and with little awareness of how much performance is being sacrificed. The divergence between academic literature and state-of-the-art in databases is now almost unrecognizable, and the handful of people with expertise in high-end implementation are not working on open source systems.
The only other post-fix operator in rust is `?` which will return early and do error conversion like the old `try!` macro.
Both of these operators are post-fix so that you can easily intermix them with method calls and member accesses without temporaries. Going from a sync function to an async function is often little more than putting `async` in the function prototype and sprinkling some `.await`s in. If `.await` wasn't post-fix you would need to rewrite a large number of lines to introduce new temporaries.
Ya it looks a little weird at first but this is a pretty lazy criticism that doesn't hold water for anyone who's spent more than 5 minutes looking at async Rust code.
To be fair to your parent, this exact critique was brought up by many experienced Rustaceans during the development of async/await. In the end, it was decided that this drawback is worth it, but this is a drawback.
Just because they're experienced doesn't make it a great argument. Additionally they generally articulated more substantial concerns than "it looks weird."
To be exact my argument is that strange syntax does not increase the complexity of the feature. The complexity of async/await, especially when compared to features like the borrow checker as CJefferson complained, is in its semantics. You need to learn general async/await semantics like the fact that awaits always have yields regardless of which side of the expression you put it on among other things. And a number of rust-specific complexities that are very different from other languages (cold by default futures, executors vs reactors, pinning, etc.)
Neither does saying "this is a pretty lazy criticism that doesn't hold water for anyone who's spent more than 5 minutes looking at async Rust code." It clearly does and did.
Now I wonder what other bits of rust examples I've been misreading. What other postfix operators are there where I might assume it's a member?
That is not necessarily true. Not sure in the case of Nim or Kotlin Native, but the GC itself can be written in Nim/Kotlin Native in principle as well.
It's not a crazy idea. Go actually works that way already; the runtime of Go is implemented in Go. For example, here's the sweeping component of the gc: https://github.com/golang/go/blob/master/src/runtime/mgcswee... But that doesn't mean you can get a Go without a runtime; the final executable's GC may have come from compiled Go rather than something else, but it still irreducibly has GC in it. The whole thing is bootstrapped, so at any given time when some bit of Go code is running, it is supported by GC and the rest of the non-optional runtime, even when compiling code that itself is going to be the runtime of some other executable in the future.
There's nothing simple about Rust.
The compiler is complex and slow.
The borrower semantics is anything but simple.
It isn't uncommon for the syntax to look arcane.
The standard library is shallow often making developers have to resort to third party libraries for trivial things.
Rust improved upon languages like Cyclone but it's a far cry from being the next C/C++ when it comes to potential market adoption.
I'd rather have a thriving and single (looking at you Python) ecosystem of third-party packages, than a standards-driven pile-on of standard libraries, with all the footguns and deprecation that tends to come with it.
That said: almost all your points apply to C++.
There's nothing simple about C++.
it can be incredibly complex and slow (though faster than Rust, but that's not much of an achievement).
The rules around what constitutes valid (non-UB) code is anything but simple.
It isn't uncommon for the syntax to look arcane (IMO much more so than Rust, although obviously YMMV).
The standard library is extremely shallow, making developers have to resort to third party libraries for trivial things, except there's no package manager or standard build system, so good luck with that.
And yet, here we are, with C++ dominating the market. So I don't think any of those reasons are dealbreakers at all.
I believe Rust has the potential for market adoption as a serious C++ competitor. It still has a long road to get there, but C++ didn't reach this point in a day either. We see more and more adoption in big companies of Rust. Microsoft recently expressed interest in using rust in critical components to reduce security bugs for instance. More and more companies are experimenting with Rust, and some are even using it in production.
Besides, there's another victory to Rust: more and more languages are looking into integrating borrow semantics in their own language, like swift[0]. This is proof that Rust's core ideas work. If those trickle down to other languages, the "complexity" of borrows will probably become just one of the other things you just have to get used to to learn programming.
[0]: https://github.com/apple/swift/blob/master/docs/OwnershipMan...
And nobody here said C++ was a good language. Actually, is it even considered a systems programming language?
Yes, absolutely. It's really not even under any sort of doubt at all..?
It doesn't mean that the language is complex.
> The borrower semantics is anything but simple.
The borrow checker doesn't change the meaning of the program in any way. You can perfectly understand Rust code without any knowledge of the borrow checker (there is a working Rust compiler that doesn't even have a borrow checker), so it cannot make the semantics of the language more complicated.
> It isn't uncommon for the syntax to look arcane.
Again, I did call out the syntax has being moderately complicated. The great thing about syntax is that it's superficial. Syntax has to be pretty bad to continue being an issue beyond the initial stages of learning a language, and I don't think Rust's syntax is close to being that bad, it just has some unfamiliar elements to a lot of people.
> The standard library is shallow often making developers have to resort to third party libraries for trivial things.
This sits on the "Rust is simple" side.
> Rust improved upon languages like Cyclone but it's a far cry from being the next C/C++ when it comes to potential market adoption.
In terms of actual market adoption I'd agree. But you said potential market adoption, so I don't.
This means that it's possible to write a compiler that correctly compiles all correct rust programs without having a lifetime system at all. It also means that having the lifetime system in makes writing programs more complicated, but it makes reading them, and understanding what a program does, easier.
The type system is still relatively simple compared the type systems in Java/C#/C++/etc. Let's take Java's as an example because I think people regard it as being simple:
Building blocks of the type system: Rust: Types, Traits Java: Primitive types, classes, interfaces, (value types yet?)
Relationships between those building blocks: Rust: Implements (Type <=> Trait) Java: Extends (Class => Class | Interface => Interface), Implements (Class => Interface), Auto-boxes (Primitive/value type => Class)
Generics: Works mostly the same in both languages. In Java you have to deal with the side-effects of type-erasure from time to time, and there's also the `? extends Y` syntax, for which Rust has no counterpart. In Rust you can have a generic parameter which is a lifetime, for which Java has no counterpart. Again though, these lifetimes are only used by the borrow checker, unlike normal type parameters, they do not affect the generated code.
Java appears simple because it is built on OOP concepts that are very familiar to most people. However, those OOP concepts contain a huge amount of complexity (inheritance trees, constructors, abstract classes, virtual/final methods, monitors, etc) and Rust simply gets rid of all of those concepts entirely. That's why I think it's simple.
I keep seeing people saying this when discussing Rust in relation to C++. It's not untrue, but I would counter by saying that people who were writing C++ without having a full understanding of ownership and lifetime semantics were writing buggy code. Rust just makes understanding those semantics required to get your code to compile.
Anywhere you have any kind of containment hiearchy, Rust starts becoming more invasive than other languages. In languages like Go, you can have complex graphs of objects referring to each other, and you can — often arguably safely — work with these structures without thinking about who owns what.
As an example of something that got me stuck, I recently had some code that populated a map of mutable buffers:
struct Builder {
buffers: BTreeMap<Term, RefCell<PostingBuffer>>,
}
At the end of the building, it needs to flush the buffers, in key order, to a file and then empty the map so it can be reused. Turns out this is trickier than expected because the map owns its contents, so you can't take ownership of the RefCell that wraps the buffers: for (term, buf) in &self.buffers {
// into_inner consumes buf, so it moves; fails with "Cannot move out of borrowed context"
let data = buf.into_inner().get_data();
w.write(term, data)?;
}
self.buffers = BTreeMap::new();
In the end, the trick was to replace the map for the iteration: for (term, v) in std::mem::replace(&mut self.buffers, BTreeMap::new()) {
let data = v.into_inner().get_data();
w.write(term, data)?;
}
Initially I used a HashMap to optimize for build speed, then sorted its keys at the end. This was also a challenge, because maps apparently have no way of getting a copy of the keys by value. (There might be an easier way, but again, this just illustrates the learning curve for new developers.) In the end, I chose BTreeMap so the keys are already sorted.This is all probably entirely obvious to Rust experts, but not so to new developers. Swapping out the entire map with std::mem::replace() would never have occurred to me. Even if you understand borrowing in principle, you have think about a container means in terms of borrowing, and how stuff will move in and out of a container. And you have to design the way you interact with them accordingly.
In principle, I think this awareness is a good thing, and that the resultant code will be smarter and more optimal as a result, but at the same time, it doesn't make for such an ergonomic developer experience; so far it's been a drain on my productivity more than a gain. I like to say that Rust "scales down" towards the low levels, but does less well "scaling up"; you can't write high-level code without also thinking about the low levels, when even things like the size of your data type is always in your face.
My solution above may not even be the most optimal, idiomatic way of doing things. I'm sure someone will come along and point out that there's a trick involving using something other than RefCell or whatever that simplifies everything.
Not necessarily a criticism of Rust, just a data point in terms of complexity and learning curve.
I think that if you wrote
for (term, buff) in self.buffers {
it should have Just Worked. By iterating over a reference to self.buffers, you iterate over a reference to the contents, which you can't move out of. But iterating over it by value, it should give it to you by value, and you'd be fine. That said, I have not tried it, so there might be some context I'm missing. One nice thing about a strict compiler is that I have to think about the details less, and use the error messages to help me fix any problems I find. But that makes it harder to know how it goes without the compiler...The reason I can't just iterate by value is that I'd be consuming data that is already held by the map. The buffer is defined thus:
use bitstream_io::{BigEndian, BitWriter};
struct PostingBuffer<W: io::Write> {
bw: BitWriter<W, BigEndian>,
// ...
}
impl PostingBuffer {
pub fn get_data(self) -> W {
self.bw.into_writer()
}
}
bitstream_io::BitWriter's [1] into_writer consumes itself: pub fn into_writer(self) -> W {
self.writer
}
[1] https://docs.rs/bitstream-io/0.8.2/bitstream_io/write/struct...This is exactly why I couched my last paragraph the way I did, heh.
I took CS 101 ~ 301 in college, and have worked briefly in C++ here and there during my career. I like to think I have a working understanding of pointers and references.
Despite that, whenever I go into a C++ codebase (recently, the Chromium codebase) I get _completely lost_ trying to figure out what the owner and lifetime of an object is, whether it's appropriate to pass by value, reference, or clone. I have absolutely no clue the impact of these decisions aside from a vague sense that pass-by-reference is more memory efficient and cloning is safest (When do we care? Is it something you can just pick one and forget about until performance matters?).
How do you start to pick up an intuition for manually working with memory this way? Is there a set of references that can take you past the beginner level? I find the C++ reference documentation completely opaque, and most search hits tend to be too domain specific to be useful.
Is this something that Rust would help with? Is their model easier to understand when each case is correct? Is it something that comes with language exposure? Or tooling?
It took me a little bit to get used to, but learning this definitely helped me learn to think about ownership and lifetimes better. I highly recommend you try learning some Rust.
While I did preserve to make it work, not everyone is willing to keep trying.
Default assumption: all data lives in a value type, or an STL container. The destructor/copy constructors will automatically deal with lifetimes for you.
Fallback #1: the remainder lives in a std::unique_ptr, or perhaps std::shared_ptr. The destructor/copy constructors will deal with lifetimes for you.
Fallback #2: Take a moment to reflect upon the mistakes that you've made that have led you here. This is your fault. Write a class with correct constructor, destructor, copy constructor, move constructor, and move/copy assignment operators. This is roughly the equivalent of resorting to unsafe in rust in a place that makes calls to other rust code. (as opposed to a syscall or a c function call or somewhere else that it's obviously required) If you're here, it's because you fucked up.
Since c++11 had become available, I've eliminated all use of new/delete from new code that I write. I've slowly refactored old code (by myself and others) to do the same. The only significant attention I've paid to lifetimes is when I'm using a c library or am writing c# where half my objects are IDisposable. (yes, really, I do more manual memory management in a garbage collected language)
The problem with c++ isn't that you have to worry about lifetimes, it's that you used to have to worry about object lifetimes and many codebases were written during that time. Sure, we could rewrite the world in rust and all of that would go away, or we could rewrite the world in modern c++ and it would also go away.
It is still in the early stages and work is being done to make it a common feature across major compiler, while improving their capabilities.
>but it's a far cry from being the next C/C++ when it comes to potential market adoption.
Because C++ is very similarly complex/arcane etc.
I was with you till this point. Rust is ugly, complex, but far batter that C++ in this regards.
* Functions instead of 666 constructors with obscure rules of which you should implement in pairs
* Hygienic macros instead of that ugly unsafe mess in C and C++ called macro and templates
* Move by default and Copy by calling a copy() function
* Everything is so explicit
But the best parts of Rust are its move semantics, enum datatypes and traits, everything that makes programs a joy to express.
[1]: https://ziglang.org
I've seen ppl say that something seems amiss with the V project.
That are trying to get into the C replacement business.
The fact that Mozilla was behind Rust played a lot in Rust becoming popular.
But a lot of nice languages have stayed under the radar, and have been eventually abandoned.
To succeed, Zig needs maturity, but also marketing. And that is hard without some big name advocating it.
It's like when bands cover songs. Who cares if another band plays the song if they don't put their own spin on it?
So it's more that you don't care for the tool than the language in this case. Although, I invite you to try ripgrep again and/or lay out some of your gripes with it. I find it a very good replacement for grep.
alias rg="rg -uuu"
That will disable all smart filtering and make it behave more like `grep -r`. Defaults matter and a lot of folks appreciate them, including plenty of old Unix greybeards that I've talked to about it. (I used plain grep myself for well over ten years before writing ripgrep.) But the defaults don't need to pin you down. They can be disabled very easily. :)I'm glad people are enjoying rust, and sure, memory safety is nice. Fun and comfort are even better, though, so I'll stick to C, Lua and Go for my personal projects.