#include
directives. We cover import
and export
statements, partitions, submodules, how to combine modules with legacy code, and more.For a long time, the #include
directive has been the primary way we import functionality from C++ libraries and organize more significant projects. However, it is an excessively crude approach, effectively copying all the code from one source file and pasting it to another. As we covered previously, this has some issues:
#pragma once
#include
directivesIt is an ongoing mission of the C++ community to reduce the dependency on the preprocessor. In the C++20 spec, an alternative to the #include
directive was introduced. They are called modules.
import
StatementsPreviously, we imported iostream
(and similar headers) using the #include
directive as follows:
#include <iostream>
int main() {
std::cout << "Hello World!";
}
To use the equivalent module, we update our code to use the import
statement, as below:
import <iostream>;
int main() {
std::cout << "Hello World!";
}
Note, import
is a C++ statement rather than a preprocessor directive, so we end it with a semicolon ;
like any other statement.
iostream
Modules are a relatively new addition to the language, added in C++20. As such, tooling support can be slightly limited.
Typically, compilers can take a few years to implement the latest features for a language, and a few more years to enable those features by default.
If attempting to use modules is resulting in compiler errors, you may need to update or tweak the settings of the compiler you’re using.
If you’re using Visual Studio, the following settings are things you may need to change. They are available by navigating to Project > Properties from the top menu bar:
C/C++ > Language > C++ Language Standard: C++20 (or later)
C/C++ > Language > Enable Experimental C++ Standard Library Modules: Yes
To create our own module, we need to create a module interface file. This is just a typical file that will contain C++ code, similar to the .cpp
or .h
files we’ve been creating. There is no agreed convention on naming yet, but using the extension .cppm
for a module interface file is becoming common.
On the first line of our module interface file, we use two new keywords, export
, and module
, followed by a name we want to give our module. The naming restrictions for modules broadly follow the same rules as naming variables.
Here, we define a module called Math
:
// Math.cppm
export module Math;
Within this file, we can write C++ code as normal:
// Math.cppm
export module Math;
import <iostream>;
namespace Math {
void SayHello() {
std::cout << "Hello!";
};
};
constexpr float Pi{3.14};
void SomeMathFunction(){};
class SomeClass {};
using Alias = SomeClass;
Unlike the traditional #include
directive, modules support encapsulation natively. We can decide which parts of our module are private, and which parts are public.
By default, everything is private. So, the namespace, class and everything we defined in the previous example is private - it is not part of the module interface.
Our module can be selective with what it wants to make public. We do this by adding the export
keyword before anything we want to be part of our public interface.
Pretty much anything with an identifier can be exported - variables, functions, using statements, classes, structs, namespaces, and more.
Below, we export our Pi
variable and our Math
namespace. When a namespace is exported, all of its members are automatically exported too:
// Math.cppm
export module Math;
import <iostream>;
// Public
export namespace Math {
void SayHello() {
std::cout << "Hello!";
};
};
export constexpr float Pi{3.14};
// Private
void SomeMathFunction(){};
class SomeClass {};
using Alias = SomeClass;
In one of our other files, we can now import
our module and use its exported features:
// main.cpp
import <iostream>;
import Math;
int main() {
std::cout << Pi;
Math::SayHello();
// Error - not exported
SomeMathFunction();
}
We can export multiple identifiers at once from our module using an export block:
export module Math;
import <iostream>;
export {
namespace Math {
void SayHello() {
std::cout << "Hello!";
};
};
constexpr float Pi{3.14};
}
void SomeMathFunction(){};
class SomeClass {};
using Alias = SomeClass;
export void Blah(){};
With the #include
directive, we saw that the inclusion was recursively. A single #include
directive can paste in a file that itself has yet more #include
directives, and that continues any number of levels deep.
Below, our main.cpp
is using iostream
functions even though it is not including them. It is gaining indirect access to them by including a file that happens to include <iostream>
:
// Greetings.h
#pragma once
#include <iostream>
void Greet() {
std::cout << "Hello!";
}
// main.cpp
#include "Greetings.h";
int main() {
Greet();
std::cout << "\nHello!";
}
Hello!
Hello!
Our code having hidden dependencies like this is not a good thing. In complex projects, changes in unrelated files causing other files to stop compiling is a headache.
C++ modules do away with this. When we import
a module, its sub-dependencies don’t magically become visible in the file we imported it to:
// Greetings.cppm
export module Greetings;
import <iostream>;
export void Greet() {
std::cout << "\nHello!";
}
// main.cpp
import Greetings;
int main() {
Greet();
std::cout << "Hello!";
}
In this example, we have not imported <iostream>
into main.cpp
, so the use of std::cout
is problematic.
The phrases commonly used to describe this distinction are reachable vs visible. Within main.cpp
in this example, std::cout
is reachable, so line 4 works. But, it is not visible, so line 5 is not valid.
For std::cout
to be visible, the file where it is defined needs to be imported explicitly:
// main.cpp
import Greetings;
import <iostream>;
int main() {
Greet();
std::cout << "\nHello!";
}
Hello!
Hello!
Preprocessor macros are usable within modules, but cannot normally be exported from a module. There is an exception, which we’ll cover later in this lesson, but under normal circumstances, #define
directives are restricted to just the file module file where they’re used:
// Greetings.cppm
export module Greetings;
import <iostream>;
#define SayHello
export void Greet() {
#ifdef SayHello
std::cout << "Hello from the Module!";
#endif
}
// main.cpp
import Greetings;
import <iostream>;
int main() {
Greet();
#ifdef SayHello
std::cout << "Hello from main!";
#endif
}
Hello from the Module!
For projects that require importing macros, and also want to use modules, the recommended approach is to move the macros into dedicated files and #include
them where they’re needed in the traditional way.
When we want to break up a large module into smaller chunks, we have two options.
A module partition file is implemented in the same way as a regular module. The only difference is that a partition has a semicolon in the name.
A partition name has three parts:
:
For example, if our Math
module had a partition called Geometry
, we would call the partition Math:Geometry
.
In the following example, we create a Math
module with two partitions: Math:Geometry
and Math:Algebra
It would look like this:
// MathAlgebra.cppm
export module Math:Algebra;
export float MilesToKM(float Miles) {
return Miles * 1.61;
}
// MathGeometry.cppm
export module Math:Geometry;
export constexpr float Pi{3.14};
We then create the main module interface file for the Math
module, which exports the module, as well as imports and re-exports all of the partitions. The syntax looks like this - note for our partitions we omit the part of the name that comes before the semicolon:
// Math.cppm
export module Math;
export import :Algebra;
export import :Geometry;
The partitions are not accessible to consumers. They simply use our module as normal, as if it was not partitioned:
// main.cpp
import Math;
import <iostream>;
int main() {
std::cout << "Result: " << MilesToKM(100);
}
Result: 161
The notion of a submodule is not an official part of the C++ language, but it is something that is adopted by the community. Module names can include .
characters, and we use this to organize multiple modules into a hierarchy. For example, we can have a module called Math
, with “child” modules called Math.Geometry
and Math.Algebra
.
Whilst partitions must use a strict naming pattern, containing the parent module and a semicolon, submodules can have any name. The naming convention of ParentModule.ChildModule
is just a common convention.
Unlike partitions, consumers can see this hierarchy of a submodule hierarchy, and interact with it. For example, they can choose to import just Math.Geometry
rather than the whole Math
 module.
