Rust
Some time ago a tweet linked me to a blog post arguing that everyone should replace all their C code with Rust code. This implied that Rust was a programming language that could, in principle replace C - and therefore also C++.
The first question I asked was "but does it have a garbage collector?" Garbage collection is a feature of nearly every new language since Java, and not without good reason. For most purposes GC is just a better solution. But for some purposes, GC is a dealbreaker, and programmers who have those purposes must exclude GC'd languages from consideration.Thus, for a very long time those programmers have had to choose between C and C++.
Rust does not have a garbage collector. So I read more and it all sounded good enough that I decided to port Unit Testris to Rust. I've just got to the point where the three unit tests found in /tests/ are ported and I figure this is a good time to stop and write about the Experience So Far.
The book warns that the issue that takes up a new Rust programmer's time is dealing with the borrow checker. I feel that it didn't take that long to come to grips with it. What's been more persistent is the issues with value semantics and move vs. copy. When you pass anything to a function by value Rust wants to move it - but if you use that value later, a move is invalid and Rust will silently check the type for the Copy trait. If the type is Copy, the compiler will silently convert the move to a copy. Otherwise, you'll get a compiler error saying you can't move X for reason Y. So I'll do a thing with a Copy type, then do the same thing with not-Copy and get a move error. Most often, the not-Copy type is Clone, and then I generally decide to clone() it.
As I expected when I read the relevant chapter of the Rust Book, the greatest improvement going from C++ to Rust is templates. Rust calls them generics but I've been raised on C++ so I'm calling them by their C++ name. The issue with C++ templates is that they're permissive until the last moment - if you're writing a template function with type parameter T, and write t.frob(), C++ will allow it and thenceforth anyone who tries to call that function with a type that doesn't have a method named frob that takes no arguments will get a compiler error pointing to that specific line of your template. This is not the most helpful message. A solution has been proposed but has not yet been adopted into the most recent C++ standard: Concepts. Quoting from the link:
Formal specification of concepts (ISO/IEC TS 19217:2015) is an experimental technical specification, which makes it possible to verify that template arguments satisfy the expectations of a template or function during overload resolution and template specialization.
The library concepts listed on this page are the named requirements used in the normative text of the C++ standard to define the expectations of the standard library. It is expected that a future version of the C++ standard library will formalize these library concepts using the facilities of the above-mentioned technical specification. Until then, the burden is on the programmer to ensure that library templates are instantiated with template arguments that satisfy these concepts. Failure to do so may result in very complex compiler diagnostics.
There are at least two problems with concepts: First, they aren't in the standard yet. Second, they're not the default. Backwards compatibility will demand that conceptless templates continue to be permitted, and so the correctly restrictive behavior won't occur unless programmers choose to use the new feature.
In Rust, templates are restricted by default, which means that without a trait bound, a variable of a template type is limited to assignment. Doing anything else requires a trait, and so if the user of a template tries to use a type that doesn't support a needed operation, they'll get an error identifying the trait that defines that operation. The worst time I've had from Rust templates so far was due to the standard library implementing IntoIterator on references to arrays but not array values, and a confusing error message from the compiler trying a templated implementation of that same trait.
Rust's superior templates are also how I supported mocking without compromising performance. Rust doesn't copy the C/C++ compilation model, so I can't use it to isolate code for no additional charge. But Traits fill the role of both Concepts and Interfaces, allowing the same mechanism to control dynamic and static dispatch. Consider this implementation of the House example:
mod dependencies { pub struct Sink; impl Sink { pub fn new() -> Self { Sink } } pub struct Dishwasher; impl Dishwasher { pub fn new() -> Self { Dishwasher } } pub struct Refrigerator { is_locked: bool } impl Refrigerator { pub fn new() -> Self { Refrigerator { is_locked: false } } pub fn lock(&mut self) -> () { self.is_locked = true; } pub fn is_locked(&self) -> bool { self.is_locked } } } mod house { use dependencies::*; trait ISink { fn new() -> Self; } impl ISink for Sink { fn new() -> Self { Sink::new() } } trait IDishwasher { fn new() -> Self; } impl IDishwasher for Dishwasher { fn new() -> Self { Dishwasher::new() } } trait IRefrigerator { fn new() -> Self; fn lock(&mut self) -> (); } impl IRefrigerator for Refrigerator { fn new() -> Self { Refrigerator::new() } fn lock(&mut self) -> () { self.lock() } } struct HouseImplementation<S: ISink, D: IDishwasher, R: IRefrigerator> { sink: S, dishwasher: D, refrigerator: R, is_locked: bool } pub struct House { pimpl: HouseImplementation<Sink, Dishwasher, Refrigerator> } impl<S: ISink, D: IDishwasher, R: IRefrigerator> HouseImplementation<S, D, R> { fn new() -> Self { HouseImplementation { sink: S::new(), dishwasher: D::new(), refrigerator: R::new(), is_locked: false } } fn is_locked(&self) -> bool { self.is_locked } fn lock(&mut self) { self.is_locked = true; self.refrigerator.lock(); } } impl House { pub fn new() -> Self { House { pimpl: HouseImplementation::new() } } pub fn lock(&mut self) { self.pimpl.lock() } pub fn is_locked(&self) -> bool { self.pimpl.is_locked() } } }
With this setup, a house::test module can create a MockRefigerator, et. al., impl IRefrigerator for MockRefigerator, etc. and then instantiate HouseImplementation<MockSink, MockDishwasher, MockRefigerator>. I get value semantics and static dispatch while substituting mocks just as if I used reference semantics and dynamic dispatch. Further, if I used dynamic dispatch, I could, and would, use the same Traits.
Speaking of those traits, note that they're defined in mod house, rather than in mod dependencies. That's because the traits exist for the benefit of HouseImplementation. For example, IRefrigerator doesn't cover all the methods of Refrigerator - only those actually used by HouseImplementation. A different module could define its own IRefrigerator to cover the parts of Refrigerator that it uses. In Unit Testris, piece.rs and game.rs each have their own trait IField. Because they are both sparse traits, the two MockField structs get to skip implementing a bunch of functions that will never be called.
C++ templates could obtain the same result, but I prefer using the compilation model.
I strongly suspect that when I start porting implementation of the classes, Rust's safety rules will compel changes to the tests.






