Understanding C++20 Modules: A Comprehensive Guide
Written on
Chapter 1: Introduction to C++20 Modules
In modern C++, we typically include libraries using the #include directive, which involves transferring code from header files to source files. While this method is prevalent, it has significant drawbacks:
- Multiple Inclusions: A source file may be included multiple times in the same translation unit. To mitigate this, developers often use #pragma once or #ifndef.
- Code Duplication: This can lead to increased compilation times. Any changes to a header file necessitate recompiling all files that include it.
- Order of Inclusions: The sequence of #include statements can sometimes result in unexpected compilation errors.
C++20 introduces a novel alternative to the traditional #include directive: modules. Let's explore the concept of modules in depth.
Section 1.1: Module Units
A C++ module comprises one or more translation units (TUs) that incorporate specific keywords for declaration. These translation units are termed module units. Non-module units belong to an anonymous global module that contains standard non-module code and lacks an interface.
Subsection 1.1.1: Global Module Fragment
Module units can initiate with a global module fragment, which facilitates direct utilization of existing code when importing headers is not feasible, particularly when preprocessor macros are involved.
For instance:
module;
#ifdef Say
void hello();
#endif
export module Foo;
// purview
void world();
It's crucial to note that if a module features a global module fragment, its initial declaration must be module;. Any deviation will trigger an error.
Subsection 1.1.2: Purview
Purview refers to the entire scope of a module, extending from the module declaration to the end of the translation unit. For instance:
void hello(); // <- Not within the purview of the Foo module
export module Foo; // <- Within the purview of the Foo module
void world();
export void GetData();
Subsection 1.1.3: Private Module Fragment
The main module interface unit can incorporate a private module fragment as a suffix. This fragment can only exist in the primary module interface unit, ensuring that the module's interface and implementation remain encapsulated within a single translation unit.
For example, if we want to define a Shape class and compute its area, we can expose only the necessary interface while keeping implementation details hidden:
export module Shape;
export class Shape {
public:
virtual double CalculateArea() = 0;
};
export { std::shared_ptr CreateRectangle(double length, double width); }
module :private; // This is where it appears
class Rectangle : public Shape {
private:
double length;
double width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
double CalculateArea() override { return length * width; }
};
std::shared_ptr CreateRectangle(double length, double width) {
return std::make_shared<Rectangle>(length, width);
}
However, it's important to recognize that support for private module fragments may vary across compilers. For example, GCC version 13 currently does not support them, while Clang version 16 does.
Chapter 2: Utilizing Modules
Section 2.1: Creating a Module
Creating a module resembles defining a header file, typically with a .cppm extension. The export and module keywords precede the module name to create an exportable module:
// foo.cppm
export module foo;
// main.cc
import foo;
The export keyword can be used for various entities, including functions, classes, and variables, with two common syntax forms available.
Section 2.2: Exporting Entities
A key question arises: what can be exported? Variables, classes, structs, functions, namespaces, template functions/classes, and concepts are exportable. However, internal linkage entities, such as static variables and functions in anonymous namespaces, cannot be exported.
For example:
export static constexpr double PI = 3.14; // Error: Cannot be exported
Export declarations must occur at the namespace level.
Section 2.3: Importing Modules
When it comes to importing, certain rules apply:
- A module cannot import itself.
- In a module unit, all imports must precede any declarations.
- Only global-scope imports are permitted.
Section 2.4: Including in Modules
To include files in a module, you can replace #include with import:
export module foo;
import <iostream>;
For compiling with GCC-13, you may need to compile system headers explicitly.
Chapter 3: Module Decomposition
When breaking down a large module into smaller components, two methods can be employed: module partitions and submodules.
Section 3.1: Module Partitions
This allows for splitting a module into multiple files without user visibility. For instance:
export module shape;
export import :circle;
export import :rectangle;
Section 3.2: Submodules
Submodules allow for dividing a main module into various nested submodules. The main module's name must precede submodule imports.
Chapter 4: Interface and Implementation
Even with modules, you can still adhere to the traditional code separation approach. For example, to define a shape and its drawing functionality:
export module shape;
export class Shape {
public:
Shape();
void Draw();
};
// Implementation
import shape;
import <iostream>;
Shape::Shape() {}
void Shape::Draw() { std::cout << "draw a shape" << std::endl; }
References
This video discusses the packaging and binary redistribution of C++20 modules, providing insights into their structure and benefits.
In this video, Cameron DaCamara shares practical applications of C++20 modules and the future of tooling surrounding them.