Getting Started with C++20 Modules: A Step-by-Step Guide for Beginners

17 Dec 2024 by Daniel Gakwaya | comments

C++20 introduced modules, a modern way to structure and manage code that replaces the traditional #include system. Modules improve compilation speed, enhance encapsulation, and resolve longstanding issues like macro conflicts and double inclusion. This guide will help you understand the basics and practical uses for C++20 modules. This guide is part of our C++20 Masterclass, where we cover modules in detail and provide real-world examples to help you master their use in professional projects. This guide assumes you have a C++20 ready enviroment. If you don’t, you can follow our C++20 installation guide.

What are C++20 Modules?

Modules organize C++ programs into logical units. They consist of module interfaces (public-facing parts) and module implementations (private parts). Unlike header files, modules avoid textual inclusion, parsing code only once per build.

Key Benefits of Modules:

  • Faster Compilation: Avoids repeated parsing of headers.
  • Better Encapsulation: Explicit control over symbol visibility.
  • Improved Maintainability: Easier management of large projects.

Module File Extensions

C++20 does not enforce a file extension for modules, but common conventions have emerged:

  • ixx: Stands for interface extension, used for module interface files.
  • cppm: Another less common extension.

For clarity and consistency, this post uses the .ixx extension.

Basic Module with a Single File

Below is an example of a simple module with a single file:

//Module code stored in a file named math.ixx

module;  

#include <iostream> 

export module math; 

//Import statements for other modules usually show up here

//What follows after here is the module purview
export int add(int a, int b) {  
    return a + b;
}

export class Point {
public:
    int x, y;
    Point(int x, int y) : x(x), y(y) {}
};

This file declares a module named math with an add function and a Point class. The export keyword makes these entities visible outside the module. The module name is specified in the export module math; line. The module name doesn’t have to match the file name, but it’s a common practice for clarity. Our module file is made up of three parts:

  1. Global Module Fragment: This part is optional but comes right after the module; line. It’s often used to include any necessary headers or other code that applies to the entire module. In this example, the global module fragment only includes the header.

  2. Module Preamble: This follows the export module ; declaration. It may contain import statements to bring in other modules.

  3. Module Purview: This part contains the bulk of the module’s implementation, such as function and class definitions. It’s where the actual logic of the module lives, and it is typically not visible outside unless explicitly exported.

Separating Declarations and Definitions in a Single Module File

In the name of readability and maintainability, it’s a good practice to separate declarations from definitions. It is possible to do that in a single module file like this:

module;

export module math; // A module named math

export int add(int a, int b); // Declaration

int add(int a, int b) {       // Definition
    return a + b;
}

Separating Declarations and Definitions Across Multiple Files

For larger projects, declarations and definitions can reside in different module files. In the example below, the declaration lives in the module interface file named math.ixx, while the definition is in the module implementation file named math.cppm:

Module Interface File (math.ixx):

module;

export module math;

export int add(int a, int b); // Declaration

Module Implementation File (math.cppm):

module;

module math;

int add(int a, int b) {       // Definition
    return a + b;
}

Notice that in the module implementation file, we use the module math; directive to specify that this file is part of the math module. We can now use our math module in the main.cpp file as shown below:

import math;

#include <iostream>

int main() {
    std::cout << "3 + 4 = " << add(3, 4) << '\n';
    return 0;
}

Export import

The export import directive allows you to import a module and make it available to other modules that import the current module. This is useful when you want to re-export a module that you import. Here’s an example.

File: utility.ixx

export module utility;

export int multiply(int a, int b) {
    return a * b;
}

File: math.ixx

export module math;

export import utility; // Re-export the utility module

export int square(int x) {
    return multiply(x, x);
}

File: main.cpp

import math;

#include <iostream>

int main() {
    //Use the multiply function from the utility module
    std::cout << "3 * 4 = " << multiply(3, 4) << '\n';
    return 0;
}

The export import directive in the math module allows the utility module to be used in the main.cpp file without explicitly importing it. This makes it easier to manage dependencies and keep the codebase clean.

Submodules

Submodules allow you to organize a module into smaller, more manageable parts. Suppose that we have a graphics module made up of the image and render submodules. Here’s how you can structure the code.

File: graphics_image.ixx

export module graphics.image;

export void load_image() {
    // Image loading logic
}

File: graphics_render.ixx

export module graphics.render;

export void render_image() {
    // Image rendering logic
}

File: graphics.ixx


export module graphics;

export import graphics.image;
export import graphics.render;

File: main.cpp

import graphics;

int main() {
    load_image();
    render_image();
    return 0;
}

Please note that the directive such as export import graphics.image; has nothing special in it. The only apparent difference with what we are used to is that it has a dot in it. The compiler doesn’t do anything different with the dot. graphics.image is just a name, and it doesn’t have any special meaning to the compiler. But we can use it to organize our code in a way that makes sense to us. Looking at our code, one understands that image and render are live under the same roof.

The powerful thing here is that you can choose to use the graphics.image module on its own without the graphics.render module. This is a great way to manage dependencies and keep your codebase clean. The graphics module acts as a facade that exposes the image and render submodules to the outside world. You can use it if you want both image and render functionalities imported in one go.

Conclusion

C++20 modules represent a significant advancement in modern C++ development, offering a more efficient and maintainable approach to code organization. They help reduce compilation times and enhance encapsulation, making your projects more manageable and scalable. By following the steps outlined in this guide, you can begin incorporating modules into your work and reap the benefits they provide. For a deeper dive into C++20 modules, including advanced topics and best practices, you can join our C++20 Masterclass. There, we cover modules in detail and provide real-world examples to help you master their use in professional projects.

Qt Training Services

LearnQtGuide

Clear, up front courses on Qt. We have made it our business to provide the best online learning resources for Qt Development. We put in the required effort to make sure the code resources coming with the courses are up to date and use the latest tools to reduce the hustle for students as much as possible. We already have the basic courses on Qt C++ and QML out and there are more in the pipeline.

See our courses

© 2023 LearnQt Guide. All Rights Reserved