Jonathan Boccara's blog

Strong Types on Collections

Published July 26, 2019 - 0 Comments

Do we need a special strong type library for collections? Or can we strongly type collections like we do for any object?

If you’re joining us right now and haven’t read the previous articles on strong types, long story short, a strong type is a type used instead of another one in order to add meaning via its name.

Long story a little less short: check out this way to define a strong type in C++ with a library and that way to define one with native C++ features.

And long story long: here is the ever-growing series on strong types on Fluent C++:

Strong typing on a collection: motivating example

As a motivating example to strongly type collections, consider the following class:

class Team
{
public:
    template<typename... TEmployee>
    Team(TEmployee&&... teamMembers) : teamMembers_{std::forward<TEmployee>(teamMembers)...} {}
    
    std::vector<Employee> const& get() const { return teamMembers_; }
private:
    std::vector<Employee> teamMembers_;
};

It represents a team of people, which is not much more than a vector of Employees, but than we’d like to see tagged as “team” in the code that uses it.

This code is inspired (quite largely) from a piece of code I came across recently. It wasn’t about teams and employees, but that was the general gist of it.

Its purpose is to allow the nice following syntax:

auto team1 = Team(Alice, Bob, Tom);
auto team2 = Team(Arthur, Trillian);

Also, Team is a type that is different from std::vector<Employee>, and if there were another concept of grouping employees together, it would be yet another type, different from Team.

Granted, maybe there isn’t so many ways to group employees together. But if you replace Employee with int, then there are many more possible meanings to give to std::vector<int>, and it could be useful to make sure we don’t mix them up, by giving each one its specific type. A typical example of mixup is to pass several of them in the wrong order to a function.

All this works well for teams and for ints, but we can imagine that it would equally apply to other groups of stuff. It would be nice to make this code generic, and have a facility to strongly type collections.

We already have a library that performs strong typing on C++ objects: NamedType. Can it spare us from re-implementing the Team class?

Strong typing on collections

Let’s make an attempt to use NamedType here:

using Team = NamedType<std::vector<Employee>, struct TeamTag>;

That’s a terser declaration. Now let’s have a look at the call site:

auto team1 = Team(std::vector<Employee>{Alice, Bob, Tom});
auto team2 = Team(std::vector<Employee>{Arthur, Trillian});

Ouch. It doesn’t look as nice as before, because of the std::vector<Employee> sticking out.

But before we thing of a way of chopping it off, let’s pause and reflect on whether it is good or bad to make the std::vector show after all.

Clearly, it wasn’t the intent of the initial code. Indeed, the purpose of Team was to encapsulate the raw collection behind a meaningful type. But on the other hand, maybe we do care that it’s a vector. Indeed, as advocated in Effective STL Item 2: “Beware the illusion of container-independent code.” So maybe showing that it’s a vector isn’t such a bad thing.

But on the other other hand, what else would you want it to be? Indeed, Herb Sutter and Andrei Alexandrescu advise to “Use vector by default”, in Item 76 of their popular C++ Coding Standards.

So there are pros and cons to make the vector show, but let’s assume that we would like to hide it. Is there a way to do it and have generic code?

A NamedVector?

One idea is to design a new class alongside NamedType, that would be dedicated to handling vectors:

template <typename T, typename Parameter>
class NamedVector
{
public:
    template<typename... TElement>
    explicit NamedVector(TElement&&... elements) : collection_({std::forward<TElement>(elements)...}) {}

    std::vector<T>& get() { return collection_; }
    std::vector<T> const& get() const {return collection_; }

private:
    std::vector<T> collection_;
};

To instantiate the Team type we would do:

using Team = NamedVector<Employee, struct TeamTag>;

And we get the nice syntax back:

auto team1 = Team(Alice, Bob, Tom);
auto team2 = Team(Arthur, Trillian);

But a generic class like NamedVector has drawbacks: first, there is already a generic class (NamedType) and it would be simpler if there were only one. And what’s more, we’ve made NamedVector but we would also need NamedMap, NamedSet, NamedArray and NamedList (er, ok, maybe not NamedList).

A convenient constructor of std::vector

It turns out that we don’t need NamedVector, because a slight change to the code would make it compile, without showing the underlying std::vector:

using Team = NamedType<std::vector<Employee>, struct TeamTag>;

auto team1 = Team({Alice, Bob, Tom});
auto team2 = Team({Arthur, Trillian});

How does this work? It relies on the constructor of std::vector that accepts an std::initializer_list. And this constructor is not explicit, so we don’t need to type std::vector<Employee> to instantiate it.

An additional pair of braces popped up, but it simplifies a lot the library code.

Have you already encountered the need for a strong vector? Which solution do you prefer: a dedicated Team class, a NamedVector, or a NamedType with std::vector‘s implicit conversion? Do you have another solution?

You may also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin