A Tour of C++ Modules in Visual Studio

Will Buik

C++ module support has arrived in Visual Studio! Grab the latest Visual Studio Preview if you want to try it out. C++ modules can help you compartmentalize your code, speed up build times, and they work seamlessly, side-by-side with your existing code.

This preview only supports C++ modules in the IDE for MSBuild projects. While the MSVC toolset is supported by any build system, Visual Studio’s IDE support for CMake doesn’t support C++ modules yet. We will let you know once it is! As always, please try it out and let us know if you have any feedback.

Module Basics

C++ modules allow you to closely control what is made available to the translation units that consume them. Unlike headers, they won’t leak macro definitions or private implementation details (no ridiculous prefixes needed). Also, unlike headers, they are built once and then can be consumed many times across your projects, reducing build overhead.

C++20 introduces new keywords to define and consume modules and Visual Studio uses a new file type “.ixx” to define a module’s interface. Read on for the details.

Getting Started with Modules in Visual Studio

If you created a brand-new project in the latest preview, you don’t need to do anything. However, before you can add or consume modules in existing projects you need to make sure you are using the latest C++ Language Standard.

To do this, set the C++ Language Standard to “Preview /std:c++latest”. If you have multiple projects in your solution, remember to do this for all of them.

General > C++ Language Standard, set to “Preview /std:c++latest”

And that’s it! You are ready to use C++ modules with Visual Studio.

Creating Modules

To add a module to a project you will need to create a module interface. These are normal C++ source files with the extension “.ixx”. They can include headers, import other modules, and will include the exported definitions of your module. You can add as many of these to a project as you want.

Add New Item > “C++ Module Interface Unit (.ixx)”

Here’s how this then looks in the Solution Explorer. In this example, the fib and printer projects both define C++ modules.

Solution Explorer with three module interfaces

Note: While this example shows all module interfaces in “.ixx” files, any C++ source file can be treated as a module interface. To do this, set the “Compile As” property on a source file to “Compile As Module”. The “Compile As” property can be found on the “Advanced” tab on any source file’s properties page.

Exporting Modules

So, what actually goes into a module interface? The example below defines a simple module called DefaultPrinter and exports a single struct:

module; //begins global module fragment

#include <iostream>

export module DefaultPrinter;

export struct DefaultPrinter
{
    void print_element(int e)
    {
        std::cout << e << " ";
    }

    void print_separator()
    {
        std::cout << ", ";
    }

    void print_eol()
    {
        std::cout << '\n';
    }
};

To break the example down a bit, you can see the new export syntax on lines 1, 5, and 7. Line 1 specifies that this is a module interface. Line 5 defines and exports the module itself and line 7 exports a struct. Each module can export many items, such structs, classes, functions, and templates.

Module interfaces can include headers and import other modules. When they are imported, they will not leak any details from these included headers or modules unless you explicitly import them. This isolation can help avoid naming collisions and leaking implementation details. You can safely define macros and use namespaces in module interfaces too. They will not leak like traditional headers.

To #include headers in a module interface, ensure you put them in the global module fragment between module; and export module mymodule;.

This example puts the implementation in the module’s interface, but that is optional. If you look back at the solution explorer before you can see the fibgen.ixx interface has a corresponding implementation in fibgen.cpp.

Its interface looks like this:

export module FibGenerator;
export fib gen_fib(int start, int &len);

With a corresponding implementation:

module FibGenerator;

fib gen_fib(int start, int &len)
{
	//...
}

Here, the interface defines the module name and exports gen_fib. The corresponding implementation uses the module keyword to define which module the implementation belongs to so everything can be combined into a cohesive unit automatically at build time.

Consuming Modules

To consume modules, use the new import keyword.

module;
#include <ranges>
#include <concepts>

import DefaultPrinter;

struct DefaultFormatter
{
    template<is_series S, is_printer T>
    void format(T t, S s)
    {
        while (!s.done())
        {
            t.print_element(s.next());
            t.print_separator();
        }
        t.print_eol();
    }
};

All exported items from the module interface will be available for use. This example makes use of the DefaultPrinter module in the first example, importing it on line 5.

Your code can consume modules in the same project or any referenced ones automatically (using project-to-project references to static library projects).

Consuming Modules from Other Modules

You can also import modules from another module interface. Here is an example that expands on the DefaultPrinter module above:

module;
#include <iostream>
import DefaultPrinter;

export module TabbedPrinter;

export struct TabbedPrinter : DefaultPrinter
{
    void print_separator()
    {
        std::cout << "\t";
    }
};

This example imports the DefaultPrinter module above and overrides its print_separator function. Other code can now import this TabbedPrinter without needing to worry about the details of DefaultPrinter. Visual Studio will make sure everything is built in the right order.

External Modules

It is also possible to reference modules that exist on disk, instead of ones belonging to another project in the solution. Care needs to be taken here, however, because modules are compiled, binary files. You must make sure they are compatible with the way you are building your projects.

You can tell Visual Studio to look for modules on disk by editing the Additional Module Dependencies property:

C/C++ > General > Additional Module Dependencies, you can add modules with “/reference[[module_name]=]path”, multiple modules can be separated by semicolons

IntelliSense and Modules

All the IntelliSense features you know and love also work with modules. Features like code completion, parameter help, Find All References, Go To Definition and Declaration, rename, and more all work across solutions the way you would expect when you use modules.

Here you can see Find All References and Peek Definition working with our TabbedPrinter module above. For instance, it can show all references of the DefaultPrinter structure exported from the DefaultPrinter module and display its definition:

Find All References Find All References of the “DefaultPrinter” structure across the solution

Peek Definition Peek Definition of the “DefaultPrinter” structure

You can also Go To or Peek the Definition of a module itself from anywhere that imports it:

Peek Definition of an imported module, "DefaultPrinter"

See Modules in Action

To see all of this in action, check out our modules demo from CppCon 2020. There are many other demos of the latest Visual Studio and C++20 features in action too if you are interested.

Header Units

A header unit is a standard C++ incantation to invoke the generation of metadata (IFC files) – for well-behaved header files, in particular standard library headers – similar to those generated for modules with the goal of speeding up overall build time, if done judiciously. However, unlike Modules, header units do not really provide isolation the way Modules do: macro definitions and other preprocessor states are still leaked to the consumers of the header units. You use a header unit via the import "header.h"; or import <header>; syntax. In Visual Studio, the metadata for header units are automatically generated by the build system. All items declared and reasonable definitions in the header file (and its includes) are made available to the consumer, as would an #include file. Like in the case of module consumption, macro definitions and other preprocessor states active in the code that imports a header unit will not influence the imported header unit in any way. However, unlike a module, any macro definition will be available for use in your code when you import a header unit. Header units are primarily a transition mechanism, not substitute for modules. If you have a chance to consider a named module vs. a header unit, we encourage you to invest the effort in designing proper modules. We will cover header units in depth in future blogs, especially their use in migrating existing codebases to uses of modules.

Full IDE and toolset support for header units is coming soon. You can track the status of header unit support for the Microsoft STL here on GitHub.

Feedback

If you are interested in trying out C++ modules with your own code, I urge you to grab the latest Visual Studio Preview. Please try it out and let use know if you have any questions or feedback. If you find any issues or have a suggestion, the best way to reach out to us is to Report a Problem.