This won’t be a long post, I promise. I decided to post more often, and I realized my posts require exponentially long time to write, correct, and more importantly convince myself they’re good enough and won’t contain too many mistakes (and inevitably I end up finding another couple of mistakes just after hitting “Publish”).

Ok, let’s go to the point.

I have this small game I’m developing with my kids, a rogue-like game, and the build command ends in something like this:

[driver] Build completed: 00:00:03.358

nothing fancy, and probably completely irrelevant for this discussion so, why am I even mentioning it?

I don’t know, I’m sorry for wasting your time. Let’s go back to the point.

Well, I just felt like I wanted to experiment a bit, and decided to “modularize” the code, and now, full of joy, I want to describe my experience.

The process of learning the feature: took me a day of fun guesses, looks like nobody wanted to try the features, just a few toy examples – and my code isn’t much more than that!

I tried reading articles that claim to explain modules. They don’t. I won’t name names, and I didn’t find any notable name anyway. In retrospect, I should have looked for those specific names to begin with (I’m sure they wrote very good articles, and they didn’t appear in my searches because my search engine hates everybody, and me in particular).

The most notable thing is that each of this article do is talking about partitions like they’re taught in elementary school, showing an half-assed example with a single-file module, and then avoid mentioning partitions ever again.

Let’s figure out what they are.

Eventually I did, and it’s obvious from the name: a partition is a part of the module. Can’t a compiler figure out that, if I declare files A.cppm and B.cppm to be the same module, they’re actually two parts of the same module?

Clearly not.

But maybe there’re some hidden advantages, like I can have circular dependencies between different partitions, right?

No biscuit.

I can keep my plain old forward-declarations. In my mind, I expected modules to solve this by doing some smart multi-pass parsing, or resorting to black magic.

Nope, forward declaration it is.

Ok, so, since all my partitions are parts of the same module, they’re automatically added to the module, right?

Not really. You need to import all the partitions, and export them in the main module export. Why?

Because. Shut up and write that file.

Good! This compiles under MSVC solution, so it’s straightforward to make the code compiles everywhere else, with some portable build system, right?

No. Because I wanted to use CMake.

CMake had once heard the word documentation, but hated the sound of that word. So there’s literally no example on the website, and most of the pages that mention modules (barely) are outdated (but they claim to refer to the latest CMake version, of course).

Ok, I interpolated random CMake code found on GitHub, and came up with a solution that seems to work.

My code now uses modules and import std. Of course, I’m oversimplifying the problem, because CMake has (or had) a bunch of (literally) magic incantations to enable modules that vary from version to version (on purpose, I’m told). Anyway, once I finally settled for CMake 4, everything became relatively simple and straightforward.

And of course failed on clang.

Why? First because whatever CMake code works on MSVC doesn’t automatically work on clang (in the latter, you need to explicitly say which files are module files and which are normal translation units, while in MSVC that is somehow magically deduced).

Once you got that, it will still fail because “Import of module ‘std’ imported non C++20 importable modules”, whatever that means.

Rumor has it that you can use “import std” with libc++, and in order to do it “properly” in CMake (i.e. without shoving a bunch of clang-specific command line option in your otherwise portable CMakeLists.txt) you should use CMake toolchains feature. I found an entire mine of them in a 6 years old collection of toolchains, which of course predates modules, and need to be adapted to my specific use case. I gave up on clang in this configuration because, honestly, I had better things to do.

Like compiling GCC!

Well, cppreference.com says Standard Library Modules feature is available from 15* where the * apparently means “or maybe not”: the feature might be there, but the only traces I’ve found of it working in CMake (in a CMake merge request, of course, not in actual documentation!) are strictly connected to Fedora 22.

No luck for my distro. The only option is to recompile the compiler, and probably libstdc++. That’s probably my fault for choosing such a niche distro: it’s called Ubuntu 24.04 LSB, but you probably never heard of it.

