Hypothetical C++: easy type creation

In this essay, we will explore an imaginary extension of the C++ language that could simplify programming, help detect more errors automatically and make programs clearer.

When I look at typical C++ code, I see a lot of uses of simple built-in types: int, float, double, std::wstring, const char *-- even though in each instance they may be used for wildly different purposes. One may be a width or a height, another a temperature, a third, a range limit.

The reason programmers go for these simple types is easy to explain: they’re built-in, they have a lot of support functions, and their behavior is well known. But there is another reason behind their ubiquity: programming languages make it a chore to create a new type with enough functionality.

What if this weren’t the case? What I’d like to see in C++ is an easy way to create a new type from an existing one.

The current state of affairs

C++ has the typedef keyword, but that merely creates a synonym for an existing type, and the two are interchangeable. You cannot create Celsius and Fahrenheit types from double safely with a typedef because nothing will prevent adding Celsius to Fahrenheit.

On the surface, this property of typedef seems useful. If all we need is a short-hand version for a long type declaration or a semantic name for a type, typedef fits the bill. Or does it? Using a pure short-hand version saves some typing but does not introduce meaning. On the other hand, giving the typedef a semantic name, for example ListOfPeople, helps understand the code, but does not prevent assigning a list of file-names to it by mistake.

The current way to avoid such typing errors is to wrap the type in a class. The downside is that you have to painstakingly forward every member function, or create a lot of operators. The fact that this solution exists and yet is rarely used should tell us something. Code is still littered with ints and doubles and typedef, because they’re easy to create.

Easy Type Creation

We need something as simple as a typedef which will actually create a new type. Let’s call it typedecl. Ideally, it would be so simple to use that programmers will reach for it by default. The barrier to entry should be as low as possible. Here’s what we need typedecl to do:

  1. Create a new type.
  2. Allow easy-declaration of literals.
  3. Allow basic internal functionality automatically.
  4. Allow easy additional external functionality.

1. Create a new type

Creating a new type is easy. Just make it the definition of typedecl in the language. With a new type, we avoid accidental assignments and allow function overloading. Taking our Celsius and Fahrenheit example, here are two function declarations that could not be written side-by-side if they were a typedef:

Celsius convert(Fahrenheit);
Fahrenheit convert(Celsius);

While anyone could come up with a naming scheme to allow this to work with typedef, the fact that you need to come up with such a scheme and, more importantly, that you need to worry about such an issue in the first place, points to the problem of not having a unique type for each.

2. Declaration of literals

Easy declaration of literals is important for usability. Without usability, the feature would not be used. Somewhat like how a numeric literal will automatically be silently typed as a int, long or double if it fits the limits of the type, the same behaviour should be supported by typedecl.

3. Allow internal functionality

The need for internal functionality is again to fulfill our need for usability. For example, with numerical types (int, double, ...) we don’t want to have to declare all possible operations between two variables. If it’s tedious, the typedecl won’t be used, just like wrapping an integer in a class is not often used. The same should be true for more complex types that are used as the basis for a typdecl. A typedecl based on a std::string should bring its member functions, with all instances of std::string parameters replaced with the new type.

4. Allow external functionality

The hardest part is the last: allowing useful external functionality. Once again, how easy it is for the programmer will have a direct influence on how often it is used. It should be easy to clone an existing function for the new type. Ideally, it should be easy to clone a whole group of functions. The syntax I suggest is to reuse the same keyword, typedecl, with a clone modifier. This would allow the cloning of one or more functions. For example:

typedecl Celsius clone std::abs;
typedecl Celsius clone { std::abs; std::pow, ... }

Ideally, it should be easy to clone an entire namespace, too:

typedecl Celsius clone namespace std;

Unfortunately, this is too broad and far-reaching in many cases. Ideally, we would need to add the equivalent of a namespace to C++, without creating an additional identifier while programming, but merely creating a semantic grouping that could be accessed. For example, all trigonometric functions could be grouped under one semantic, all I/O functions under another. Here’s what this hypothetical language feature could look like:

namespace std
{
   namespace semantic trig
   {
      double cos(double);
      double sin(double);
   }

   namespace semantic io
   {
      ostream& operator << (ostream&, double);
      // ...
   }
}

With this feature, the cos() function could still be accessed directly within the std namespace. The trig semantic namespace would be allowed, but optional. Cloning all trigonometric functions would then merely be:

typedecl Celsius clone namespace std::trig;

In some cases, it may be useful to change only some parameters of a function. In that case we could borrow the syntax of a deduction guide to give the compiler a map of how the automatic conversion should be done. For example:

typedecl Celsius clone double std::pow(double, double) -> Celsius std::pow(Celsius, double);

The payoff

Now, I will show a few examples of code improvements that can be achieved with typedecl. First, it can lead to fewer coding errors where arguments to a function are misplaced:

// Our world...
void foo()
{
   int width = get_width();
   int height = get_height();
   bool stroked = should_stroke();
   bool filled = should_fill();
   // Is this call correct?
   draw_rect(width, height, stroked, filled);
}

// Hypothetical C++ world...
void foo()
{
   Width width = get_width();
   Height height = get_height();
   Stroked stroked = should_stroke();
   Filled filled = should_fill();
   // The order of the arguments is necessarily correct.
   draw_rect(width, height, stroked, filled);
}

Second, it allows overload or templates to be specialized based on the semantic of a type instead of purely on its mechanical type. This is much better compared to typedef. With typedef, you need to know what the underlying type is to know if an overload or template instantiation really is different. If you used typedef from a third party, you would have to wrap it in a class, with all the interfacing annoyance. As an example, take the std::variant type. It allows you to access its elements by its type, but if two elements have the same type, then there is ambiguity. With typedecl, having different types makes this problem disappear.

Conclusion

With these changes to C++, we could finally get rid of a lot of incidental usage of purely mechanical types. There would no longer be any reason to use bare int, double, std::string, std::map, etc. in code. We could program with meaningful types that would provide even more type safety than we currently achieve because creating them would be simple.

This entry was posted in Desktop application development
by Pierre Baillargeon.
Share this article