Skip to content

Understanding MSVC C++ Modules: Principles, Motivation, and Engineering Practice

A quick pointer: if you don't already know how to use modules on MSVC, I would seriously recommend that you try them first before drawing any conclusions.


Why Do We Need Modules? — Starting with the Fundamental Flaws of #include

For a very long time, C++ only really had one "module system":

cpp
#include <vector>
#include "foo.h"

I believe everyone knows how #include works, so I won't spell it out—it's purely text replacement. This dependency mechanism based on #include sometimes feels more like something that was discovered rather than deliberately designed (given the history of the C language).

When the compiler sees #include <vector>, it doesn't think you are "depending on a library". Instead, it takes the contents of the <vector> header file, copies them verbatim into the current .cpp, and continues compiling.

This might sound harmless, but I believe anyone doing real engineering has experienced these issues:

Problem 1: Compilation Speed Disaster (Exponential Amplification)

The core issue with the header file mechanism is repeated parsing. Every .cpp file needs to re-parse all the headers it #include, such as <vector>, <string>, and <iostream>. When dealing with templates, macros, and conditional compilation, this repeated work becomes a performance nightmare, causing compilation time to grow exponentially.

Precompiled headers (PCH) merely cache the parsing results; they do not fundamentally address the structural flaw of repeated parsing. Essentially, this is because the compiler doesn't know which declarations are "already-processed module interfaces", so it blindly processes them over and over again.

Problem 2: Uncontrollable Macro Pollution

Macros are unscoped, which is the root cause of uncontrollable macro pollution. Once a macro like #define min(a,b) ... is defined and introduced via #include, it permanently pollutes all subsequent code until the end of the file or until it is hit by #undef. (This is why you'll see some projects habitually #undef their defined macros—you don't want a macro you defined to blow up because someone else messed up the include order, right! For example, including a library like <windows.h> might introduce a massive number of macros that could accidentally replace functions or variables with the same names in your code. The compiler cannot prevent or isolate this global macro pollution.

Problem 3: Tight Coupling of Interface and Implementation (Transitive Dependencies)

The header file mechanism forces the exposure of unnecessary implementation details in the interface (.h files). For example, even if a class Foo only uses std::vector<int> internally:

c++
// foo.h
#include <vector> // <-- 不必要的暴露

class Foo {
    std::vector<int> data;
};

You simply want to use the Foo class, but you are forced to bring in all of <vector>'s dependencies via #include "foo.h". This is known as transitive includes: users are forced to depend on all the headers required by the underlying implementation details of the interface, leading to a network-like explosion of compilation dependencies.

Problem 4: Too Many Implicit Rules: ODR, ABI, and More

The header file mechanism brings a series of complex and implicit rules, such as inline, template definitions, static variables, and implementing functions inside headers. The most dangerous of these is the one definition rule (ODR). ODR violations often pass the compilation stage (because each translation unit only sees one definition), but they only surface during the linking stage, resulting in hard-to-debug "linker errors" that greatly increase code fragility.


The Core Idea of C++ Modules: Making the Compiler Truly "Understand Modules"

So, being the smart developer you are, you know that since these problems exist, modules are here to solve them! (Although I must complain that using modules in my current project feels like a mixed bag, so I'm still experimenting). Simply put: Modules = compiler-understandable, cacheable, and isolatable interface units

The import Keyword ≠ #include

import std; simply imports the current standard library modules into our code. It tells our MSVC compiler: "Please import the compiled interface information of the std module into the current translation unit."

The Smallest Unit of a Module: BMIs (Binary Module Interface)

In MSVC, each module interface unit is compiled into a .ifc file. This is an intermediate artifact of the module, designed to easily integrate into existing build systems. It stores the serialized results of the frontend AST—structured descriptions of types, functions, and templates (honestly, my first reaction was "a C++ version of a .class file (Java)").

Workflow Differences

Previously, header file processing relied on the preprocessor, directly pasting headers into source files to form a single compilation unit. Now, modules handle this much better: the module is compiled only once, and when you use it, the .ifc file is loaded directly, significantly cutting down compilation time. Design characteristics of MSVC Modules (very practical)

What Exactly Happens with import std;?

When you write import std;, MSVC will:

  1. Look up the standard library module std

  2. Load its .ifc file (pre-compiled officially by the STL)

  3. Inject all exported symbols into the current TU

  4. Not introduce any macros (this is extremely important). This is also why the min/max macro issue naturally disappears in the world of Modules.

    Note that modules do not export macros by default. Macros do not propagate across import boundaries, so the macros you write cannot leak into dependent files.


When Should We Use MSVC Modules Today?

As mentioned above, C++ Modules is a structural solution to the traditional header file mechanism. However, when applying it to production environments—especially under MSVC (Visual Studio)—we need to use it strategically.

1. Using import std; to Replace Standard Library Headers

This is currently the safest and most valuable use case for Modules. We have now thoroughly solved the compilation speed disaster and macro pollution issues caused by standard library headers (like <vector>, <string>, <iostream>).

Moreover, with just one import std;, we no longer need to painstakingly write a bunch of includes. The compiler only needs to process the pre-compiled Standard Library Module interface once, greatly boosting compilation speed. Internal macros from the standard library also won't pollute your code.

2. Modularization Within New Projects (Business Module Isolation)

For newly created projects primarily targeting the Windows platform or for internal use, consider dividing the internal business logic into independent Modules. User code only needs to import MyModule;, without being forced to #include all the headers depended upon internally by the module. In terms of syntax, business logic is organized into .ixx or .cppm module interface files, and export only exposes the necessary interfaces. The interface and implementation are thoroughly decoupled. When changing internal implementation details and private dependencies of a module, user code depending on that module does not need to be recompiled (unless the interface itself changes).

Use With Caution

1. Public Interfaces of Large Cross-Platform Libraries

If what we are doing is: developing a public/open-source library that needs to be stably used by multiple compilers (such as MSVC, GCC, and Clang), please be cautious about using Modules for its public API. After all, this feature hasn't been around for many years, and the Modules implementations across mainstream compilers still have differences and potential bugs. As a library ready for distribution, it seems it would still bring additional configuration complexity for the library's users.

2. Projects Requiring Completely Consistent Behavior Across GCC / Clang

If your project needs to achieve completely consistent and highly stable behavior across different platforms and compilers (such as embedded systems, high-integrity financial applications), potential implementation differences in Modules could introduce risks. After all, the semantics of Modules (especially in complex scenarios involving import order, linking, and ODR) might have subtle differences across compilers.

On this matter, conservatively relying on traditional header files is currently the best way to guarantee cross-platform behavioral consistency, because it relies on the #include preprocessing semantics that have been mature for decades.

ScenarioRecommendation LevelReason / Value
Using import std;✅ Strongly RecommendedSolves standard library compilation speed and macro pollution issues; high value, extremely low risk.
New projects / internal business modularization✅ RecommendedEliminates transitive dependencies, decouples interface from implementation, improves internal compilation efficiency.
Public / cross-platform library APIs⚠️ Use With CautionCross-compiler implementation differences and toolchain maturity issues may affect compatibility.
Extremely strict behavioral consistency requirements⚠️ Use With CautionAvoids unpredictable behavior caused by potential compiler implementation differences.

Built with VitePress