C++20 Modules and Project Structure, CSC01B1 P7 – Study Notes
offline

Difficulty: Intermediate | Prerequisites: Basic C++ syntax, header files, compilation basics

Big Picture

This practical covers C++20 modules, a modern replacement for header files that gives you cleaner compilation boundaries and explicit control over what code is visible to other files. You also build a batch file to automate the multi-step compilation process that modules require. If you are comfortable with classes, structs, enums, and basic compilation, you are ready for this material.


TL;DR

C++20 modules replace traditional #include header files with export module and import statements, giving the compiler a proper dependency graph instead of textual copy-paste. The LibUtility module exports a UJPixel struct (RGB colour values) and an ExitCode enum used across the project. A Windows batch file orchestrates the entire compile-link-run pipeline, compiling each source file separately with -fmodules-ts to respect module dependencies.

Key Terms

Module (C++20)

A self-contained compilation unit declared with module; (module implementation) or export module ModuleName; (module interface). Replaces header files by letting the compiler understand dependencies directly.

In simple terms, a module is a file that says "here is what I offer" and "here is what I need," and the compiler figures out the rest.

export

The keyword that makes a declaration (struct, class, function, enum) visible to any file that imports the module. Anything not marked export stays private to the module.

Think of it as the module's public API: if you do not export it, nobody outside the module can see it.

import

The statement that brings another module's exported declarations into the current file. Replaces #include for module-based code.

In simple terms, import LibUtility; means "give me access to everything LibUtility exported."

UJPixel

A plain struct exported from LibUtility with three int fields: intRed, intGreen, intBlue. Represents a single RGB pixel. Uses aggregate initialisation, so you can write {250, 150, 50} to construct one.

ExitCode

An unscoped enum exported from LibUtility with values SUCCESS, ERROR_RANGE, ERROR_ARGS, ERROR_CONV, and ERROR. Used as return values for main() and exit() calls to signal how the program terminated.

Batch file (.bat)

A Windows script that runs a sequence of shell commands. In this project it creates directories, compiles each .cpp file separately, links the object files, cleans up build artefacts, and runs the resulting program.

-fmodules-ts

A GCC compiler flag that enables C++20 module support (the "technical specification" implementation). Required on every g++ compilation line that uses module or import.

Row-major order

A way of laying out a 2D array in a flat, one-dimensional sequence: all columns of row 0 first, then all columns of row 1, and so on. The formula to convert (row, col) to a flat index is row * numCols + col.

Think of it as reading a grid left to right, top to bottom, the same way you read text.


Core Content: The C++20 Module System

Module declaration and the global module fragment

  • Every module file starts with module; on its own line. This opens the "global module fragment," a special zone where traditional #include directives live.

  • After the includes, export module ModuleName; declares the module's name and makes everything below eligible for export.

  • The global module fragment exists because modules cannot export macros or preprocessor definitions. Standard library headers that rely on macros (like , ) must be included the old way, before the export module line.

The LibUtility module

  • Declared as export module LibUtility;

  • Exports two things:

    • UJPixel: a struct with intRed, intGreen, intBlue (all int). This is the fundamental data unit for the image class.

    • ExitCode: an unscoped enum providing named exit codes (SUCCESS = 0, ERROR_RANGE = 1, etc.).

  • The struct uses aggregate initialisation, so you can write UJPixel pixel = {255, 0, 128}; or pass {255, 0, 128} directly as an argument.

The UJImage module

  • Declared as export module UJImage;

  • Imports LibUtility with import LibUtility; to gain access to UJPixel and ExitCode.

  • Defines type aliases at module scope (not exported):

    • using Row = UJPixel*; (a pointer to an array of pixels, one row of the image)

    • using Grid = Row*; (a pointer to an array of rows, the entire 2D pixel grid)

  • These aliases are private to the module. Code that imports UJImage cannot see Row or Grid directly.

How import differs from #include

  • #include physically copies the header's text into your file before compilation. Every file that includes it reprocesses every line.

  • import tells the compiler to load a pre-compiled module interface. The compiler reads the module once, caches the result, and reuses it.

  • Modules enforce a proper dependency order: if UJImage imports LibUtility, then LibUtility must be compiled first. The batch file handles this by listing source files in dependency order.

The main.cpp file

  • Uses #include for console I/O (not a module), then import LibUtility; and import UJImage;.

  • Demonstrates constructing UJImage objects, using overloaded operators, and outputting the image as PPM.

  • Returns SUCCESS (from ExitCode) to indicate clean termination.


Core Content: The Batch File Compilation Workflow

Directory setup

  • The batch file creates three directories if they do not already exist: ..\bin (for the compiled executable), ..\docs, and ..\output (for the generated PPM image).

  • The if not exist guard prevents errors on repeated runs.

Compilation order matters

  • With modules, the compiler must process dependencies before the files that import them. The batch file compiles in this order:

    1. LibUtility.cpp first (no dependencies)

    1. UJImage.cpp second (imports LibUtility)

    1. main.cpp third (imports both)

  • Each file is compiled with g++ --std=c++20 -fmodules-ts -c FileName.cpp. The -c flag means "compile only, do not link" and produces a .o object file.

  • The final link step joins all .o files into one executable: g++ LibUtility.o UJImage.o main.o -o ..\bin\prog.

