Logo

Blog


C++20 Modules: Reachability and Visibility

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 (C++20 Modules: The possible speedup) is the improved compilation time. We also looked at encapsulation and controlling your interface in Controlling your interfaces.

In today's post, I like to talk about visibility and reachability.

Reachable but not visible

C++14 introduced a way to have an entity that is reachable but not visible, making it impossible to transform such code in C++ Insights correctly. I'm talking about auto as a return type. The difference to C++11, where we already could declare auto as a return type, is that in C++14, the trailing part is no longer required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auto Fun()
{
  struct S {
    int x;
  };

  return S{3};
}

int main()
{
  auto ret = Fun();
  ret.x    = 5;
}

If you look at this code with C++ Insights, you can see that Fun returns S. We can also change the value of the data member x outside of Fun. However, we cannot create an object of type X outside of Fun (at least not without decltype).

This is an example where we look at a case of an entity that is reachable but not visible. So far, we didn't need modules for this. I guess the case presented is not the most useful one.

Modules, reachability, and visibility

If we look back at last month's post, there we discussed the example of Normalize.

 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);
  }
}

In this version, I did use import <string> in the Purview of the module. By that, it follows the rules of named modules. Everything is private by default, except if we say export. The implication is that, for example, in main, we can import the module strcat, call Normalize and call .size() on that result.

1
2
3
4
5
6
7
8
import strcat;

int main()
{
  // std::string s{}; A Doesn't compile
  auto x = Normalize(true);
  return x.size();  B size is reachable
}

We can see that A doesn't compile. The type std::string is not visible within main. It is only visible inside the module strcat. However, we can call .size() on the return type of Normalize because this function is reachable. We are looking at the same case as in the C++14 example.

Whether this is good or bad is a very delicate question. It seems totally weird that we have a type that we cannot really use. On the other hand, not polluting everybody's translation unit comes with a certain interesting aspect.

Anyhow, we can change the behavior by re-exporting the imported module prefixing it with export:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export module strcat;

import<type_traits>;
export import<string>;  A export the imported module string

std::string ConvertToBoolString(bool b)
{
  return b ? std::string{"true"} : std::string{"false"};
}

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);

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

  } else {
    return (arg);
  }
}

This means that we are back at the topic of last month's post. We have much finer control over our interfaces. It also means that we have to consider the fact above, and whenever we import something, we need to ask the question of whether it makes sense to export it as well, what a great new world modules bring us!

Andreas