While programming in Rust, I've never thought to myself, "man, this would be so much easier to express in C++". I've plenty of times thought the reverse while programming in C++ though.
Edit: except when interfacing with C APIs.
Then you must be avoiding situations that traditionally use OOP
Most kinds of OOP can be expressed idiomatically in Rust. The big exception is implementation inheritance, which is highly discouraged in modern code anyway due to its complex and unintuitive semantics. (Specifically, its reliance on "open recursion", and the related "fragile base class" problem)
People often say that modern c++ doesn't have the problems needing a solution like rust. Ironically that means people who write modern c++ haven't had any aramp up time needed when joining our rust projects. They were already doing things the right way. At least mostly. But now they don't have to worry about that one person who seems to be trying to trick the static analysis tools on purpose.
Anything that involves object graphs (as opposed to trees) is a pain in Rust.
True, but not in a way that wouldn't be just as painful in C++.
In Rust, the de facto standard advice for such cases seems to be, "just use indices into an array instead of references".
While this is sometimes done in C++ as well for various reasons, it's certainly not the default pattern there. If you have two things that need to point to each other, you just do that.
> While this is sometimes done in C++ as well for various reasons, it's certainly not the default pattern there. If you have two things that need to point to each other, you just do that.
And then you have to handle all the subtle memory bugs that you've introduced by doing that.
I'm not arguing that there isn't a gain here, but GP's original assertion was that
> While programming in Rust, I've never thought to myself, "man, this would be so much easier to express in C++".
This is a concrete example of something that is much easier to express in C++. And, sure, you do pay the tax for that (although I will also dispute the notion that it is impossible to write C++ without memory bugs; it's just hard).
I guess this is a semantics argument, but I assume they mean to express the same thing with same (or reasonably same) security guarantees. After all, the security and "bug freeness" is part of what they are expressing. If you attempt to create something reasonably similar to Rust, you do suddenly need a lot of complex checking code and maybe tests for things that were trivial in Rust (because the compiler does the tests for you).
Is it really easy to express if the straightforward way is buggy and error-prone?
People think C++ is expressive because they think they are allowed to do a lot of things that they aren't, in fact, allowed to do in C++.
This is interesting because i'm writing quite a bit of embedded Rust, and i always run into limitations of very barebones const generics. I always wish they'd have half the expressiveness of C++ constexpr and templates.
Win some, lose some though, as the overall development workflow is lightyears ahead of C++, mostly due to tooling
The expressiveness of const generics (NTTPs) in C++ wouldn't go away if it adopted lifetime annotations and "safe" scopes. It's entirely orthogonal.
Rust decided to have more restrictive generic programming, with the benefit of early diagnostic of mistakes in generic code. C++ defers that detection to instantiation, which allows the generics to be more expressive, but it's a tradeoff. But this is an entirely different design decision to lifetime tracking.
Rust generics are not intended as a one-to-one replacement for C++ templates. Most complex cases of template-level programming would be addressed with macros (possibly proc macros) in Rust.
Const generic expressions are still being worked. They are what is blocking portable simd. They are also a much cleaner way to implement things like matrix operations or really anything where a function with two or more arguments of one or more types returns things that have types that are a combination or modification of the input types.
The problem AIUI is that "const generic expressions" in full generality are as powerful as dependent types. It's not clear to me that the Rust folks will want to open that particular can of worms.
I thought dependent types were types that depended on a value? What they are proposing are types that depend on types or compile time constants.
The problem is combining the "const generic" and "expression" part. If your "compile time constants" can actually be complex expressions, you arguably end up with the same kind of generality as dependent types.
This is true even for expressions that are only evaluated in a compile-time context, since dependently-typed languages do "everything" at compile time anyway, they don't have a phase distinction where you can talk about "runtime" being separate.
Ah, yeah! I get it now. So c++ is a dependently typed language. That is hilarious. I want lisp syntax in c++29. That said, too many features are blocked on const generic expressions, so I think they are going to have to bite that off. There is already talk about migrating proceduralacros to be something more like normal rust, this moght fit in with that.
C++ is not a dependently typed language, for the same reason that templates do not emit errors until after they are instantiated. All non-type template parameters get fully evaluated at instantiation time so they can be checked concretely.
A truly dependently typed language performs these checks before instantiation time, by evaluating those expressions abstractly. Code that is polymorphic over values is checked for all possible instantiations, and thus its types can actually depend on values that will not be known until runtime.
The classic example is a dynamic array whose type includes its size- you can write something like `concat(vector<int, N>, vector<int, M>) -> vector<int, N + M>` and call this on e.g. arrays you have read from a file or over the network. The compiler doesn't care what N and M are, exactly- it only cares that `concat` always produces a result with the length `N + M`.
I'm not sure what "dependently typed" means but in C++20 and beyond, concepts allow templates to constrain their parameters and issue errors for the templates when they're specialized, before the actual instantiation happens. E.g., a function template with constraints can issue errors if the template arguments (either explicit or deduced from the call-site) don't satisfy the constraints, before the template body is compiled. This was not the case before C++20, where some errors could be issued only upon instantiation. With C++20, in theory, no template needs to be instantiated to validate the template arguments if constraints are provided to check them at specialization-time.
This is the wrong side of the API to make C++20 dependently typed. Concepts let the compiler report errors at the instantiation site of a template, but they don't do anything to let the compiler report errors with the template definition itself (again before instantiation time).
To be clear this distinction is not unique to dependent types, either. Most languages with some form of generics or polymorphism check the definition of the generic function/type/etc against the constraints, so the compiler can report errors before it ever sees any instantiations. This just also happens to be a prerequisite to consider something "dependently typed."
> performs these checks before instantiation time
Notably Rust type-based generics do this, a key difference wrt. C++ templates. (You can use macros if you want checks after instantiation, of course.)
In c++ it does care what N and M are at compile time, at least the optimizer does for autovectorization and unrolling. Would that not be the case with const generic expressions?
The question of whether a language is dependently typed only has to do with how type checking is done. The optimizer doesn't come into play until later, so whether it uses the information is unrelated to whether the language is dependently typed.
Ok, I think I understand now, but is it really dependently typed just because it symbolically verified it can work with any N and M? Because it will only generate code for the instantiations that get used at compile time.
Is what really dependently typed? I'm saying C++ is not dependently typed, because it doesn't do any symbolic verification of N and M.
If rust did add const generic expressions I mean. It still would only generate code for the used instantiations.
Ah, I wasn't really talking about Rust.
Rust already does have some level of const generic expressions, but they are indeed only possible to instantiate with values known at compile time, like C++.
The difficulty of type checking them symbolically still applies regardless of how they're instantiated, but OTOH it doesn't look like Rust is really trying to go that direction.
the only thing needed here is to be able to lift N & M from run-time to the type system (which in C++ as it stands exists only at compile-time). For "small" values of N&M that's doable with switches and instantiations for instance.
The point of dependent types is to check these uses of N and M at compile time symbolically, for all possible values, without having to "lift" their actual concrete values to compile time.
Typical implementations of dependent types do not generate a separate copy of a function for every instantiation, the way C++ does, so they simply do not need the concrete values in the same way.
> as the overall development workflow is lightyears ahead of C++, mostly due to tooling
My experience has been the other way around. Eclipse-based IDEs from NXP, TI, ST all have out-of-the-box usable tooling integration:
- MCU pinout and configuration codegen
- no need to manually fiddle with linker scripts
- static stack and code size analyzers (very helpful for fitting stuff in low-cost MCUs)
- stable JTAG-based debugging with:
- peripheral registers view (with bitfield definitions)
- RTOS threads view (run status, blocked on which resources, ...)
And yes, these are important enough for me to put up with Eclipse and pre-modern C/C++. I really want to write Rust for embedded but struggling with the tooling all the time didn't help. That's actually quite interesting because this is not an inherent limitation of Rust, and it is definitely planned to be improved. And AFAIK, today (as opposed to last years) it is even being actively worked on!