Logo

Blog


C++20 Modules: Controlling your interfaces

This post is a short version of Chapter 4 Modules from my latest book Programming with C++20. The book contains a more detailed explanation and more information about this topic.

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
namespace details {
  /// \brief Convert a boolean value to a string representation of "true"
  /// or "false"
  static inline std::string ConvertToBoolString(bool b)
  {
    return b ? std::string{"true"} : std::string{"false"};
  }

}  // namespace details

template<class T>
static inline decltype(auto) Normalize(const T& arg)
{
  // Handle bool's first, we like their string representation.
  if constexpr(std::is_same_v<std::remove_cvref_t<T>, bool>) {
    return details::ConvertToBoolString(arg);

  } else if constexpr(std::is_integral_v<T>) {
    return std::to_string(arg);

  } else {
    return (arg);
  }
}

There are more conversion functions available, and in Normalize, you can see that the function does different things when the type T is 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
export module strcat;  A Declare and export a module strcat

import<type_traits>;
import<string>;

B The namespace is gone
std::string ConvertToBoolString(bool b)
{
  return b ? std::string{"true"} : std::string{"false"};
}

C Note that I say export here
export template<class T>
inline decltype(auto) Normalize(const T& arg)
{
  // Handle bool's first, we like their string representation.
  if constexpr(std::is_same_v<std::remove_cvref_t<T>, bool>) {
    return ConvertToBoolString(arg);  C Again, namespace is gone

  } else if constexpr(std::is_integral_v<T>) {
    return std::to_string(arg);

  } else {
    return (arg);
  }
}

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, <type_traits> and <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 ConvertToBoolString 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.

Andreas