During session 2, I discussed a couple of techniques to use the static type system to let the compiler find my bugs. Here is a “short” summary of the code we created during the session.

For our first problem, assume that you are creating a small library for terminal 2D-graphics (because that’s a very novel idea…). You create a function with the following signature to let the user print a character at a specific coordinate:

1 |
void draw_char_at(int x, int y, char c); |

As long as the user calls this function with coordinates in the x,y-plane, this will work fine, but what if the user doesn’t read our (of course excellent) documentation and instead tries to call it with a row and column? The compiler would even allow for this stupid mix-up:

1 |
draw_char_at('a', x, y); |

The first step of helping the user (and ourselves) is to add an extra level of abstraction!

1 2 3 4 5 |
struct Point { int x; int y; }; void draw_char_at(Point p, char c); |

This extra type makes it difficult for the user to call our function with it’s parameters reordered. The user also get the extra bonus of having our Point type to use locally.

Of course, the user can still use it in stupid ways:

1 |
draw_char_at(Point{some_row, a_column}, 'a'); |

We could of course have made Point into some advanced class with private data members, but it doesn’t really make sense here; Point doesn’t have any invariants to keep track of! We could make it easier to use though. Lets start by adding a couple of reference members so that I can access y as row and x as column:

1 2 3 4 5 6 |
struct Point { int x; int y; int& row{y}; int& column{x}; }; |

We can still initialize a point with values for only x and y and the compiler will initialize the references according to our default values. It is, however, not possible to only initialize a Point by only providing row and column 🙁

What I want to do now is to make the following construct valid:

1 |
Point p{row=7, column=8}; |

Now, this doesn’t really look like C++, but it is possible to implement. Assume that there are variables named row and column available in this scope and that we can assign integers to those variables, then this would work. The question is what type should those variables have? The simplest solution would be to make them int. This would however not work since we already have a constructor that accepts two int parameters – the one that initializes x and y. Let’s instead create new types that fulfills the requirement above.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct Column_Type { Column_Type& operator=(int val) { value = val; return *this; } int value; }; struct Row_Type { Row_Type& operator=(int val) { value = val; } int value; }; |

Now we create our variables and add a constructor to Point:

1 2 3 4 5 |
Column_Type column; Row_Type row; Point::Point(Row_Type row, Column_Type col) : x{col.value}, y{row.value} {} |

We could of course hide the value member of Row_Type by adding a conversion operator:

1 2 3 4 5 6 7 8 9 10 11 |
class Row_Type { public: Row_Type& operator=(int val) { value = val; } operator int() const { return value; } private: int value; }; |

If we make the same change to Column_Type, we can change our Point constructor accordingly:

1 2 |
Point::Point(Row_Type row, Column_Type col) : x{col}, y{row} {} |

**The next problem** I wanted to “fix” was to create a wrapper for integral types that makes sure that we won’t go outside a pre-defined range of the type. Our goal at the beginning was to have a type that supported addition with positive integers and will throw an exception iff the addition makes the value go out of range.

To test our code we use the excellent catch library. When we are done, we want this test case to pass

1 2 3 4 5 |
TEST_CASE("First test case") { Number<int, 0, 10> N {2}; // a number based in int that can't be outside [0,10] CHECK(N + 2 == 4); // should be implicitly convertible to underlying type CHECK_THROWS(N + 10); // going out of range throws } |

Lets start with the basics, a template class that can be constructed from a number of underlying type and can be converted back to the underlying type implicitly.

1 2 3 4 5 6 7 8 9 10 11 12 13 |
template <typename T, T Min, T Max> class Number { public: Number(): value{Min} {} Number(T val): value{val} { // maybe add some checks here... } operator T() const { return value; } private: T value; }; |

Now it’s time to add addition. I usually like to implement it by first implementing compound addition. In this example, we’ll just support addition with a positive integer, for a full implementation we’ll have to take care of addition with negative values as well…

1 2 3 4 5 6 7 8 9 10 11 |
Number& operator+=(int val) { if (Max - val < value) { // don't want integer overflow throw std::domain_error{"Sum outside given range"}; } value += val; return *this; } friend Number operator+(Number n, int inc) { return n += inc; } |

So, great, we now have a type that represent an enumerable type that makes sure the internal value is in the given range. Now lets make it a bit more usable and make it possible to change the addition behavior. Instead of throwing, we might want to clamp the value. To fix this, we’ll move the addition behavior to a policy class. This policy has to be a template class (or possibly have a templated function that does the addition).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template <typename T, T Min, T Max> struct Throws { static T add(T val, int inc) { if (Max - val < value) { // don't want integer overflow throw std::domain_error{"Sum outside given range"}; } value += val; return value; } }; template <typename T, T Min, T Max, template <typename, T, T> class Add_Policy> class Number { // ... Number& operator+=(int val) { value = Add_Policy<T, Min, Max>::Add(value, val); return *this; } }; |

Now we can add a new policy for clamping:

1 2 3 4 5 6 7 8 9 10 |
template <typename T, T Min, T Max> struct Clamp { static T add(T val, int inc) { if (Max - val < value) { return Max; } value += val; return value; } }; |

With this modification, the following test case should pass.

1 2 3 4 5 |
TEST_CASE("Policies") { Number<int, 1, 10, Clamp> N {2}; CHECK(N + 20 == 10); CHECK(N + 2 == 4); } |

To help the user, default values for template arguments could be nice.

1 2 3 4 5 |
template <typename T, T Min = std::numeric_limits<T>::min(), T Max = std::numeric_limits<T>::max(), template <typename, T, T> class Add_Policy = Throws> class Number { ... }; |

We could even add templated aliases.

1 2 3 4 5 |
template <typename T, T Min = std::numeric_limits<T>::min(), T Max = std::numeric_limits<T>::max()> using Limited_Number = Number<T, Min, Max, Clamp>; Limited_Number<int> val; // will never have integer overflow |

Now we have a type that allows the user to have a safe type for integral values. It’s rather simple to modify the behavior by adding a new policy and best of all, it’s still really effective. Check out the full example on godbolt!