C++20's modules are one feature of the big four, supposed to influence how we write C++ code in a huge way. One expectation we discussed in the last post (C++20 Modules: The possible speedup) is the improved compilation time. In today's post, I like to shed some light on another part of modules that I think weights much more, encapsulation and controlling your interfaces.
I have a secret I like to hide
For today's post, I like to refer to the implementation of
Normalize in C++ Insights. This function template is used to convert various Clang types into a
std::string. Below you see a version reduced to the parts important for this post.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
There are more conversion functions available, and in
Normalize, you can see that the function does different things when the type
bool or something else. Let's focus on the
bool case for today.
To convert a
bool into a
std::string, I have a function called
ConvertToBoolString. I hope the name of the function leaves no room for surprises.
If you look at the function definition of
ConvertToBoolString, or better, where it is located, you can spot that I put it into a namespace called
details. Other people have different names. Sometimes I call it
helper. I once saw
hands_off. They all aim to carry the same intent saying this is part of internal implementation, please don't use it somewhere. Yes, sometimes I have something to hide.
However, this approach is very weak. First and for all, nobody knows what
details means. Within the entire project? Only within this header file? Do you like to tell me this in a detail of the implementation just that I know, but I can use it in every way I want?
There are more interpretations for us humans. For the compiler is simply means that we need to say
details:: to reach
ConvertToBoolString. That's it. I will get processed each time this header file is included, and it will also be compiled if required. And of course, everyone who is able to type
details:: before the function name is allowed to use
ConvertToBoolString as far as the compiler is concerned.
Looking at it, we can say that we failed badly without having better options. We don't get the compiler to check and obey the meaning of whatever name we chose for that namespace.
But that is the past. Let's see how modules make this look like.
Yes. now I can hide my secrets from you...
With C++20's modules we can shape our API design in a much better, robust way. Have a look at the C++20 version below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
First, I start declaring that this file is a module named
strcat in A. Next, as in a header file, I import the required headers,
<string>, for this module.
Then in B, we see
ConvertToBoolString again. The implementation is unchanged, except that the function is no longer in a dedicated namespace. Why? Simply because it is no longer necessary. As I stated above, the name of this namespace wasn't helpful. Its intention was to mark the elements in this namespace private. With modules and without saying
export for a symbol, we get this meaning which the compiler understands and obeys for free. Not having to come up with a name for a namespace here leaves us this energy for the really important names. In terms of clean code, I think this approach is also better. We manage to reduce this code this it's essence.
Moving on to C. we see
Normalize, which like
ConvertToBoolString, is unchanged, except that it starts with
export. By that, we tell the compiler and our fellow developers, that
Normalize is a function that this module exports for use in importing modules or files. Everything that is not marked export is unusable outside our module.
What you gain
With the new ability to mark functions as private (or not-exported) we have a better way to guarantee a stable ABI for our customers if we are a library vendor. Why? Because we can now explicitly name the symbols which should be exported and can hide the others. That way we no longer expose internals which we may want to change later. For example, suppose one day I'm able to make
constexpr. That's nothing my library customers should see. The other way around, maybe I decide to remove
inline. In the old world, that would risk an ABI break because the compiler has the right to inline this function but isn't required to. Adding or removing
inline can those lead to an ABI break. That is still valid to evaluate the case for exported functions but not longer for private ones.
The ability to express the difference between private and public symbols is what I think the way more valuable part of modules. For the first time, we have a mechanism to state which symbols we like to be exported - leaving us with the choice to have module-private symbols to shape our code internally and no longer sacrifice because it would allow others to call internal parts. In some sense, modules give us a similar control we have with classes for ages.
Is there more?
It is C++! What do you expect?
The answer is: Yes! Next time I plan to talk about visibility and reachability. Two things which become more interesting with modules.