The gcm.cache directory

  • When GCC compiles a module, it creates a gcm.cache directory containing the cached module interface. This is an implementation detail of GCC's module support.

  • The batch file deletes gcm.cache and all .o files after linking to keep the workspace clean. This also forces a fresh recompilation on the next run, which avoids stale cache issues during development.

Running the program

  • The batch file executes ..\bin\prog directly. There is a commented-out line showing how you could redirect stdout to a file (> ..\output\image.ppm) to capture the PPM output.

  • The pause command at the end keeps the console window open so you can read the output.

Real-world application

Build automation scripts like this batch file are a simplified version of what tools like Make, CMake, and Ninja do in professional C++ projects. Understanding the dependency order and compilation steps here prepares you for working with those systems.


Common Misconceptions

  • "Modules completely replace #include." They do not, at least not yet. Standard library headers that depend on preprocessor macros still need #include in the global module fragment. The module; line at the top of the file opens that fragment specifically for this purpose.

  • "Compilation order does not matter with modules." It does. If you compile main.cpp before UJImage.cpp, the compiler will not find the UJImage module interface. This is different from header-based code, where include guards handle redundancy but order rarely matters.

  • "export makes everything in the module public." Only declarations explicitly marked export become visible to importers. The Row and Grid type aliases in UJImage.cpp are not exported and remain internal.

  • "A struct and a class are the same thing." They are almost the same in C++, but the default access differs: struct members are public by default, class members are private by default. UJPixel is a struct because all its fields are meant to be directly accessible.


Why It Matters / Exam Flags

  • ⚠️ Know the difference between module; (global module fragment) and export module Name; (module interface declaration). They serve different purposes and appear in a specific order.

  • ⚠️ Be able to explain why LibUtility.cpp must be compiled before UJImage.cpp. This tests your understanding of module dependency graphs.

  • ⚠️ Understand why UJPixel uses aggregate initialisation ({r, g, b}) rather than a constructor. Structs with no user-declared constructors support this by default.

  • ⚠️ Know what constexpr means in the context of the class constants (e.g. DEFAULT_PIXEL = {255, 255, 255}). It means the value is computed at compile time.

  • ⚠️ Be ready to trace through the batch file and explain what each step produces (.o files, the executable, the gcm.cache directory).


Quick Self-Test

  1. True or false: import LibUtility; physically copies the contents of LibUtility.cpp into the importing file. (False. import loads a pre-compiled module interface; #include is the one that copies text.)

  1. Fill in the blank: The line module; at the top of a module file opens the ______ ______ ______. (global module fragment)

  1. True or false: The Row and Grid type aliases defined in UJImage.cpp are visible to main.cpp. (False. They are not exported.)

  1. Fill in the blank: The -c flag tells g++ to ______ without ______. (compile without linking)

  1. True or false: You can compile main.cpp before UJImage.cpp when using modules. (False. The module interface must exist before any file that imports it can compile.)


Practice Q&A

Q: What is the purpose of the global module fragment (the code between module; and export module Name;)?

A: It provides a place to use traditional #include directives for standard library headers that rely on preprocessor macros. Modules cannot export macros, so headers like and must be included in this fragment.

Q: Why does the batch file compile LibUtility.cpp before UJImage.cpp?

A: Because UJImage.cpp contains import LibUtility;. The compiler needs the compiled module interface for LibUtility to exist before it can process any file that imports it.

Q: What would happen if you removed the export keyword from export struct UJPixel in LibUtility.cpp?

A: The UJPixel struct would become internal to the LibUtility module. Any file that imports LibUtility would no longer be able to use UJPixel, causing compilation errors in UJImage.cpp and main.cpp.

Q: Explain the difference between using Row = UJPixel*; and typedef UJPixel* Row;.

A: They are semantically equivalent. Both create a type alias where Row means "pointer to UJPixel." The using syntax is the modern C++11 form and is generally preferred because it works with templates more cleanly.

Q: What does the -fmodules-ts flag do, and why is --std=c++20 also required?

A: --std=c++20 tells GCC to use the C++20 standard, which is where modules were introduced. -fmodules-ts specifically enables GCC's module implementation (based on the technical specification). Both are needed because module support is not enabled by the standard flag alone in GCC.


Connections to Other Topics

This material connects directly to operator overloading and dynamic memory management (covered in the UJImage class), since the module system is what makes those classes available across files. If you have studied header guards and #pragma once, modules are the next evolution of the same problem: controlling what is visible and avoiding duplicate definitions.


Related Terms / Search Tags

C++20 modules, export module, import statement, global module fragment, module interface unit, fmodules-ts, g++ modules, batch file compilation, Windows batch script, UJPixel struct, ExitCode enum, aggregate initialisation, type alias using, typedef vs using, row-major order, compilation order, object file linking, gcm.cache, PPM image format, constexpr class constants