I am in the planning stage of what will be a large, expensive, long-lived project at ${DAYJOB} and need to make some choices concerning our tech stack that will have serious consequences for the organization. Our current team of 5 people is made up of experienced programmers (average 8 years in industry) who I would consider to be of average ability in both C++ and Rust. Performance and integration are critical for our app; the latency incurred by garbage collection or a heavy runtime are unacceptable, and we cannot rewrite all our dependencies (mostly C libraries). This seems to leave us with few options other than writing in Rust or C++.
Although I think Rust is the right choice for this project, a couple of team members have expressed concern about the cost in development speed needed to get Rust's static checks. My experience is that Rust's superior error handling and error reporting more than justify the need for careful initial design of a Rust app, but the team members express unease with issues like #25860Implied bounds on nested references + variance = soundness hole.
I looked into #25860, and this particular issue is worrisome because it has been known for a decade, its impact on the ecosystem is unknown, and the Rust team has not communicated to the community a concrete plan to address it. The Rust team has communicated that a fix will be attempted after the next-generation trait solver is in production, but it is difficult to identify which specific features of the next-generation solver enable a fix and how specifically those features will be used. Nobody on my team is an expert in type systems and we aren't qualified to judge if a solution is theoretically possible even with industry experts working on it. No (reasonably-expressive) safe subset of the language or other workaround for the issue has been identified, leaving us with a difficult decision. Although I have pointed out to my teammates that Rust absolutely does offer static guarantees over C++ (see RustBelt: Securing the Foundations of the Rust Programming Language) they remain unconvinced that these guarantees are strong enough today to justify going with Rust over C++.
In light of these things I need to take the team's concerns seriously. Since my role is more "technical intermediary between devs and management" than actual developer, I'm not in a position to override the team's preferences without strong evidence that they are misguided. Is there any information (especially concerning #25860 specifically) that I should be made aware of?
For whatever it’s worth, I’ve yet to run into compiler unsoundness unless intentionally trying to. Though I have seen tricks expose supposed-to-be-unstable features like the never type in stable, safe Rust. Additionally, I’ve seen a bug letting dyn trait objects implement an impossible trait be used to express higher-ranked trait bounds, in a way that feels very unsoundness-adjacent even though AFAIK the exact usage (just expressing a complicated trait bound with less verbosity) was not unsound.
I get the impression that the code triggering unsoundness in safe Rust, compiler bugs, or other odd behavior in the type/trait system is usually not very subtle. It’s doing something crazy with weird trait bounds and/or trait objects. Sure, it means you should probably skim through a dependency’s source to make sure it’s not doing anything obviously shady in safe Rust, but I think that’s necessary for security anyway, and looking through unsafe Rust would remain necessary either way.
People’s buggy unsafe code is a much, much greater threat in my eyes, and I’d like to think that Rust is at least somewhat better than C++ in providing guardrails and instructions on using unsafe functions and operations correctly. That doesn’t stop people from just not reading those instructions and doing something dumb, at least in some of the open-source software I’ve looked at.
I only have slightly over two years of experience in Rust, though, so take my opinion with a grain of salt.
There's no reason to be worried about that particular issue. There are no recorded cases of anyone running into it by accident.
But if you are worried about that kind of thing, then you may also be worried about unsafe code. However, the data pretty clearly shows the benefit even in codebases that use unsafe. See:
Of course, there are serious trade-offs if this is the company's first Rust code. There may be good reasons to use C++. I think Jon said it well in:
The focus on soundness holes in the compiler/language (and moreso one particular hole) seems misdirected to me. If avoiding UB is that high of a consideration, there are much bigger areas to consider (even within Rust, like unsafe-using third-party dependencies; but also outside Rust, like your C dependencies).
Beyond that, I would expect there to be other important and larger considerations, like how comfortable the team is writing primary Rust, writing FFI-focused Rust more specifically, crate maturity in the project area, etc.
So maybe Rust is the right choice and maybe it isn't. I can't know the ideal choice for your particular situation. But if it is not the right choice, I would be surprised if "possibility of unsoundness" was the reason why.
Click to expand the rest of my reply, which are comments on the specific cases you wrote about. But the summary is still "the deciding factor should be something else".
Is the concentration on #25860 just due to its age? I don't think it's a soundness hole that you would accidentally stumble into. It can be exploited if you try to, but if you have a dependency[1] that would try to, you already have much bigger problems. There are other compiler soundness holes which are probably more likely to be hit accidentally. (Still not at all likely to be hit, mind you. Just some holes less impossible to imagine coming up organically due to, e.g., complicated generic implementations.)
But again, even when the concerns are really about accidental UB, it's an odd choice to concentrate on specifically.
Is the unease just that a soundness hole exists? If you have a bunch of C dependencies, I'm certain you can use them in a way that causes UB without doing anything special in your code base, which would be the analog to an unsound Rust library. Is there equal unease about that?
I would posit that in any significantly sized C or Rust code base,[2] there is no absolute soundness, just like there is no absolute security. Since there is no absolute, you strive to make the bad outcome vanishingly likely instead. Rust goes a long way in that regard.
C doesn't have that sort of safe subset, either -- and as far as I'm aware, doesn't aim to, unlike Rust. The ideal in Rust is to require unsafe on your end for UB to be possible (and if it's not, there's unsoundness in a dependency or the compiler). In C, everything is silently unsafe. You have to hope the docs are complete and then do your best to hold things correctly to avoid UB.
In terms of soundness -- the ability to avoid UB in vanilla code -- Rust is a strict upgrade.
I agree with the previous replies, but all concerns regarding UB can basically always be translated directly to C++, so the question isn't so much whether Rust is an improvement in that respect (it is), but whether it's a sufficient improvement to offset the cost.
If there is anything security-critical about the software, e.g. if it will be network-accessible with potential attackers on the network, you should probably push strongly against C++. History has shown that it wouldn't be a matter of whether UB-related security holes exist, but rather a matter of whether/when they will be found.
I would also tend strongly towards Rust if multi-threading will be involved, as that is much easier to get right in Rust compared to C++.
Apart from that, there may not be a strong argument either way. I would personally also expect development velocity to be higher with Rust in general, but that is both subjective and dependent on many factors.
it's also worth keeping in mind that rust is not the only single solution to memory safety problems in programming, and, the memory safety of a particular program is not the only concern for the entire system.
as a programming language, rust solves a large portion of the common human errors related to memory safety (and concurrency), but you still need to pay great attention to all the other problems that are out of the scope of rust.
I recently came across this tweet. let's not jump into a "rust-vs-<insert your alternative>" flame war here, my point is, even if[1] a program[2] is written in 100% safe rust[3], there's still so much potential security vulnerabilities in the system that might compromise the user of the program.
It's more challenging to write Rust bindings for C libraries than it is to write regular Rust code, especially if those libraries use threading, callbacks, etc. It's much more likely you'll have a bug at the foreign function interface than with one of the soundness issues in the Rust compiler. If your team isn't experienced in Rust this is a major issue.
If someone came to this forum professing concerns about Rust's viability because of issues like that one, and argued that C++ was a superior choice categorically because of it, I would conclude that they were a concern troll and that any professed interest in Rust was an excuse to argue against its use. I'd also drop them to the bottom of my ignore list and move on with my day rather than arguing with them. C++'s stable of soundness holes is larger by far, easier to encounter by accident, and still only a limited barrier to getting useful work done in the language. Arcana like that issue barely registers.
You probably don't have that luxury with coworkers, especially as your organization expects you to build coalitions, but I would consider the possibility that your coworker may feel trapped between a genuine and perhaps even well-reasoned preference for C++, on the one hand, and wanting to be a team player with peers who would prefer Rust, on the other. Dubious behaviours can sometimes come from good places, and you might get somewhere by talking to him privately to probe how likely he thinks it is that the linked issue is representative of the problems your team will run into in designing this system. Others have also talked through the very limited prevalance of real-world reports of bugs due to issues like that one, and the competing data showing fewer memory-soundness bugs in programs built with memory-soundness-checking languages like Rust.
Personally speaking, if the design is going to involve extensively wrapping C libraries, I might opt not to use Rust. It's not the catalogue of type-system soundness problems that would concern me, but rather the difficulty in wrapping non-trivial C libraries in Rust while maintaining Rust's memory invariants at the boundaries, dealing with Rust's panic semantics, and in particular dealing with any shear around allocators or threading. It's not that it can't be done - it can, as amply demonstrated by the crate ecosystem - but rather that it's expert work that I wouldn't necessarily want to commit my organization to training new hires on, and which, if done badly, substantially undermines the memory-safety guarantees Rust can provide.
There has been little to no real world code affected by this. However, it is hard, if not impossible, to prove that people haven't ran into this in real world code. If we could have a reliable detector of this unsoundness, the compiler would use it already. Therefore, the best we can have is a lack of evidence of real world code being affected, which is not much, but not nothing.
Consider that C++ is "unsound by default". That is, it's hard if not impossible to write C++ code that's "sound" by Rust's standards. Rust making it really difficult to write unsound code is a clear win in this regard.
As per this comment, fixing this unsoundness requires implementing "where-bounds on binders". This refers to the ability to write something like for<'a, 'b: 'a> fn(&'b i32) -> &'a i32. That is, the ability for a for binder to specify relationships between lifetimes. Implementing this requires the next-generation solver. Once this is implemented, Rust can internally desugar the type of foo in the github issue to be for<'a, 'b: 'a> fn(&'a &'b (), &'b T) -> &'a T, turning the implicit 'b: 'a bound in the type &'a &'b () into an explicit bound. Rust can then detect that substituting 'a with 'static here would violate the 'b: 'a bound, preventing the unsoundness.
Over the last few goal periods, the trait solver went from being an early prototype to being in production use for coherence checking. The goal for 2025H2 is to prepare it for stabilization.
That is, the next-generation trait solver has already been used in parts of the rust language, and we're close to its full stabilization.
For an exhaustive list of known remaining technical issues before stabilization, see this.
It's not unknown. It has zero real-world impact on the ecosystem.
This particular issue requires a combination of references-to-references, implied bounds, coercions, and contravariant lifetimes. This is a rare combination, and the problem can be easily avoided by writing explicit lifetime bounds. In real code, if you had lifetimes as convoluted as these, you'd write their bounds explicitly.
Note that it's not a miscompilation. Lifetimes are only compile-time assertions, and don't impact code generation in any way (valid Rust code can be compiled correctly without having any borrow checker. The mrustc implementation demonstrates that).
This soundness issue merely downgrades one contrived case in Rust to the level of soundness checking you have by default in C++.
In practice, borrow checking works very reliably, and makes a huge difference in large codebases. It does catch problems even in complex code, with generics, async, and multi-threading. The set of all problems it catches[1] is vastly larger than the set of cases it misses, and it's still a stronger correctness guarantee than what you get from C++ static analysis.
Soundness bugs in the compiler come up as a loophole/blocker when users would like to run untrusted and potentially actively intentionally malicious Rust code, under assumption that Rust code without any unsafe can be made harmless. That's not the intent of safe/unsafe split, and Rust doesn't even try give such guarantee (e.g. std::process::Command runs arbitrary code and is not marked unsafe). The safety is for catching mistakes of implicitly trusted programmers, not a sandbox.
or disallows due to limitations of the type system and undecidability ↩︎
The tools exist that spot such (extremely unlikely (soundness)) mistakes when you run them on your tests.
cargo +nightly miri test
error: Undefined Behavior: constructing invalid value: encountered a dangling reference (use-after-free)
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std=std
==8541==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b2dcdbe4010 at pc 0x5614c5678e07 bp 0x7b0dcb7fcc50 sp 0x7b0dcb7fcc48
RUSTFLAGS="-Zsanitizer=memory" cargo +nightly test -Zbuild-std=std --target=x86_64-unknown-linux-gnu
==9642==WARNING: MemorySanitizer: use-of-uninitialized-value
p.s. also for curiosity sake give c2rust a try on existing library. (Just don't expect it to be pretty.)