Since the introduction of header files in the C programming language
header files have been the main tool for declaring elements that are not
defined but are used in source files. E.g., to use printf
in main
the preprocessor directive #include <stdio.h>
had to be specified.
Header files are still extensively used in C++, but gradually some
drawbacks emerged. One minor drawback is that, in C++, header files
frequently not merely contain function and variable declarations but often
also type definitions (like class interfaces and enum definitions). When
designing header files the software engineer therefore commonly distinguishes
headers which are merely used inside a local (class) context (like the
internal-header approach advocated in the C++ Annotations) and header files
which are used externally. Those latter header files need include guards to
prevent them from being processed repeatedly by sources (indirectly) including
them. Another, important, drawback is that a header file is processed again
for every source file including it. Such a task is not a trivial one. E.g., if
a header file includes iostream
and string
then that forces a compiler
like g++ 14.2.0
to process over 900,000 bytes of code for every source
file including that header.
To speed up compilations precompiled headers were
introduced. Although the binary format of precompiled headers does indeed
allow the compiler to parse the content of header files much faster than
their standard text format, they are also very large. A precompiled header
merely including iostream
and string
exceeds 25 MB: a bit more
than its uncompiled text-file equivalent....
Modules were introduced to avoid those complications. Although modules can still include header files, it's a good design principle to avoid including header files when designing modules. In general: once a module has been designed its use doesn't require processing header files anymore, and consequenly programs that merely use modules are compiled much faster than corresponding programs that use header files.
There is another, conceptual, feature of modules. The initial high-level programming languages (like Fortran and Algol) (but also assembly languages) provided functions (a.k.a. subroutines and procedures) to distinguish conceptually different task levels. Functions implement specific tasks. A program reading data, then processing the data, and finally showing the results can easily be designed using functions, and is much easier to understand than replacing the function calls by their actual implementations:
int main() { readData(); processData(); showResults(); }Often such functions use their own support functions, etc, etc, until trivial decomposition levels are reached where simple flow control and expression statements are used.
This decomposition methodology works very good. It still does. But at the global level a problem does exist: there's little integrity protection. Function parameters may help to maintain the program's data integrity, but it's difficult to ensure the integrity of global data.
In this respect classes do a much better job. Their private
sections
offer means for class-designers to guarantee the integrity of the classes'
data.
Modules allow us to take the next step up the (separation or integrity) ladder. Conceptually modules offer program sections which are completely separated from the rest of the program. Modules define what the outside world can use and reach, whether they are variables, functions, or types (like classes). Modules resemble factories: visitors can go to showrooms and meeting halls, but the locations where the actual products are being designed and constructed are not open to the public.
In this chapter we cover the syntax, design, and implementation of modules as
offered by the C++ programming language. To use modules with the current
edition of the Gnu g++
compiler (version 14.2.0) --std=c++26
(or more
recent) should be specified as well as the module compilation
flag -fmodules-ts
. E.g.,
g++ -c --std=c++26 -fmodules-ts -Wall modsource.cc
Unfortunately, currently it's not all sunshine and roses. One (current?)
consequence of using modules is that the standard that was specified when
compiling those modules is also required when compiling sources using those
modules. If the specified standards differ (e.g., the modules were compiled
with option --std=c++26
, but for a source file using those modules
--std=c++23
was specified) then the compilation fails with an error like
error: language dialect differs 'C++26/coroutines', expected 'C++23/coroutines'A similar error is reported when the modules were compiled with
--std=c++23
and the module using source file is compiled specifying
--std=c++26
. Therefore, once a new standard becomes available, and a
module defining source files is recompiled using the new standard then module
source files using that module must also be recompiled using that standard.
At https://gcc.gnu.org/bugzilla/show_bug.cgi?id=103524 an overview is
published of the (many) currently reported compiler bugs when modules are
compiled by Gnu's g++
compiler. Many of those bugs refer to internal
compiler errors, sometimes very basic code that correctly compiles when
modules are not used but that won't compile when using modules. Sometimes
reported errors are completely incomprehensible. Another complexity is
introduced by the fact that, e.g., a class which is defined inside a module is
no longer declared in an interface providing header file. Instead, it is
defined in a module defining source file. Consequently, those module-interface
defining source files must be compiled before the member functions of such a
class can be compiled. But it's not the module's interface's object file
that's important at that point. When the module-interface defining source file
is compiled the compiler defines a `module-name'.gcm
file in a
sub-directory gcm.cache
(cf. section 25.3). Whenever a source
file that uses the module is compiled the compiler must be able to read that
gcm.cache/module-name.gcm
file. As a software engineer you cannot simply
compile such module using source files, but you must ensure that the
compiler has access to the proper module-name.gcm
files.
Modules may define sub-components (partitions) defining facilities which may be completely inaccessible outside of the modules themselves. Partitions are covered in section 25.6.
export module Name;
Name
is the module's name, its module-compiled
interface unit becomes available in ./gcm.cache/Name.gcm
;
module Name;
module Name;
, where Name
is the module's name.
export import Name;
export import Name;
specifications. Using export
is optional. In module interface units
specifying export
means that the exported module is also available
when sources import the module. E.g.,
// module interface unit: export module Group; export import <iostream>; ... // source file: import Group; int main() { // cout available via Group std::cout << "hello world\n"; }
export module Name:Partition;
Name
is the module's name, Partition
is
a partition of module Name
. Its compiled interface unit becomes
available in ./gcm.cache/Name-Partition.gcm
;
export import :Partition;
export import
:Partition;
. In module interface units using export
is required,
otherwise using export
is optional.
modname.cc
' (where name
is the
(possibly lowercase) module's name), located in a sub-directory having the
(possibly lowercase) module's name. Using plain (internal) header files should
be avoided when defining and/or using modules.
Here's an example of a module's interface. The module's name is Square
and
it declares a function, a class, and a variable:
export module Square; // source: modsquare.cc export { double square(double value); class Square { double d_amount; public: Square(double amount = 0); // initialize void amount(double value); // change d_amount double amount() const; // return d_amount double lastSquared() const; // returns g_squared double square() const; // returns sqr(d_amount) }; } extern double g_squared;This module interface merely serves as an illustration. In practice module interfaces don't contain many different items, but usually just a single class or, alternatively, a series of utility functions. For now, however, the slightly overpopulated module
Square
is used as an initial illustration.
The interface's top-line exports and defines the module name. This must be the
first line of the module's interface unit. Next, the function square
and
class Square
are declared inside an
export
compound. It is possible to export
componentsindividually, but using an `export compound' is convenient. Exported
components can be used outside of the module. Non-exported components (like
g_squared
) are only available to the module's components, and are
therefore like global components, albeit with a restricted (module) scope.
Also note that the variable g_squared
is listed in the interface as
extern double g_squared
: it is therefore a declaration, not a
definition. To define variables in a module omit extern
(as in
double g_squared
), but to avoid overpopulating modules it's advised to
merely put declarations in module interface units.
The modsquare.cc
file can now be compiled, requiring the -fmodules-ts
compiler option:
g++ -c --std=c++26 -fmodules-ts -Wall modsquare.cc
Compiling modsquare.cc
not only produces the file modsquare.o
, but
also a sub-directory gcm.cache
, containing the `module compiled interface
file' Square.gcm
, which is somewhat comparable to a traditional
pre-compiled header file. This .gcm
file must be available when compiling
source files implementing components of module Square
, and it must also be
available to other source files that import (i.e., use) module
Square
. Consequently, the directories containing such files must therefore
also have gcm.cache
sub-directories containing the Square.gcm
file. This requirement is complex, and in practice a so-called modmapper
(cf. section 25.7) is used to handle the complexity. A simple way to
make module compiled interface files available to all sections of a project is
by defining a top-level directory gcm.cache
and using soft-links (like
square/gcm.cache
) to the top-level's gcm.cache
directory. The
top-level gcm.cache
directory and the soft-links in the project's
sub-directories can be prepared before compiling square/modsquare.cc
resulting in the following directory structure:
. +-- gcm.cache | +-- Square.gcm | +-- square +-- gcm.cache -> ../gcm.cache
All components of the module Square
must specify that they're part of
that module. Traditionally this is realized by using (internal) header
files. But projects using modules should no longer need header files. Instead
of the (traditionally used) square.h
and square.ih
files a module
frame file, tailored to the modules' requirements
can be used when defining source files belonging to modules. In this example
the requirement for the remaining source files of the Square
module is
simple: just specify that the source file belongs to the Square
module. Here's a basic frame
file, tailored to the members of the class
Square
:
module Square; Square::() { }
The function square isn't part of the class Square
, so when it's
defined the Square::
scope is omitted:
module Square; double square(double value) { return g_squared = value * value; }
But the members of the class Square
can be defined as usual after copying
the frame
file to the source filew defining those members. Here is
Square's
constructor:
module Square; Square::Square(double amount) : d_amount(amount) {}and the other members are defined analogously:
module Square; void Square::amount(double value) { d_amount = value; }
module Square; double Square::amount() const { return d_amount; }
module Square; double Square::lastSquared() const { return g_squared; }
module Square; double square(double value) { return g_squared = value * value; }
As an aside: the members of this class Square
are all very simple, and
instead of defining them in separate source files they could also be
defined inline
in the module.cc
file itself. Since the class
Square's
interface is exported its members are too, and inline
implementations of its members don't have to be provided in the export
compound.
The module can now be used by, e.g., the program's main
function. Source
files importing a module must import that module, and if multiple source
files are defined in the top-level directory (like main.cc, usage.cc,
etc.), that directory can define its own frame
file. In this initial
example there's only a single main.cc
source file, which merely has to
import module Square
. But since it also uses std::cout
it must import
the
module compiled system header iostream
(cf. section 25.3.1):
import Square; import <iostream>; int main(int argc, char **argv) { std::cout << "the square of " << argc << " is " << square(argc) << '\n'; Square obj{12}; std::cout << "the square of 12 is " << obj.square() << "\n" "the last computed square is " << obj.lastSquared() << '\n'; }
The gcm.cache
directories are only required during compilation time. The
linker doesn't use them and once the source files have been compiled a
program can be constructed as before, by linking the object files, resulting
in a binary program.
This example illustrates several characteristics of modules:
export module
followed by the
module's name. Module names are identifiers, possibly followed by
`. identifier
' sequences (like My.Module
).
:name
or name1:name2
)
are also encountered. Such names refer to module partitions,
covered in section 25.6.
export module
'module-name'
line. Therefore module interface files cannot define
multiple modules;
export
compound or must be
specified using their own initial export
keyword. If no export
is used the component is only accessible to the components of the
module;
import <iostream>;
) are
used, but preprocessor directives like #include <iostream>
aren't.
.gcm
file (the equivalent of a compiled header file) in the
gcm.cache
sub-directory of the module interface unit. If the module's name
is Square
, defined in the modsquare.cc
source file then the compiler
produces gcm.cache/Sqaure.gcm
as well as the modsquare.o
object
file. The .gcm
file must be available to any source file importing the
module or implementing one of the module's components.
Usually projects define several sub-directories, and files in those sub-directories (as well as files in the top-level directory) may import modules defined in other sub-directories. To ensure that module-compiled interface units are available to all of the program's source files the following setup can be adopted:
gcm.cache
sub-directory
gcm.cache -> ../gcm.cache
This setup works fine as long as all the project's modules have different names, but that by itself is a good design principle.
std::string
and std:ostream
. But when using modules including headers
is deprecated and instead their module-compiled equivalents should be imported
using import
statements.
To avoid recompiling system header files for different projects consider
storing module-compiled headers in /usr/include/c++/14
(here, '14' is
g++'s
main version number: update it to your actually installed
version). Using the procedure described in this section project source files
can import module-compiled system headers (assuming that projects have defined
./gcm.cache
directories as described in the previous section).
gcm.cache
directory define the soft-link
usr -> /usr
;
iostream -> iostream.gcm
) then as execute root in
/usr/include/c++/14
execute the command
g++ --std=c++26 -fmodules-ts -x c++-system-header iostream(if the C++ standard isn't
c++26
then adapt it to the
C++ standard that should be used)
iostream.gcm
to the current directory:
mv gcm.cache/usr/include/c++/14/iostream.gcm .
The same procedure can also be used to module-comile header files of installed
libraries. E.g., the bobcat(7) library stores its headers in
/usr/include/bobcat
. To module-compile its arg
header file:
cd /usr/include
g++ --std=c++26 -fmodules-ts -x c++-system-header bobcat/arg
mv gcm.cache/usr/include/bobcat/arg.gcm bobcat/
Note: currently (using g++
version 14.2.0) compilation sequence issues may
be encountered when module-compiling system headers. For some system headers
module-compilation fails if some other module-compiled system headers are
already available. Those issues can usually be `solved' by first moving all
existing .gcm files to, e.g., a ./tmp
sub-directory, followed by the
module-compilation of the intended system header file, and then moving the
tmp/*.gcm
files back to the current directory.
To module-compile a standard header file (e.g., an existing support.h
header file in a support
sub-directory) to a module-compiled header file
issue the command
g++ --std=c++26 -Wall -fmodules-ts -x c++-header support.hThe compiler then writes the module-compiled header file
support.h.gcm
in the ./gcm.cache/,/
sub-directory (note: in gcm.cache's
`comma'
sub-directory).
When a library is constructed there are usually multiple header files, between
which some hierarchy may exist. Consider a library consisting of three
classes: Top
, depending on Middle
, which in turn depends on Base
Each class is defined in its own sub-directory. E.g., base/base.h
:
#ifndef INCLUDED_BASE_ #define INCLUDED_BASE_ #include <fstream> class Base { std::ifstream d_in; public: Base(); }; inline Base::Base() : d_in("demo.in") {} #endif
middle/middle.h
:
#ifndef INCLUDED_MIDDLE_ #define INCLUDED_MIDDLE_ #include <string> #include "../base/base.h" class Middle { std::string d_text; Base d_base; }; #endif
and top/top.h
:
#ifndef INCLUDED_TOP_ #define INCLUDED_TOP_ #include <vector> #include "../middle/middle.h" class Top { std::vector<std::string> d_vect; Middle d_middle; }; #endif
To module-compile the library's headers the gcm.cache
organization as
described in section 25.2 is used (a gcm.cache
directory is
defined at the project's top-level directory, while sub-directories define
gcm.cache
soft-links to the top-level gcm.cache
directory). Next each
header file is module-compiled whereafter the gcm.cache
subdirectory
contains:
,/base.h.gcm ,/middle.h.gcm ,/top.h.gcm usr -> /usr
This, however, is not the organization which is expected by programs that need
to use the module-compiled headers. To compare: the module-compiled system
header files are, together with the system header files themselves, located in
/usr/include/c++/14
(like iostream
and iostream.gcm
). When
module-compiled headers should be used by programs a similar organization is
expected. But that's easily realized by defining a sub-directory (e.g.,
hdrs
) containing soft-links to each of the library's header files and
to each of the library's module-compiled header files. If hdrs
is a
sub-directory of the libraary's top-level directory then it contains
base.h -> ../base/base.h base.h.gcm -> ../gcm.cache/,/base.h.gcm middle.h -> ../middle/middle.h middle.h.gcm -> ../gcm.cache/,/middle.h.gcm top.h -> ../top/top.h top.h.gcm -> ../gcm.cache/,/top.h.gcm
As elaborated in the following two sub-sections there are two ways programs
using the local library can import the library's module-compiled headers: by
using a relative path to the hdrs
sub-directory or by using an absolute
path to the hdrs
subdirectory.
hdr
sub-directory they can be imported by source
files which do not belong to the library.
Suppose the library's header files are imported by main.cc
, defined in the
directory ~/project
:
import "base.h"; import "middle.h"; import "top.h"; int main() { Base base; Middle middle; Top top; }
Furthermore, assume the local library's top-level directory is
~/support/locallib
(and so ~/support/locallib/hdrs
contains the
soft-links to its header files and module-compiled header files).
To make the library's headers available to main.cc
two soft-links to
relative destinations can be defined:
~/project
define a soft-link to the library's hdrs
directory:
ln -s ../support/locallib/hdrs .
~/project/gcm.cache/,/
(note: comma!) sub-directory define
a soft-link to the just defined hdrs
soft-link:
ln -s ../../hdrs .
Once these links are available main.cc
can be compiled using
g++ -c --std=c++26 -fmodules-ts -isystem hdrs main.cc
hdr
is located in a completely different location then the soft-link
~/project/hdrs
must point to that directory using a relative path
specification.
hdrs
directory does not
have to be hdrs
.
Using the ~/project/main.cc
from the previous section define a soft-link
to the absolute location of the library's hdrs
sub-directory in the
project's gcm.cache
sub-directory (note: here no comma is used):
ln -s ~/support/locallib/hdrs gcm.cacheNext compile
main.cc
using
g++ -c -fmodules-ts -isystem ~/support/locallib/hdrs main.cc
gcm.cache
the full path to hdrs
is not required, but at least
first sub-directory name must be be specified (since ~/...
refers to
the user's home directory, which commonly begins at /home/...
the link
could be defined as ln -s /home gcm.cache/
).
ln -s ~/support/locallib/hdrs /tmp/locallib ln -s /tmp gcm.cachethe compilation command is:
g++ -c -fmodules-ts -isystem /tmp/locallib main.cc
But, for the sake of completeness, local header files can be
module-compiled. But note that once a header file is compiled using
namespace
declarations specified in header files are lost when the header
file is either included or imported. Compiled local header files are
stored in the gcm.cache/,/
(note: a comma) sub-directory. They're
automatically used when either their header files are included (e.g.,
#include "header.h"
) or imported (using import "header.h";
).
using namespace
declarations avoiding the explicit namespace specification when referring to
components living in those namespaces.
Here is a simple example of a module exporting a variable which is defined in a namespace:
export module NS; export import <cstddef>; export namespace FBB { extern size_t g_count; }It's a very basic example, illustrating the essence of defining a namespace completely covering the module's export compound. Variations are possible: a namespace could completely surround the export compund, a namespace could be defined inside the export compound, only some of the module's components could belong to a namespace, non-exported components could belong to a namespace, multiple namespaces could be used, etc., etc..
To use the variable g_count
, its module is imported and its namespace is
specified as usual. E.g.,
import NS; import <iostream>; int main() { ++FBB::g_count; std::cout << FBB::g_count << '\n'; }
Alternatively the source file could specify some using namespace
declarations so the repeated namespace specifications can be avoided:
import NS; import <iostream>; using namespace std; using namespace FBB; int main() { ++g_count; cout << g_count << '\n'; }
Some notes:
export module
modules
cannot be defined inside namespaces. Constructions like
namespace Area { export module Nested; ... declarations of components of Nested }won't compile. But as shown, defining a namespace section inside a module is fine.
import
declarations cannot be nested (i.e., they must be at global scope) So
constructions like the following also won't compile:
namespace FBB { import NS; }
Since modules are defined in source files there are no header files anymore when using modules. Modules specify their components in source files, replacing the traditional header files: by defining templates in a module interface unit the template's recipe character is kept, and they are instantiated when needed.
Here's an initial example. A module Adder
exports a function template
add
adding two values and returning their sum:
export module Adder; export { template <typename Type> Type add(Type const &t1, Type const &t2) { return t1 + t2; } }
Source files importing Adder
can now add values of types supporting the
+
operator:
import Adder; import <iostream>; import <string>; using namespace std; int main() { cout << add(1, 2) << '\n' << add(1.1, 2.2) << '\n' << add("hello "s, "world"s) << '\n'; }
producing the following output:
3 3.3 hello world
SortMap
offers the facilities of an unordered_map
, but also
has two sort
members. Both return a vector with pointers to the elements
of the unordered map. One member returns pointers to the elements sorted by
their key values, the other member receives a functor, returning a vector of
pointers to the elements which are sorted according to the functor's
decisions.
The module interface unit defines the SortMap
module. It declares the
class template SortMap
. The module, like any module, can be imported in
other source files, like a source file main.cc
. The main
function
inserts its arguments into SortMap
and then shows the inserted values:
first ordered by the map's keys, then ordered by the map's values:
import SortMap; import <iostream>; import <string>; using namespace std; int main(int argc, char **argv) { SortMap<string, size_t> sortMap; for (; argc--; ) // fill sortMap sortMap.emplace(argv[argc], argc); for (auto const *ptr: sortMap.sort()) // sort by key cout << ptr->first << ' ' << ptr->second << "; "; cout.put('\n'); for (auto const *ptr: sortMap.sort( // sort by value [&](auto const &lhs, auto const &rhs) { return lhs->second < rhs->second; } ) ) cout << ptr->first << ' ' << ptr->second << "; "; cout.put('\n'); }
When calling a.out one two three four five six
it outputs
a.out 0; five 5; four 4; one 1; six 6; three 3; two 2 a.out 0; one 1; two 2; three 3; four 4; five 5; six 6
All members of class templates are templates, and as with header files containing class templates there's only one source file defining the module interface unit: it contains the class template's interface and also the implementations of its members. There is, however, an escape route: it's covered in section 25.6. But in this section the standard way to implement class templates is covered.
When using a single file there's maybe one drawback: the module interface
unit, as it must contain the full implementation of the class template, may
quickly grow to a very large file, which is hard to maintain. To simplify
maintenance it is advised to adopt here the one function, one file design
principle also: the module interface unit itself exports the class interface,
and then uses an #include
preprocessor directive to add the class members'
implementations:
export module SortMap; export import <unordered_map>; export import <vector>; import <algorithm>; export { template <typename Key, typename Value> class SortMap: public std::unordered_map<Key, Value> { using UMap = std::unordered_map<Key, Value>; using ValueType = typename UMap::value_type; using Vect = std::vector<ValueType const *>; private: Vect d_sortVect; public: Vect const &sort(); // sort the keys // 1.f template <typename Functor> // use a functor // 2.f Vect const &sort(Functor const &functor); }; } #include "sortmap.f"
The design of the class SortMap
itself is standard. At the top of the
module interface unit the SortMap
module name is defined followed by
importing some module compiled system header files. Since algorithm
is
only used internally by the member functions it's not exported. But imported
components must be specified at the top of module source files, and so
algorithm
cannot be imported by, e.g., sortmap.f
.
The file sortmap.f
itself merely includes the files implementing the two
sort
members, keeping the module interface unit as clean as possible. Here
is sortmap.f
:
#include "sort1.f" #include "sort2.f"
Each of its included files contain the definition of one member
function. A comment in the class's interface indicates which function is
defined in which file. Here's the first sort
member (sort1.f
):
template <typename Key, typename Value> SortMap<Key, Value>::Vect const &SortMap<Key, Value>::sort() { d_sortVect.clear(); for (auto const &el: *this) d_sortVect.push_back(&el); std::sort(d_sortVect.begin(), d_sortVect.end(), [&](auto const *lhs, auto const *rhs) { return lhs->first < rhs->first; } ); return d_sortVect; }
And here's the second sort
member, accepting a functor (sort2.f
)
template <typename Key, typename Value> template <typename Functor> SortMap<Key, Value>::Vect const &SortMap<Key, Value>::sort( Functor const &functor) { d_sortVect.clear(); for (auto const &el: *this) d_sortVect.push_back(&el); std::sort(d_sortVect.begin(), d_sortVect.end(), functor); return d_sortVect; }
In the introduction section of this chapter it was stated that modules offer new, conceptual features: modules offer program sections which are completely separated from the rest of the program, but modules themselves can define subsections (called partitions) which can define components (classes, types, variables, etc.) which are only accessible to the module and its partitions.
Within the restriction that partitions can be defined so that they're only accessible to their module and their sibling-partitions the access rights of classes defined in partitions are identical to those of classes in general: public members are accessible to their users, protected members are available to classes derived from those classes, while private members cannot be accessed from outside of those classes.
Figure 36 illustrates a simple module (Math
) having two
partitions. It shows the relationships between the module and its partitions
in the same way as commonly used for classes: the most basic (least dependent)
partition is on top, the partition depending on the topmost partition is in
the middle, and the module itself, depending on both partitions, is at the
bottom. The Math
module defines a class Math
, having a member
returning the sum of two size_t
values and a member count
returning
the number of performed additions. It's a very basic design merely
illustrating the way partitions are designed.
The object of class Add
performs an addition, and the object of class
Utility
keeps track of the number of additions that were performed.
Both Add
and Utility
are defined as classes in their own partitions:
Math:Utility
and Math:Add
.
Note that partitions themselves are not classes. When
defining a partition interface unit only a single colon is used instead
of two, as used then defining cass member functions (so Math:Utility
and
not Mat::Utility
).
The Math
interface unit exports class Math
, allowing code which does
not belong to the module Math
itself to define Math
objects after
importing the Math
module:
import Math; void fun() { Math math; ... }
Since the class Math
has data members Utility d_util
and Add d_add
these classes must be known inside the Math
module interface unit
(modmath.cc
), but also by software defining objects of class Math
(comparable to the requirement to, e.g., include <string>
when a class
defines a std::string
data member). Therefore the Math
module
interface unit specifies export import
for the Utility
and Add
partitions. As we'll shortly see these export
specifications do not imply
that software merely using the facilities of the Math
module can also
define, e.g., Math:Utility
objects: the partitions can still remain
`private' to the Math
module. Here is the Math
module interface unit,
defined in math/modmath.cc
:
export module Math; export import :Utility; export import :Add; export { class Math { Utility d_util; Add d_add; public: Math(); size_t count() const; size_t add(size_t lhs, size_t rhs); }; }
The design of the module Math
defining partitions introduces a completely
new level of separation: the components of partitions are like nested classes
but don't result in `class overpopulation': partitions are not defined
inside modules but are completely separately defined from their modules,
while making their facilities only available to their modules and sibling
partitions. And although probably not illustrating good design: by defining
export
compounds in partition interface units the elements in those
compounds become directly available to using software essentially turning the
partition into a module.
Math:Utility
partition is the most basic of the two Math
partitions. It does not depend on features of either the Math
module or
the Math:Add
partition. Since it does use the size_t
type, it
specifies export import <cstddef>
. Here, because of its export
specification, the definitions in cstddef
are also available when
Utility
is imported, and since the Math
module itself exports
:Utility
also to software importing Math
.
As Utility
is a Math
partition its partition interface unit starts by
specifing this:
export module Math:Utility;The colon indicates that this is not a plain module but a partition (note that the first line must start by specifying
export
). It contains
only non-exported components, but as it's a partition of a module, all the
partition's components are fully available to its module and its sibling
partitions. Its class Utility
has two simple members, both very simple,
never changing one-liners. Because of that they're implemented in-line. Here's
the Utility
partition interface unit:
export module Math:Utility; export import <cstddef>; class Utility { size_t d_count; public: Utility(); void inc(); size_t count() const; }; inline void Utility::inc() { ++d_count; } inline size_t Utility::count() const { return d_count; }
Defining members inline is not required, but is an option. For example, its
constructor could very well also have been defined inline, but (for
illustration purposes) is defined in a separate source file. As Utility's
constructor is a partition source file it starts by specifying its module
(Math
), followed by importing the partition component (:Utility
) which
is defined in this source file:
module Math; import :Utility; Utility::Utility() : d_count(0) {}
Math:Utility
partition the Math:Add
partition
does depend on another partition: it depends on Math:Utility
. Like
Utility's modutility.cc
it starts its modadd.cc
file by exporting its
partition name. But then it imports :Utility
, since that partition is used
by Math:Add
, making available all components of that partition.
Since :Utility
already imports <sstddef> :Add
can also use
size_t
. For :Add
the :Utility
partition wouldn't have to specify
export
in front of import <stddef>
since all components of imported
partitions are available to both the module and its partitions. But by using
export import <cstddef>
users of the Math
module automatically receive
<cstddef>'s
definitions (among which size_t
). Here's the Math::Add
partition interface unit (again: notice that the interface doesn't contain an
export compound):
export module Math:Add; import :Utility; class Add { Utility &d_utility; public: Add(Utility &utility); size_t sum(size_t lhs, size_t rhs); };
For this partition no members were defined inline (although that would also
have been possible). Instead all members were defined in separate source
files. Here is class Add's
constructor:
module Math; import :Add; // export import :Utility; OK, but superfluous Add::Add(Utility &utility) : d_utility(utility) {}
and its sum
member:
module Math; import :Add; size_t Add::sum(size_t lhs, size_t rhs) { d_utility.inc(); return lhs + rhs; }
Math
module and its partitions have been
defined (cf. figure 36) the Math
module's members can be
defined. The constructor of class Add
in the Math:Add
partition
needs a reference to a Utility
object which it receives from Math
constructor:
#include "math.i" //import Math; Math::Math() : d_add(d_util) {}
Since class Utility's count
member is public, it can be called by the
Math::count
member:
import Math; size_t Math::count() const { return d_util.count(); }
and finally, Math's add
member calls Add's sum
member, defined in the
Math:Add
partition to obtain the sum of two positive integral values:
import Math; size_t Math::add(size_t lhs, size_t rhs) { return d_add.sum(lhs, rhs); }
Math
module has been developed it can be used by a
program. The exported facilities offered by a module are available after
importing the module. Note that although a module may export import
its
partitions only partition components defined in their export
compound are
available to source files not belonging to the module itself. As
Math:Utility
exports <cstddef>
source files importing Math
automatically also can use cstddef's
features.
Here's a main
function importing Math
and using its facilities to add
two positive integral numbers. Header files are not used anymore, and the
implementation of the main
function is equal to the implementation when
using header files. However, compared to the latter implementation the
compilation of a program using modules requires significantly less time, and
the design of modules using partitions may offer better isolation of
components that are mere building blocks of the those modules.
Here is the implementation of the main
function using the Math
module:
import Math; import <iostream>; using namespace std; int main() { Math math; cout << "Initial number of additions: " << math.count() << "\n" "Enter two pos. values to add: "; size_t lhs; size_t rhs; cin >> lhs >> rhs; cout << "their sum is " << math.add(lhs, rhs) << "\n" "total number of performed additions: " << math.count() << '\n'; }
SortMap
was defined in a
module interface unit. Like class templates in header files class templates in
a module interface files may grow large because the file not only contains the
class template's interface but also the implementations of its
members. Consequently a full recompilation is required when only a single
member function is modified. Recompilations cannot completely be avoided, but
by using module partitions complete recompilation can often be avoided.
Some maintenance complications, however, remain: once a class template's member is modified the modification is not visible in members that use that member. But this dependency issue always plays a role when using templates: once a template has been modified all code using the template needs to be recompiled.
In the current section class templates are implemented using module
partitions, each containing a member of the class template. The previously
developed SortMap
module consists of three components: the class interface
and two sort
members functions. Now when using partitions each
component will be defined in its own partition, and the module itself contains
export import
statements for each partition which must be accessible by
code using the module. Consequently the modsortmap.cc
module interface
unit is remarkably simple:
export module SortMap; export import :Interface; export import :Sort1; export import :Sort2;
Note that even the class interface is defined in a partition: it cannot be
defined in the module interface unit itself, since partitions can depend
on other partitions, but not on the module itself as the partitions must (as
they are imported by the module) be available before the module itself can be
compiled. Partitions, however, can depend on other partitions, so a
partition implementing a sort
function can import a partition providing
the class's interface.
As the :Interface
partition merely specifies what's offered by the
class SortMap
it doesn't depend on the other partitions. So it merely
declares but does not contain the implementations of the sort
members, and
thus it does not itself have to import the alorithm
module-compiled
header:
export module SortMap:Interface; export import <unordered_map>; export import <vector>; export { template <typename Key, typename Value> class SortMap: public std::unordered_map<Key, Value> { using UMap = std::unordered_map<Key, Value>; using ValueType = typename UMap::value_type; using Vect = std::vector<ValueType const *>; private: Vect d_sortVect; public: Vect const &sort(); // sort the keys // 1.f template <typename Functor> // use a functor // 2.f Vect const &sort(Functor const &functor); }; }
On the other hand, the implementations of the two sort
members do
depend on the interface, since they implement their declarations. They
also depend on the facilities provided by the algorithm
module-compiled
header. Both are therefore imported. Here's the implementation of the first
sort
member in the :Sort1
partition:
export module SortMap:Sort1; import :Interface; import <algorithm>; export { template <typename Key, typename Value> SortMap<Key, Value>::Vect const &SortMap<Key, Value>::sort() { d_sortVect.clear(); for (auto const &el: *this) d_sortVect.push_back(&el); std::sort(d_sortVect.begin(), d_sortVect.end(), [&](auto const *lhs, auto const *rhs) { return lhs->first < rhs->first; } ); return d_sortVect; } }
The second sort
member is implemented analogously:
export module SortMap:Sort2; import :Interface; import <algorithm>; export { template <typename Key, typename Value> template <typename Functor> SortMap<Key, Value>::Vect const &SortMap<Key, Value>::sort( Functor const &functor) { d_sortVect.clear(); for (auto const &el: *this) d_sortVect.push_back(&el); std::sort(d_sortVect.begin(), d_sortVect.end(), functor); return d_sortVect; } }
The main
function can remain as provided in section 25.5.1. When
constructing the program the partition dependencies must be taken into
account:
:Interface
partition is compiled;
:Sort1
and :Sort2
partitions are
compiled;
main.cc
is compiled and the compiled object files are
linked resulting in the final program.
With modules traditional headers (.h
files) should be avoided. System
header files (cf. section 25.3.1) still exist, but for those import
statements should be used or made available. For locally developed libraries
module-compiled header files can also be used (cf. section 25.3.2).
It can be difficult to determine which module sections can
immediately be compiled, and which ones depend on already compiled
sections. Moreover, source files belonging to modules but not defining their
interface units may import modules which aren't yet available when
compiling the module's interface unit. For example, module
Basic's
interface unit is independent of other modules:
export module Basic; export { class Basic { public: void hello() const; }; }
But a module Second
imports Basic
and therefore the compilation of
its module interface file depends on the availability of
gcm.cache/Basic.gcm
:
export module Second; export import Basic; export import <iosfwd>; export { class Second { public: static std::string hello(); }; }
Consequently basic/modbasic.cc
must be compiled before compiling
second/modsecond.cc
. However, module Second
may export a class
Second
, and an object of class Second
could be defined by a member
of the class Basic
, exported by the Basic
module:
module Basic; import Second; import <iostream>; import <string>; using namespace std; void Basic::hello() const { cout << Second::hello() << '\n'; }
In such situations the module interface units must first, and in the right order, be compiled. Then, once they are available the remaining source files of the modules can be compiled.
This two-step process (first compile the module interface units in the right order and then compile the remaining source files) quickly becomes a challenge, which is commonly handled by using a module mapper. Module mappers inspect the modules of a project, building their dependency tree, and maybe compiling the module interface units in the right order. The module mapper icmodmap(1), a utility program of the icmake(1) project, is such a module mapper.
By default (but configurable via options) icmodmap(1) expect projects using modules to be organized as follows:
CLASSES
, where
each line (ignoring empty lines and C++ comment) specifies the
name of a sub-directory implementing one of the project's
components. Partitions can be defined in their own sub-directories or
in sub-directories of the module to which they belong. Sub-directories
do not have to define modules, but all of the project's source files
may import modules;
main.cc
defining the main
function);
mod
,
followed by the (optionally lower-case) name of the module or
partition they define. Following this convention a module Support
is defined in the file modsupport.cc
;
Icmodmap(1) inspects each of the source files in the project's top-level
directory and in each sub-directory specified in the CLASSES
file. If a
source file's first line starts with export module
it's a module or
partition interface unit. If so, its module or partition name is stored in a
data-base. Since module and partition interface units may themselves import
modules and/or partitions the current interface depends on those imported
components. Those imported components are declared in its data-base
registering that the current interface unit depends on those imported
components.
Likewise, if another source file imports modules or implements a component of a module or partition then those modules (and/or paritions) are also declared in its data-base.
Once all source files were inspected icmodmap(1) determines the module
dependencies: its data-base must indicate that each imported module or
partition also has a module/partition interface unit, and that there are no
circular dependencies among the module/partition interface units. If these
requirement are satified then icmodmap by default calls the compiler to
compile the interface units in their proper order. By default the object files
are stored in the standard icmake(1) fashion: each compiled interface unit
is stored in the project's tmp/o
sub-directory and their names begin with
a number which is equal to the line number in the CLASSES
file specifying
the inspected sub-directory (like 1modfirst.o, 2modsecond.o,
) using prefix
0 for module(s) defined in the project's top-level directory.
When the inspection and compilation successfully completes then the interface
units' object files are stoed in tmp/o
, and the project's top-level
directory contains a sub-directory gcm.cache
containing the .gcm
files
of all modules and partitions. In addition each inspected sub-directory has
received a soft-link gcm.cache
to its parent's gcm.cache
sub-directory, so each of the project's source files can import each module.
Now that the module and parition interface files are available the remaining source files can be compiled as usual. They can import and use the components of the the defined modules (and where applicable the defined paritions).
When a module interface unit is modified and it's recompiled then the modified definition replaces the old one. Consider this interface unit:
export module Demo; export { class Demo { int d_value = 100; public: int value() const; }; }The member function
Demo::value
(returning d_value
) is implemented
in its own source file, and is called by main
:
import Demo; import <iostream>; using namespace std; int main() { Demo demo; cout << demo.value() << '\n'; }
When the program is run it outputs 100. Now the module interface unit is
modified: d_first
is added to the class as its first data member:
export module Demo; export { class Demo { int d_first = 13; int d_value = 100; public: int value() const; }; }
Next moddemo.cc
and main.cc
, are recompiled and the three object files
are linked constructing the binary program. The new binary outputs 13:
value's
object file returns the first bytes of the Demo
object as an
int
, but since the class Demo
was modified those bytes no longer
contain the value 100.
Such complications are familiar: when using a traditional header declaring a class and the class changes the organization of its data members then in practice all source files using the class must be recompiled (and recursively: if another class is derived from, or contains a data member of the modified class then all source files using the other class must also be recompiled). When using modules such recompilations are also required. The icmodmap(1) support program has an option to recompile module (partition) interface units depending on modified interface units and either to modify the last write times of source files using those modules or to write their names to a file so a build utility can recompile those module using source files.
What if a library's design merely uses modules? In that case the library must
make available its module-compiled interface files (i.e., .gcm
files) to
its users. Copying the library's .gcm
files to the gcm.cache
sub-directory of a using project is not necessary (and probably a bad design
anyway, since modifications of the library's .gcm
files will not be
noted), but soft-links should be used. However, often modules import other
modules or partitions, and consequently such library-modules in turn may
depend on other modules (or partitions). When a project must import a
library's module not only the module's .gcm
file must be available but
also (recursively) the module/partition .gcm
files that module depends
on.
As an illustration consider the situation where a project defines three
modules: Module1, Module2,
and Module3. Module1
imports External
,
which is a module offered by some library; Module2
imports
Module1
, and Module3
imports Module2
(cf. figure 37).
The External
module doesn't belong to the current project, and so the
External.gcm
file lives elsewhere. When constructing the project its
gcm.cache
directory must make External.gcm
available. This is realized
by defining a soft-link to the actual location of External.gcm
. But in
addition soft-links to the modules/partitions imported by External.gcm
must be made available in the project's gcm.cache
sub-directory.
The icmodmap(1) support program can be used to determine and to satisfy
the requirements of externally defined modules.
To determine the modules' dependencies icmodmap
is called in the
project's top-level directory specifying its --dependencies
(or -d
)
option. It shows the dependencies amount the modules and reports an error as
the module External
isn't found:
[Error 1] 1 UNKNOWN module Dependencies: LOCAL module Module1 imports UNKNOWN module External UNKNOWN module External LOCAL module Module2 imports LOCAL module Module1 LOCAL module Module3 imports LOCAL module Module2And indeed, there's an error: the
External
module wasn't developed in
the context of the current project. It's defined in an external library,
offering its module/partition compiled interface units to its users. By
informing icmodmap
where the external .gcm
files are located the error
is solved. The relative or absolute path to the directory containing the
library's .gcm
files is either specified as a command-line option or it's
specified in a file. Either of these can be passed to icmodmap
using its
--extern
(or -e
) option. E.g., if the library's .gcm
files are in
/tmp/library/gcm.cache
then by calling
icmodmap -d -e /tmp/library/gcm.cachethe error disappears and
icmodmap
reports:
Dependencies: LOCAL module Module1 imports EXTERN module External EXTERN module External LOCAL module Module2 imports LOCAL module Module1 LOCAL module Module3 imports LOCAL module Module2specifying
--extern
also defines the soft-links to the external
modules: the project's gcm.cache
sub-directory now contains the soft-link
External.gcm -> /tmp/library/gcm.cache/External.gcmNow that the requirements of the project's module interface files are all satisfied they can be compiled (they are compiled by
icmodmap
if the
-d
option isn't specified), followed by the compilation of the project's
remaining source files. Finally, all object files can be linked to the used
object files of the external library in the usual way (specifying, e.g., the
linker's -L
and -l
options).
mod
, followed by the lower-case module- or
partition-names, using .cc
extensions (e.g. modgroup.cc
);
./group
)
Group
is defined in ./group
,
than its partiton Group:Support
is defined in ./group/support
;
gcm.cache
,
initially containing a soft-link usr/
to the system's /usr
sub-directory allowing source files to import module-compiled
system header files (e.g., import <iostream>;
);
gcm.cache
to ../gcm.cache
, so all compiled module- and partition-interface
units are available in the (top-level) gcm.cache
sub-directory.
The compiler writes compiled interface units in their gcm.cache
sub-directories. The .gcm
filenames of modules are equal to the names of the
modules (e.g., for Group
it is Group.gcm
). The filename of
a module-compiled partition starts with its module name, followed by a dash
(-
), followed by the partitions's name (e.g., for Group:Support
it is
Group-Support.gcm
).
To avoid overfilling interface units they should (like class headers) not define components but merely declare them. Their definitions are provided in separate source files, defined in their interface unit's sub-directory, using the familiar the one component, one source file design principle.
Math:Add
partition (cf. section 25.6)
all start with
module Math; import :Add;Since the
Math:Add
partition interface unit declares a class Add
most or all of its source files implement a member of class Add
. In these
cases it's convenient to have a predefined frame
file containing the
essential elements of its source files. E.g., for Math::Add
sources this
could be
module Math; import :Add; using namespace std; void Add::() { }When implementing a member copy the
frame
to the intended source file,
specify the member's name, define its body and maybe change its return type.