To set this up, we create submodules in the normal way, including a .
in their name:
// MathAlgebra.cppm
export module Math.Algebra;
export float MilesToKM(float Miles) {
return Miles * 1.61;
}
// MathGeometry.cppm
export module Math.Geometry;
export constexpr float Pi{3.14};
We then create the parent module, which “rolls up” all the submodules, by both importing and re-exporting them. Note, with partitions, we omitted the part of the name before the :
but with the submodule pattern, we must include the full module name in each of our export import
 statements:
// Math.cppm
export module Math;
export import Math.Algebra;
export import Math.Geometry;
Now, within any other file, our consumers can choose to import just a submodule, or import the parent and get everything:
// main.cpp
import Math.Algebra;
import <iostream>;
int main() {
std::cout << "Result: " << MilesToKM(100);
}
Result: 161
We're not restricted to one level of hierarchy - we can nest modules as deep as we want. For example, we can have a Math.Geometry.Circles
 module.
When working with large projects and the #include
directive, it became important to separate code into header files and implementation files to keep compile times down.
With modules, that is no longer a factor. However, many will still want to implement this separation for code organization reasons.
Below, we show two approaches to this. First, we move the interface to the top of the file, with implementations at the bottom:
// Character.cppm
export module Character;
import <string>;
import <iostream>;
// Definition
export class Character {
public:
Character(std::string Name);
void SayHello();
std::string GetName();
private:
std::string Name;
};
// Implementation
Character::Character(std::string Name)
: Name{Name} {}
void Character::SayHello() {
std::cout << "Hello there! The name is "
<< Name;
}
std::string Character::GetName() {
return Name;
}
We can also split our module into an interface file, and one (or more) implementation files.
In our implementation file(s), we use the module declaration at the top, without the export
statement. For example:
module Character;
// Character implementation here
This has the effect of importing the Character
module and its dependencies from our interface file, and also declaring that the subsequent code is providing implementations for that module. We then provide implementions in the normal way.
Below is an example of splitting our previous Character
module into an interface and implementation file:
// Character.cppm
export module Character;
import <string>;
import <iostream>;
// Definition
export class Character {
public:
Character(std::string Name);
void SayHello();
std::string GetName();
private:
std::string Name;
};
// Character.cpp
module Character;
Character::Character(std::string Name)
: Name{Name} {}
void Character::SayHello() {
std::cout << "Hello there! The name is "
<< Name;
}
std::string Character::GetName() {
return Name;
}
When we split a complex module across many partitions, we’ll often come across scenarios where we want some segments of code to be shared across multiple files.
An internal partition (sometimes called an implementation partition) can help us here. This is a partition that is a repository for shared code to be used across our module, but not exported as part of the public interface.
A minimal internal partition for a Math
module would look something like this:
module Math:Helpers;
constexpr float Pi{3.14};
Internal Partitions are another area where compilers do not yet have a shared approach. If the above code does not compile, we need to change some settings. However, the way to do that depends on the compiler being used, so will require some investigation.
If this file does not compile in Visual Studio, we can select it in the Solution Explorer and press Alt + Enter to bring up the properties menu, or right click it and select Properties from the dropdown.
Under Configuration Properties > C/C++ > Advanced > Compile As, selecting the “Compile as C++ Module Internal Partition” option should resolve any issues.
Once we’ve set up our internal partition, we can import and use it in any of our other module files:
export module Math;
import :Helpers;
export class Circle {
public:
float Circumference(float Radius) {
return Radius * 2 * Pi;
}
};
It’s still very early in the lifespan of modules. If we want to use them now, we’re going to be mixing them with third-party libraries and other code that is still using traditional header files.
Assuming we can’t convert those files to proper modules, there are two workarounds. The first is we just use the import
statement, and provide a path to the header file within quotes. This creates what is known as a Header Unit:
// Character.h
#pragma once
#include <iostream>
class Character {
public:
void SayHello() { std::cout << "Hello!"; };
};
// main.cpp
import "Character.h";
int main() {
Character Greeter;
Greeter.SayHello();
}
Hello!
Header units are a stop-gap measure implemented by compilers. In essence, they convert a header file to a module, and everything in that header file (including preprocessor macros) is exported. The exact implementation here varies from compiler to compiler, and it doesn’t always work.
The other option we have for mixing traditional code with modules is to just #include
the older files within our module files. However, we need to be careful there, as we explain in the next section.
#include
directive within modulesInevitably, we’ll be tempted to place an #include
directive within the purview of our module:
// Character.cppm
export module Character;
#include <SomeLibrary>
// ...
This may not work, but even if it does, it’s not something we should do. Given #include
is simply a crude copy-and-paste, dropping one below our module
declaration is going to cause some issues.
When this code gets preprocessed and sent for compilation, the compiler is just going to see the entire contents of whatever we included as part of our Character
module. That’s almost never what we intend so, in most compilers, we should at least receive a warning:
'#include <SomeLibrary>' in the purview
of module 'Character' appears erroneous
But, if we still need to #include
the library, what do we do?
In the world of C++ modules, all code must be attached to a module. Anything that is not explicitly attached to a module (such as the main
function) is part of the global module.
This is also where we want the output of our #include
directives to go. When we need to #include
a file within a module, we can specify it as being part of that global module, using a global module fragment. It looks like this:
module; // Global module fragment
#include <SomeLibrary> // Legacy include
export module Character; // Named module begins
import <iostream>;
export class Character {
public:
void SayHello() { std::cout << "Hello!"; };
};
The only use case for global module fragments is to solve these preprocessor-related problems. As such, we can't put any other type of code there - only preprocessor directives are allowed within the global module fragment.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.