No, I won’t compile an entire compiler and its standard library just to get a toy example to compile on Linux.

Let’s drop the “import std” feature entirely.

<TODO: insert a description of my long dive into all compiler-specific modes to import the standard library (or some parts of it?), which are all looking like good ideas, but fail to be compatible with each other, and sometimes with themselves> (jk, I’m not gonna do that either)

At the end of the day (almost two days, at this point), to keep the code compatible, I ended up using includes for the standard library (back to C++20 modules, as a matter of fact).

Ok, so here’s the final solution (the code is completely made up in the editor in my blog, so any mistake you can find is really due to the fact I never tested the code, and just copied part of my actually-working code and replaced it with fictious names and added funny comments):

cmake_minimum_required(VERSION 4.1.1)

project(MyProject VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)

add_executable(MyProgram)
target_sources(MyProgram
  PUBLIC 
    "main.cpp"
  PUBLIC
    FILE_SET MyModules TYPE CXX_MODULES FILES
    "Partition1.cppm"
    "Partition2.cppm"
    "MyModule.cppm"
)

And then, for the two partitions you’ll have something like this:

module; // this incantation tells the compiler you are a module
        // but you aren't ready to tell which module you are
        // so you can include stuff without being shouted at
#include <string>
#include <tuple>

export module MyProgram:Partition1; // and now you're a real module!

// don't even thing of exporting both the namespace and the class!
namespace my_namespace { 
    export struct MyRootObject {
        virtual setName(std::string n) = 0;
        virtual ~Object() = default;
    };
}

and

module;
#include <string_view>

export module MyProgram:Partition2;

import :Partition1;

using namespace std;

namespace my_namespace {
    export struct MyClass : public MyRootObject {
        virtual string_view name() const = 0;
    };
}

Small digression about the “export”: of course, it can’t be all that easy. In MSVC I can export both the namespace and the class in it, but on clang that’s a big no-no. You either export the namespace (and everything within, I assume) or you choose which element to export one by one. Which is fine, I guess, but it’s another of these quirks I incurred during my weekend-fun with modules.

And finally, the most important file of the entire module:

export module MyModule; // this is the real thing

export import :Partition1; // seriously?
export import :Partition2;

All this make sense, right?

Then you have your main and, you guessed, it starts with something like this:

#include <tuple>
#include <iostream>

import MyModule;

// ... more stuff here

We did it!

Ok, and now we can finally get what we were promised on paper, the fastest compilation times ever!

[driver] Build completed: 00:00:08.223

Ok, maybe it’s just the first build, all the other will be faster: let’s just touch one module partition file and hit compile again.

[driver] Build completed: 00:00:07.603

Ok, but that’s just MS. The others will surely be better, right?

Compiler versionincludes (s)modules (s)
MSVC 19.44.35215.03.3588.223
GCC 14.2.02.16813.165
Clang 22.0.01.99811.429

Ok, it’s past midnight, I’ll just click on “Publish” and hit the bed.

And cry.

2 Comments

  1. Cris

    If you want to split the code, you can also use:
    “`cpp
    // Something.cppm
    export module something; // Like a header file “.hpp”
    export struct Something { /* code … */ }

    // Something.impl.cpp
    module something:impl; // Like a source file “.cpp”
    // code …

    // And you can use also “*.tpp” files
    // At the end of the “*.cppm” file, add:
    #include “the_file_name.tpp”

    “`

    Reply

  2. This is what happens also (at least partially) because “standardization” over the years changed meaning.

    Long ago the meaning was “find what works out there in the field and that everyone likes and uses, and then standardize to remove little irrelevant differences”.

    Now it means “we’re the smart ones, let’s us design this new thing on the whiteboard (and, eventually, may be, with a partial, toy, special case demo) and declare it to be the new standard despite no existing real working implementation used in the field that people is happy about, just because we say so”.

    What could go wrong? You found out.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.