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 45 MB: a bit more
than their preprocessed 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
. 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.
module.cc
', 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; 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) }; } double g_squared;This module interface merely serves as an illustration. In practice module interfaces don't contain so 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 shown.
The interface's top-line exports and defines the module name. This must be a
line by itself. Next, the function square
and class Square
are
declared inside an export
compound. Components can
also individually exported, but using an `export component' is
convenient. Exported components can be used outside of the module. Components
that are not exported (like g_squared
) are only available to the
components of the module itself, 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
double g_squared
: it is therefore a definition, not a declaration. To
merely declare variables use extern
(as in extern double
g_squared
), but in that case another module-specific source file defining
g_squared
is required.
This module.cc
file can now be compiled:
g++ -c --std=c++26 -fmodules-ts -Wall module.cc
Compilation of the module.cc
file not only produces the file
module.o
, but also results in 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 that implement components of the
Square
module, and it must also be available to other source files that
import the Square
module: 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.6) is used to handle this
complexity. In practice the requirement can often boil down to defining a
top-level directory gcm.cache
and defining a soft-link
square/gcm.cache
to the top-level's gcm.cache
directory before
compiling square/module.cc
resulting in the following directory
structure:
. +-- gcm.cache | +-- Square.gcm | +-- square +-- gcm.cache -> ../gcm.cacheUsing this structure the
Square.gcm
module compiled interface file is
available to all of the program's source files.
All components of the module Square
must specify that they're part of
that module. Traditionally (internal) header files were used. But including
header files should no longer be required and should therefore be avoided when
defining and using modules. Instead of the (traditionally used) square.h
and square.ih
files it is advised to use a frame
file, specifying the requirements for compiling source files. 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 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.
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 at the top-level directory, 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 the Square
module. It also
imports <iostream>
(cf. section 25.3) so the program can
interact with its user:
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 still be constructed as we're used to, 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. Names are identifiers, possibly preceded by series of
`identifier .
' sequences or an `identifier :
' sequence. The
former syntax defines a module's name (like this.is.my.module
),
the latter is used for partitions, covered in section
25.5.
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;
using namespace
declaration 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 { size_t g_count; }It's a very simple example, but it shows the essence of defining a namespace inside a module definition. In practice a class would probably be specified inside the namespace.
To use the variable g_count
, its module must be imported and its
namespace must be specified. E.g.,
import NS; import <iostream>; int main() { ++FBB::g_count; std::cout << FBB::g_count << '\n'; } // alternatively: // // 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; }
std::string
and std:ostream
. But when using modules including headers
should be avoided, and instead their module equivalents should be imported
using import
statements.
And that's maybe understandable considering that merely reading the
iostream
and string
system headers forces the compiler to process some
900,000 bytes. Traditionally using precompiling headers was an attractive
alternative improving compilation speed, but precompiled headers are
often really big: precompiling iostream
results in iostream.gch
, a
file exceeding 45 MB. When using the importable alternatives there's at least
a large reduction in file sizes compared to using precompiled headers. The
importable iostream
file is some 9 MB large, the importable string
file some 5 MB.
To avoid recompiling system header files for different projects consider
storing the compiled headers in /usr/include/c++/14
(here, '14' is
g++'s
main version number: update it to your actually installed
version). To make system headers available to
modules define the soft-link usr -> /usr
in the gcm.cache
sub-directory of source files importing system header files.
To compile system headers so that they're stored in /usr/include/c++/14
define a /usr/include/c++/14/gcm.cache
sub-directory, containing the same
soft-link: usr -> /usr
. Then, to compile (e.g., iostream
) execute
g++ --std=c++26 -fmodules-ts -x c++-system-header iostreamproducing the file
/usr/include/c++/14/gcm.cache/iostream.gcm
, and
then move gcm.cache/iostream.gcm
to the current directory. Source files
can then do `import <iostream>;
' to use the iostream
facilities.
To summarize:
/usr/include/c++/14/gcm.cache
;
usr -> /usr
;
/usr/include/c++/14/
compile the required system header(s)
using the abovementioned g++
call;
gcm.cache
directory move
it to its parent directory;
usr
-> /usr
in their gcm.cache
sub-directories.
Note: currently (g++
version 14.2.0) you may encounter compilation
sequence issues when module-compiling system headers. For some system headers
compiling them 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 compilation of
the system header file, and then moving the tmp/*.gcm
files back to the
current directory.
In the introductory paragraphs of this chapter it was already mentioned that, when using modules, using a frame file is preferred over using (internal) local header files: source files importing modules or defining module components are all plain source files, and therefore, since the frame file imports all required module components it's easy to define another component or another source file using modules. It's of course possible that at some point, during the implementation of such sources additional imports are required. In those cases simply add the additional imports to the frame file. It doesn't invalidate the already defined source files and simplifies defining new source files.
But, for the sake of completeness, local header files can also be
(module-)compiled. However, 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";
).
But modules are defined in source files so there are no header files anymore when modules are defined. Modules specify their components in source files, replacing the traditional header files. Therefore, by defining templates in the module's interface file the template's recipe character is kept, and they are instantiated when needed.
As a very basic illustration consider the following module interface source file exporting a template:
export module Template; export { template <typename Type> Type add(Type const &t1, Type const &t2) { return t1 + t2; } }
When the Template
module is imported in a source file the add
template
can be instantiated for several types:
import Template; 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
ostream
manipulator insertion operators. In addition to those manipulators all kinds
of types can be inserted into ostreams
. Since all possible insertable
types were unkown when std::ostream
was implemented, insertion of possible
types is handled by a template.
In this section that same approach is adopted to define a module Error
exporting a class Error
offering insertion operators. An error message
is considered complete once a std::endl
is inserted into the Error
object, and the class also offers a member count
returning the number of
error messages that have been inserted into the object. The inserted messages
are also written to std::cerr
.
The module.cc
file of the Error
module is
1: export module Error; 2: 3: export import <iostream>; 4: 5: export 6: { 7: class Error 8: { 9: template <typename Type> 10: friend Error &operator<<(Error &error, Type const &type); 11: 12: friend Error &operator<<(Error &error, // 2 13: std::ios_base &(*func)(std::ios_base &)); 14: 15: friend Error &operator<<(Error &error, // 3 16: std::ostream &(*func)(std::ostream &)); 17: 18: size_t d_count; 19: 20: public: 21: Error(); 22: size_t count() const; 23: }; 24: } 25: 26: template <typename Type> 27: Error &operator<<(Error &error, Type const &type) 28: { 29: std::cerr << type; 30: return error; 31: } 32: 33: inline Error::Error() 34: : 35: d_count(0) 36: {} 37: 38: inline size_t Error::count() const 39: { 40: return d_count; 41: }
std::ios_base
(line 13) and std::iostream
(line 16)
are used the iostream
system header is imported.
class Error
declares a function template inserting anything
that's insertable as its friend (lines 9, 10), as well as the standard
manipulators mentioned in section 11.11.1 (lines 12 through
16). Furthermore, the class has a size_t d_count
data member (line 18),
declares a constructor (line 21), and a count
accessor member
function.
type
into std::cerr
. In case std::err
doesn't accept
an instantiated Type
then instantiation (and thus compilation) fails.
d_count
data member. It's a simple one-line function, implemented
inline.
count
accessor (lines 38 through 41) is a simple one-line
function, also implemented inline, merely returning the object's
d_count
value.
The second and third insertion operators are implemented as free functions;
both returning their left-hand side (Error
) operands. Both implementations
are no different from the implementations that traditionally would have been
used. Here's the second insertion operator:
module Error; using namespace std; Error &operator<<(Error &error, ios_base &(*func)(ios_base &)) { cerr << func; return error; }
The third insertion operator also increments error.d_count
when
std::endl
is inserted:
module Error; using namespace std; Error &operator<<(Error &error, ostream &(*func)(ostream &)) { if (ostream &(*endMsg)(ostream &) = endl; func == endMsg) ++error.d_count; cerr << func; return error; }
Once module.cc
has been compiled and the gcm.cache
components are
available to a using program that program can use the Error
class. Here is
a main
function doing so:
import Error; using namespace std; int main() { Error error; error << "hello " << 12 << '\n'; cout << "Number of errors: " << error.count() << '\n'; error << "after inserting std::endl: " << endl; cout << "Number of errors: " << error.count() << '\n'; }
Running the program produces the following output:
hello 12 Number of errors: 0 after inserting std::endl: Number of errors: 1
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 may define data and types (like classes) which are only accessible to the module and its partitions.
If such partitions define classes then the access rights of the members of those classes are not altered: the public members of those classes are accessible to the members of the module and its partition, their protected members are available to classes derived from those classes, defined in the module or its partitions, while their private members are not available outside of the class.
Figure 35 illustrates a simple module (Math
) having two
partitions. It shows the relationships between the module and its partitions
in the same way as the relationships between classes are commonly displayed:
the most basic (least dependent) partition is on top, the partition depending
on the topmost partition is in the middle, and the module depending on both
partitions is at the bottom. The Math
module defines a class Math
,
defining a member returning the sum of two size_t
values and a member
count
returning the number of performed additions. It's a very simple
design, illustrating the way partitions are designed. The class Add
object
performs the addition, and the class Utility
object 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
.
Partitions are not class members: partitions are specified using only a single colon instead of two, as used then defining cass member functions.
The Math
module and its Math
class are exported, so the class
Math
can be used by other source files after importing the module
Math
:
import Math;On the other hand,
Math:Add
and Math:Utility
are partitions, and
components of partitions cannot be imported by sources not belonging to
the Math
module or its partitions. Partitions, therefore, allow the
implementation of partially private components: not accessible outside of the
module, but accessible all over the module itself.
The Math
module itself exports all its components, including its
partitions, since they're all used by the exported Math
class. When
importing partitions their (Math
) module name isn't specified: since
partitions can only be defined within the context of a module the module name
is implied, and partitions are imported without mentioning their module names:
export import :Utility;
Other than importing its partitions the Math
module's interface contains
no other specific details, but it `exports imports' the <cstddef>
module
header file since the Math
class refers to the size_t
type. Here is
its module.cc
file:
export module Math; export import <cstddef>; 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); }; }
What is important is that what's imported by a
module must exist before the module's module.cc
file can be
compiled. So the partitions Utility
and Add
(specifically: their
gcm.cache/*.gcm
files) must be available before Math's module.cc
file can be compiled. Once the required *.gcm
files are available the
remaining source files of partitions and modules can be compiled. In practice,
a modmapper program (cf. section 25.6) is used to prepare the
required gcm.cache
files, whereafter the remaining source files can
be compiled as usual.
Since (cf. figure 35) the Utility
partition is the most basic,
it is covered next.
Math:Utility
partition does not depend on features of either the
Math
module or the Math:Add
partition. Since it does use the
size_t
type, it `exports imports' <cstddef>
.
Even though it's a partition its module.cc
file starts by exporting a
module, not by, e.g., exporting a partition:
export module Math:Utility;
The colon indicates that this is not a plain module but a partition. It
exports a class Utility
defining a size_t d_count
data member, a
constructor, and two simple members (in this example they're defined
inline):
export module Math:Utility; export import <cstddef>; export { 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 is defined in a separate source file (cf. section 25.5.4 below).
Math:Utility
partition the Math:Add
partition
does depend on another partition: it depends on the Math:Utility
partition. Like Utility's module.cc
it starts its module.cc
file
by exporting its partition name, followed by importing the components required
by the Math:Add
partition. The elements of the partition (in this case:
the class Add
) can now be exported:
export module Math:Add; export import :Utility; export import <cstddef>; export { 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 (cf. section 25.5.4 below).
Math
module's partitions have been defined
(cf. figure 35) the Math
module's module.cc
itself can
be defined. Math
itself is a mere module, but since it depends on its
partitions it has to export import
its partitions, in addition to exporting
its own module name. Here is its module.cc
file:
export module Math; export import <cstddef>; 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 Math
module's class Math
defines a Utility
and and an
Add
object. Note that these are just class type objects, but they're
defined by, respectively, the Math:Utility
and Math:Add
partitions. Next we'll cover the implementations of the class Math's
members.
Source files of partitions must first declare their module, followed by
importing their partition. E.g., for the Utility
partition this boils down
to:
module Math; import :Utility;Following these lines other specifications may be provided, like importing additional components (e.g., `
import <iostream>;
') or declaring
namespaces.
Since modules shouldn't use #include
directives, partitions don't need tailored (internal) header files. Instead,
as covered before, when implementing a partition component using
frame files are advised: when defining a new source file start by
copying frame
to the new source file, followed by completing its
implementation. For Math:Utility
the following frame file is used:
module Math; import :Utility; //using namespace std; Utility::() { }
Beyond the required declaration and imports components of partitions (and
modules) are implemented as usual. E.g., here is the implementation of
Math:Utility's class Utility's
constructor:
module Math; import :Utility; Utility::Utility() : d_count(0) {}
The Math:Add
partition's remaining source files also start from a
frame
file:
module Math; import :Add; //using namespace std; Add::() { }
Since frame
satisfies all requirements Add's
members can now
straightforwardly be implemented. Here's Add's
constructor:
module Math; import :Add; Add::Add(Utility &utility) : d_utility(utility) {}
and its sum
member, after calling Uility::inc
, simply returns the
sum of its two parameter values:
module Math; import :Add; size_t Add::sum(size_t lhs, size_t rhs) { d_utility.inc(); return lhs + rhs; }
The Math
module defines its own a frame
file, which is used to start
the definition of the members of its Math
module:
import Math; //using namespace std; Math::() { }
The Math
module's class Math
defines a constructor and two members,
which can be defined as usual. Its constructor:
import Math; Math::Math() : d_add(d_util) {}
Its count
member:
import Math; size_t Math::count() const { return d_util.count(); }
and its add
member:
import Math; size_t Math::add(size_t lhs, size_t rhs) { return d_add.sum(lhs, rhs); }
gcm.cache
sub-directory contains the .gcm
files of the partitions as well as the
module's own .gcm
file. For the Math
module that file is named
Math.gcm
, and its partitions have names like Math-Utility.gcm
. If the
program's gcm.cache
sub-directory offers access to those .gcm
files
then the program's files can simply define and use Math
class objects
(see also section 25.6).
As with modules and partitions no (internal) header file is needed anymore at
the program's top level. Instead a frame
file is used importing and
declaring what's used by the program level source files. Here is the frame
file used by a program using a class Math
object:
The program itself is simple: it merely contains a main
function. Here is
its definition:
import Math; import <iostream>; using namespace std; int main() { Math math; cout << 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 count: " << math.count() << '\n'; }
Once all sources (i.e., the program's source file, the module's source files,
and the partition's source files) have been compiled, the gcm.cache
files
are not needed anymore, and the final program can be constructed (i.e.,
linked) as usual. If main.o
is located in the program's main directory and
if all object files are located in a tmp/o
sub-directory, then the program
can simply be constructed by requesting the compiler to perform the linkage:
g++ main.o tmp/o/*.o
When using modules using traditional headers (.h
files) should be
avoided. Projects not developed using modules may still have to #include
headers, but except for those projects #include
directives should no
longer be used. System header files (cf. section 25.3) still exist,
but for those import
statements should be used or made available.
When designing a project it can be hard to determine which module sections can
immediately be compiled, and which ones depend on already compiled
sections. Moreover, source files belonging to modules which don't define the
module's interface may need to import modules which aren't yet available when
compiling the module's interface. For example, the interface of a module
Basic
is independent of other modules, as in
export module Basic; export { class Basic { public: void hello() const; }; }
But the 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/module.cc
must be compiled before second/module.cc
can be compiled. However, module Second
may offer a class
Second
, and an object of class Second
may very well be used by a
member of the Basic
module, as illustrated by the implementation of
basic/hello.cc
:
module Basic; import Second; import <iostream>; import <string>; using namespace std; void Basic::hello() const { cout << Second::hello() << '\n'; }
In situations like these the modules' interfaces must be compiled first (and
in the right order: the least dependent (Basic
) one first, then the other
(Second
) one). Then, once the interface files were compiled the other
source files of the modules can be compiled, even if a source file imports a
module whose module.cc
file had to be compiled after the module.cc
file of the module to which the source file belongs.
this two-step process (first the modules' module.cc
files in the right
order and then the remaining source files) quickly becomes a challenge. To
fight that challenge a module mapper is commonly used. Module mappers
inspect the modules of a project, building their dependency tree. The module
mapper icmodmap(1), a utility program of the
icmake(1) project, can be used as a module mapper.
Icmodmap expect projects using modules as follows:
CLASSES
, where
each line (ignoring C++ comment) specifies the name of a
sub-directory implementing one of the project's
components. Alternatively, not using CLASSES
, a (subset) of the
directory's sub-directories can be specified. Each sub-directory can
define a module, a module partition, a class (not part of a module,
but maybe using modules), or global-level functions (also maybe using
modules);
main.cc
defining the main
function). The top-level directory does not define a module, but its
sources may use modules;
module.cc
, defining the module's name and its interface
(instead of module.cc
another file name can be specified);
Icmodmap(1) inspects each specified sub-directory. If it contains the file
module.cc
(or its alternative name), then that file is inspected:
import
lines can specify system headers, modules and/or
partitions.
Once all module.cc
files were successfully inspected icmodmap(1)
determines the module dependencies, and if there are no circular dependencies
the interface files are compiled in the required order. Each compiled
interface file name will begin with an ordering number which is equal to the
line number in the CLASSES
file (or sub-directory order number of the
inspected sub-directories), e.g., 1module.o, 2module.o,
etc.
When icmodmap(1) successfully returns then all module defining interface
files were successfully compiled, and the project's top-level directory
contains a sub-directory gcm.cache
containing the .gcm
files of all
modules and partitions; and each inspected sub-directory has received a
soft-link gcm.cache
to the top-level gcm.cache
sub-directory, allowing
the files in each sub-directory to import the project's modules. Next,
the source files in the directories listed in the CLASSES
file (and
possibly the source files(s) in the top-level diretory itself) can be compiled
as usual, e.g., using a build-utility.
/usr/include/c++/14.
Currently, only these header files are installed, and
not their corresponding module-compiled .gcm
equivalents. Section
25.3 covered how to construct the module-compiled versions of those
system header files. Once the system header files were compiled and made
available next to their traditional header files (so iostream
and
iostream.gcm
are both available in the same directory) then module-aware
source files can import those header files (i.e., import <iostream>;
)
instead of including them (as in #include <iostream>
).
When constructing a library whose header files are available in a
sub-directory of the standard include directory (like the headers of the
bobcat
library, cf. section 14.9), then the header files of such
libraries are traditionally also available using #include
directives like
#include <bobcat/exception>
. To prepare the headers of such libraries for
use by module-aware source files is easy: simply construct their
module-compiled versions like the way the module-compiled versions of the
standard header files were constructed:
cd
to the directory containing the library's header files;
libclass
'
execute
g++ --std=c++26 -Wall -fmodules-ts -x c++-header libclass
gcm.cache/libclass.gcm
file to the current working
directory.
Since the system header files and their module-aware equivalents are located
in the standard system files directories (all below /usr
), the compiler
will look for module-aware .gcm
files either in the project's
gcm.cache
sub-directory or it will look for those files in the standard
system directories, starting its search in the gcm.cache
sub-directory. Hence, the icmodmap(1) module mapper, when defining a
project's gcm.cache
sub-directory, defines the soft-link usr -> /usr
.
What if a library was designed merely using modules (so no traditional
header files were used)? In that case, using the gcm.cache
organization
used by icmodmap(1), the modules' .gcm
files are all available
in the library's gcm.cache
directory. When compiling source files of
projects using the library's modules the .gcm
files of the used library's
modules must then be available in the project's gcm.cache
sub-directory.
Copying the .gcm
files of the used library modules is not necessary,
soft-links can also be used. But often modules import other modules, and so
those modules depend on other modules, whose .gcm
files must then also be
available in the project's gcm.cache
sub-directory. The icmodmap(1)
option -
-dependencies
can be used to determine the modules which are
required by a module. When a (library) module is used then the .gcm
files
on which the module depends must also be available in the project's
gcm.cache
sub-directory.
As an illustration consider the situation where a library defines three
modules: Module1, Module2,
and Module3. Module1
imports External
,
which is a module defined in some other context; Module2
imports
Module1
, and Module3
imports Module2
(cf. figure 36).
The External
module doesn't belong to the library. The External.gcm
file, therefore, lives elsewhere. So, when the library is constructed the
library's gcm.cache
directory must contain a soft-link (or copy) to the
actual location of the External.gcm
file.
Now, when a project uses modules from the library the project's gcm.cache
sub-directory must contain soft-links to the used modules, but also soft-links
to the modules used by the used module. The question which modules are
actually required can be answered by icmodmap(1): it offers the
--dependencies
option showing which modules are required by each of the
library's (or any project's, for that matter) modules. When executing
`icmodmap -d .
' in the library's base directory it shows:
External:e Module1: External:e Module2: External:e Module1 Module3: External:e Module1 Module2The
:e
which is appended to the External
module name indicates
that External.gcm
is not part of the library itself, but is defined by
some other project. The library's gcm.cache
will therefore most likely
offer a soft-link to External.gcm's
real location. Furthermore, the
output shows that a project importing Module2
not only must prepare
soft-links in its gcm.cache
sub-directory to the library's Module2.gcm
file, but also to the library's External.gcm,
and Module1.gcm
files.
To construct the module-aware versions of that library's header files the
procedure described in the previous section can be used. However, since the
header files of a local library aren't located in the standard system files
directories, they must be made available in the gcm.cache
sub-directory of
the project using them.
It is possible to copy the headers and their module-compiled versions to the
gcm.cache
sub-directory, but a simpler alternative
exists which can be used by multiple module-aware projects which may all
depend on the local library.
First some preparatory steps are performed (only once):
MyLib
is developed below the
sub-directory ~/mylib
. MyLib
has multiple sub-directories,
each of which defines a header file declaring the component (e.g.,
class) defined in its sub-directory, like classa
(defining module
ClassA
), classb
(defining module ClassB
), ...;
~/include/mylib
is defined, and each header file of the MyLib
library can be reached via a soft-link from ~/include/mylib
. E.g.,
~/include/mylib/classa -> ~/mylib/classa/classa
.
Next, if a module-aware project must import module-aware variants of (some of)
MyLib's
header files,
~/include/mylib
, be constructed in that directory;
-isystem ~/include/mylib
to the compiler flags. E.g., to compile a
project's main.cc
file use
g++ -c --std=c++26 -fmodules-ts -isystem ~/include/mylib main.cc
import <classa>;
When the local library was designed merely using modules, then a similar procedure can be used as described in the previous section for system libraries.