Traits in Rust are an amazing feature and contribute significantly to the expressive power of the language. For someone coming to Rust with a C++ background (like me), Traits appear to be quite similar to Abstract Classes in C++. Although they are similar, Traits have certain characteristics that set them apart. In this article, I will try to compare the two and highlight the key differences.
In order to keep the discussion simple and to the point, I will ignore “Generics” in Rust (C++ Templates).
The first similarity is that both Traits and Abstract Classes cannot be instantiated directly. In that sense, we can say that both of them “prescribe” behavior, to be implemented by a conforming abstraction.
Let us start with a simple example in Rust, where we define a “struct” and two “traits”.
Please note that “MyStruct” does not implement any of the two traits yet (we will do this a little later). Another interesting point to note is that it is possible to define methods (not just declare them) in Traits. When we run this code, here is what we get:
How can we implement similar functionality in C++ using Abstract Classes? Pretty straightforward actually. Here is the code:
Here also you can see that “MyStruct” does not (yet) use the two abstract classes “BaseTrait” and “DerivedTrait”. And, as in the case of “Traits”, it is possible to define methods in Abstract Classes. We get the following output when we run the program:
It is easy to see that the outputs of the Rust and C++ programs match so far.
Let us move on to the next step. How do we take advantage of Traits in Rust? We know that Traits “prescribe” behavior, which some abstraction might implement. So, here is our example with “MyStruct” implementing both the Traits:
What you must take note of is that we did not change the original definition of “MyStruct”, but added the extra behaviors independently. This is pretty cool!
Here is the output from the modified program:
What about the C++ example? The typical scenario (let us not get into Design Patterns here) is to derive “MyStruct” from “DerivedTrait” and implement the pure virtual methods “foo()” and “bar()”. The modified version looks like this:
The output from the program, in this case, will be:
The outputs from Rust and C++ match this time too, confirming that the behaviors are the same.
What is the main take-away from these two implementations? Which appeals to you more?
My view is that declaring behaviors through Traits is more natural and flexible compared to doing the same through Abstract Classes. How so? As you can see from our example, “MyStruct” can choose to take on additional behaviors without changing its original definition. In the case of C++, we had to change the definition of “MyStruct” to derive from “DerivedTrait” to take on its behaviors. What if “MyStruct” has to also implement additional behaviors? Of course, since C++ supports multiple inheritance, this might not be a problem, but you can see that the implementation can get a bit convoluted. Other approaches exist, for example, using a “Behavioral Design Pattern”, but that is beyond the scope of this article.
In summary, I feel that Traits in Rust combine the benefits of C++ Abstract Classes, Java Interfaces and Lisp Mixins.
You can download the C++ and Rust source files from here. I used Visual Studio 2022 for the C++ example and Intellij CLion 2021.3 for the Rust example.
Have a nice weekend and a great week ahead!
Recent Comments