foonathan::blog()

Thoughts from a C++ library developer.

What should be part of the C++ standard library?

At Meeting C++ 2017 — which was great, BTW — I attended a talk by Guy Davidson about the C++ graphics 2D proposal, wording here.

Now, there is some controversy about the proposal — especially by those who do serious graphics stuff. Does the C++ standard library need 2D graphics? Shouldn’t the committee focus on real issues instead of some toy library that is never going to be used for serious applications?

But I’m not here to rant about the stupid standard committee and the completely bloated and unusable standard library, like some do. Instead, this discussion got me thinking: What should be part of a language’s standard library?

BTW: I have Patreon now. If you like my blog posts, please consider supporting me. I only charge per “productive week”, so you don’t have to pay if I am not working on anything, and you can set monthly caps. One dollar a month would really help!

Imagine a perfect world

If you ask a C++ programmer to imagine a perfect world, chances are he or she thinks of a world where installing C++ dependencies is completely trivial. A world where it is absolutely no problem to get high-quality, well documented external libraries.

Am I weird that I dream of that?

In such a world, do we need a standard library at all? Couldn’t we just use this great external library code for everything?

The answer is no, and there are a couple of reasons.

1. Compiler-magic

Some standard library facilities cannot be implemented by a normal programmer. Think of std::initializer_list. It is a magic type somehow summoned by compiler magic when you write a braced initializer. You can’t implement it yourself.

Well, technically you can. However, you can’t add the compiler magic if it doesn’t exist already.

Another example would be the placement new operator that invokes a constructor. You can’t invoke a constructor yourself.

Now, you might not like such magic. And I don’t like it either.

But there are also things that are possible to implement, just difficult - especially if you want to have it portable. Think of std::abort(). Or std::memcpy(). The latter you could do yourself but it is really hard to match the speed of the standard library function. Thus it makes sense to ask the compiler to do those.

Or just imagine having to write std::atomic!

And finally, sometimes the compiler can just do a much better job than you ever could.

Take std::make_index_sequence, for example. It takes an upper bound and generates a std::index_sequence given the numbers from 0 to the upper bound exclusive. So std::make_index_sequence<5> yields std::index_sequence<0, 1, 2, 3, 4>. This is useful for some meta programming applications especially pre fold expressions.

The naive implementation of it looks something like this:

template <std::size_t N, std::size_t ... SequenceSoFar>
struct make_sequence_impl
{
  using type = typename make_sequence_impl<N - 1, N - 1, SequenceSoFar...>::type;
};

template <std::size_t ... SequenceSoFar>
struct make_sequence_impl<0, SequenceSoFar...>
{
  using type = index_sequence<SequenceSoFar...>;
};

template <std::size_t N>
using make_index_sequence = typename make_sequence_impl<N>::type;

As long as we haven’t reached 0, we’ll prepend one integer to the sequence.

This naive implementation has O(n) instantiations of make_sequence_impl. A less naive implementation can bring it down to O(log n) with some great tricks. However, if you simply ask the compiler “hey, generate me a sequence of N integers”, it can do it without any template instantiations. When doing extensive template meta programming this speed is important.

Some other TMP things are done that way, some <type_traits> for example. An SFINAE or specialization based implementation is too slow, so just ask the compiler whether or not some type is the base class of another.

2. Vocabulary types and concepts

But there are more reasons to put things into a standard library.

Let’s think more about the perfect world scenario I’ve dreamed about earlier. When it is trivial to get external libraries, everybody will do so. And this can become an issue when trying to use multiple libraries.

For example, you might want to read a string from and pad it with a certain characters on the left-hand side until the string has a certain length. Naturally you use two external libraries, one for reading and one for left-padding, because both are incredibly difficult tasks.

In case you might not get that reference.

Both libraries need to operator on strings. In particular both need dynamically sized strings, so simple const char* are not sufficient. So of course both libraries use an external library that provides strings. However, they don’t use the same one!

The I/O input library was very concerned about different encodings and things like that, so they opted for using foo::UnicodeAwareString, an expertly designed master piece.

The left-pad library was to preoccupied with the difficult task of left-padding a string to worry about such things as “Unicode”, so they used bar::hopefully_ascii_string, which is great for ASCII but not so great for anything else.

Now you can’t use the two libraries together without converting between their string types! And given that left-pad doesn’t support Unicode, this is impossible. This isn’t an issue for the left-pad example but just imagine if every library used a different string type.

There are a couple of types that are “vocabulary types”. They are used in a lot of interfaces and it is necessary that they are the same in every library, otherwise combining two is painful. Strings are examples of vocabulary types, but also optionals, simple dynamic arrays and smart pointers.

If a standard library doesn’t provide one, everybody else will do their own, slightly different version. Just look at all the optionals!

But there is more to that. There are also “vocabulary concepts”.

The C++ standard library is designed to be extensible. The algorithms are generic, the containers have a common design and the allocators are the same. This allows writing your own containers or algorithm that provide/require the same interface and everybody can use them. The underlying concepts a standard library defines are as important as the actual concrete implementations, if not more important.

The real world

But alas we don’t live in a perfect world and external dependencies are far from trivial. In such a world, there is one more reason to put things into a standard library.

3. It’s just useful

Every non-trivial program needs to do some form of I/O.

A lot of programs need to sort something or search for something.

A lot of programs need to non-trivial math.

A lot of programs manipulate strings.

Those things are ubiquitous: just put them in the standard library. If they are used by most programs, they should be included.

Downsides of putting things in the standard library

Of course, no decision is one-sided: There are downsides to standard libraries. Once they’re in, they’re almost impossible to get out, change or fix.

The standard streams are a beautiful way to do I/O — if it is 1998. Now they are bloated and overly complicated. The language has evolved: Variadic templates are a nicer formatting solution than bit shifts. But also the industry: We’ve learned that OOP isn’t the solution to everything and moved away from it, UTF-8 is a thing now.

I’m saying “we” just as a stylistic choice — I was just born back then.

But the streams are still there, not a lot has changed. std::string is still there, std::vector<bool> is still there.

Furthermore, it is simply impossible to please everyone. There are a lot of trade-offs in every design.

Just something as simple as std::vector<T> doesn’t please everyone. What if I want to have a small vector optimization? What if I want to resize it without initializing the elements? What if I want to give it a fixed upper size? …

Yet it is important that std::vector is part of the standard library: It defines how a dynamic array of T looks like, what interface it provides. If you want to write a replacement, you know how it should look and can write it in a way that generic code can handle it as well.

Conclusion

So does C++ need 2D graphics in the standard library?

It certainly doesn’t involve compiler magic and isn’t useful for most applications. Furthermore, it will never be good enough for real serious use-cases.

However, it does provide certain vocabulary types: a 2D point, for example. A single point class would greatly benefit a lot of libraries instead of having everyone roll up their own, otherwise identical point. But it is probably too late to add a point vocabulary type at this point in time.

Still, I think there is some merit to having more vocabulary types in the standard library. It is just a question of adaption.

This post was made possible by my Patreon supporters. If you'd like to support me as well, please head over to my Patreon and do so! One dollar per month can make all the difference.