Into the Future with Modules

Modules replace the traditional header-based inclusion system inherited from C. Other modern or modernized languages support modules, demonstrating their relevance and importance. Even FORTRAN, the oldest language from 1957 and my first programming language, has adopted modules.

An icon indicating this blurb contains information

Another heralded feature added in C++20 is the three-way-comparison or spaceship operator(<=>). FORTRAN, as it was known way back then, had a three-way conditional, the arithmetic if:

1 IF (expression) label1, label2, label3

The label jumped to was selected by <0, ==0, or >0; maybe C++ will someday catch up fully with FORTRAN, although Fortran 90 abandoned this conditional. Will C++ follow again in a few decades?

C++ is now catching up by introducing modules in C++20, a significant update that brings the language closer to modern programming practices.

An icon indicating this blurb contains a warning

Exercise caution about considering a module as a quirky version of a header. This analogy is as misleading as viewing a reference as a peculiar form of a pointer. We’ve all encountered individuals who have fallen into this trap, leading to significant confusion. Modules and references, while addressing similar needs, do so in distinct, and hopefully more effective, ways.

Working With Larger Programs

Early programs were written using punched cards and tapes, some quite large or long. As a computer operator, I fed many 2500 card trays into the reader, although many cards may have been data. Nicholas Wirth’s original Pascal compiler was a one-pass compiler in a large deck of cards.

As computer systems developed, the programs became more extensive, and using multiple decks was unwieldy. Dropping the deck or experiencing a card jam that destroyed cards was devasatting. Storing programs on tapes or disks significantly improved code management.

Monolithic programs were challenging to work with. Developers broke code into mainline, functions, procedures, and subroutines to create manageable and reusable portions. It was natural to split the humongous mess into more distinct blocks for more effective use, but doing this was a problem.

Inclusion and Separate Compilation

An early approach broke the program source code into logical divisions, including a section where needed by other code. Many languages adopted this technique, including C with headers as an inclusion mechanism.

Figure 1.1. History of Inclusion and Separate Compilation
Language Era Separate Compilation? Inclusion Mechanism
C 1972–present (.c files are separate units) #include for headers
FORTRAN 1957–present (Fortran 90+ MODULEs) (Pre-90 COMMON blocks) INCLUDE for old-style inclusion
Pascal 1970–1990s (UNITs enable modular compilation) USES clause for inclusion
Ada 1980–present (PACKAGEs have separate compilation) None needed
COBOL 1959–present (mostly monolithic) COPY for inclusion
PL/I 1964–1980s (mostly monolithic) INCLUDE for inclusion
ALGOL 1958–1980s (typically whole-program compilation) Some implementations supported separate compilation

Separate compilation occurred in a similar timeframe, with some implementations of Algol 60 using it. It wasn’t until the mid-60s that it appeared in many other languages. C used it from its origin in the early ’70s.

Encapsulation

The next problem was that programmers would basically work around restrictions. (Who would have guessed?) Instead of using functions provided to access data, they worked directly with the data, often leading to inconsistencies that introduced bugs.

Encapsulation to hide details was introduced to minimize this problem. Modularity was used to implement encapsulation. Modula-2 (1978) was the first module system, with Ada following in a few years. The C language never adopted a module system.

As the object-oriented development paradigm swept in, bringing along C++, encapsulation strengthened since a class can prevent external changes.

Comparing Modules and Headers

Headers are an incomplete solution, and even C++ classes are insufficient. Implementation details leak into headers, and large headers used in multiple translation units are processed for each translation unit, increasing compilation time with no real gain.

Header organization can be a challenge when circular dependencies occur. Guards in headers are required to prevent multiple inclusions, which lengthen build times and cause possible inconsistencies. The information in the header becomes part of the global scope/namespace, which can lead to conflicts across libraries from different sources.

As this table shows, modules are intended to address the flaws of headers. Most expect this to occur, and experience in other languages supports this expectation.

Figure 1.2. Comparison of Headers and Modules
Feature Header Files Modules
Compilation Speed Slow, due to repeated parsing Fast, as modules are compiled once
Encapsulation Weak, everything is exposed Strong, only exported symbols are visible
Dependency Management Hard, prone to cyclic issues Easy, avoids unnecessary dependencies
Global Namespace Pollution Yes, everything leaks No, only explicitly exported items
Security Risky, macros and internals leak Safe, controlled visibility