January 9, 2021

How Playing with Rust Improved My C++

I recently took a tour of several programming languages. The objective was to try to implement the same solution in Rust, Haskel and Clojure. The important part of this exercise was that I hadn’t written anything in any of these languages previously.

I have to say that I really like Rust and the philosophy embodied in how that language approaches things. For example, using Cargo meant that I didn’t need to mess with a build system. With C++ I usually gravitate toward GNU Make or CMake both of which I’m beginning to find are a distraction from the implementation.

The most interesting thing that came out of my experience with Rust was their approach to structures and getters or setters.

One of the problems I have with a lot of C++ code is that the default impluse seems to be to create everything as a class. I prefer to use struct for certain things–usually PODs (Plain Old Data) but with an eye towards how the structure fits into the domain. The idea is that you start with a struct and build up your implementation until you have a strong case for a class.

For example, building up a data structure for managing people might look like this:

struct Person {
   string name;
};

A Person contains a collection of attributes describing a person. Those attributes might reside in a persistent store, such as a database.

struct SelectablePerson: Person {
   bool isSelected;
   SelectablePerson()
       : Person()
       , isSelected(false) { }
};

A SelectablePerson extends a Person to include a selected attribute. This attribute might be represented in a view where the person is selected (included) in a query against the database. The constructors are for convienience and ensure a consistent state for creating selectable people.

In C++, the argument for using a class starts to look appealing because the constructor is starting to push the boundary of a POD. In Rust, your choice is to develop traits to apply to Person. I’m going to take the position that SelectablePerson should remain a struct in C++. That argument rests in the Single Resposibility, Open-Closed and the Liskov Substitution principles (the SOL in SOLID).

In C++, a lot of people will argue you want getters and setters for the name and isSelected attributes. This usually drives them down the path of a class because of member visibility rules (i.e., make these attributes private). I’ve also had arguments with C++ programmers who insist that data members in a class be public because they don’t want to write getters and setters.

Rust has a different approach. By default, stucture members are private and you need to add the pub attribute to make them visible. What surprised me was that a lot of Rust examples forego getters and setters in favour of direct access if you make the attribute public.

The argument in favor of using a getter and setter is encapsulation. You can change the attribute without affecting clients. The counter argument is that the difference between

Person p;
p.name = "brian";

and

Person p;
p.set_name("brian");

isn’t significant. It isn’t significant because the logic surrounding this is just where you want the assignment statement. The first example, puts this assignment in every client class; the second does not.

Rust falls into the first example.

Importantly, I’m not saying you should open up your data members everywhere. I’m saying there is a time and a place for it and you shouldn’t just blindly make everything a class in C++.

Here’s an example where I think a class is appropriate.

class PeopleContainer {
public:
    /*! Container for SelectablePerson. */
    typedef std::vector<SelectablePerson> ContainerType;

    /*! @brief Construct the controller.
     */
    PeopleContainer()
        : container()
        , selected_count(0) { }
private:
    ContainerType container; //< A container for selectable people.
    size_t selected_count; //< A count of the number of people currently selected.
};

Why am I using a class here? The selected_count member requires additional logic to ensure that the number of SelectablePerson.isSelected values set to true is equal to the count. Neither Person nor SelectablePerson have this constraint.

In Rust, I’d introduce a countable trait to create an equivalent construct for the PeopleContainer.

So how did Rust improve my C++? It gave me a better understanding on why I was using struct instead of class in C++. It gave me a better understanding of the importance of SOLID in C++, particularly the Liskov Substitution principle. An important element of Liskov Substituion is type inheritance, not implementation inheritance. There is only type inheritance in these examples.

In all, I’m back to using C++ but I have only good things to say about Rust and look forward to the next opportunity to explore it.

comments powered by Disqus