Since C++ compilers are starting to support import std, I ran a few experiments to see what the status of that is. GCC 15 on latest Ubuntu was used for all of the following.
The goal
One of the main goals of a working module implementation is to be able to support the following workflow:
- Suppose we have an executable E
- It uses a library L
- L and E are made by different people and have different Git repositories and all that
- We want to take the unaltered source code of L, put it inside E and build the whole thing (in Meson parlance this is known as subproject)
- Build files do not need to be edited, i.e. the source of L is immutable
- Make the build as fast as reasonably possible
The simple start
We'll start with a helloworld example.
This requires two two compiler invocations.
g++-15 -std=c++26 -c -fmodules -fmodule-only -fsearch-include-path bits/std.cc
g++-15 -std=c++26 -fmodules standalone.cpp -o standalone
The first invocation compiles the std module and the second one uses it. There is already some wonkiness here. For example the documentation for -fmodule-only says that it only produces the module output, not an object file. However it also tries to link the result into an executable so you have to give it the -c argument to tell it to only create the object file, which the other flag then tells it not to create.
Building the std module takes 3 seconds and the program itself takes 0.65 seconds. Compiling without modules takes about a second, but only 0.2 seconds if you use iostream instead of println.
The module file itself goes to a directory called gcm.cache in the current working dir:
All in all this is fairly painless so far.
So ... ship it?
Not so fast. Let's see what happens if you build the module with a different standards version than the consuming executable.
It detects the mismatch and errors out. Which is good, but also raises questions. For example what happens if you build the module without definitions but the consuming app with -DNDEBUG. In my testing it worked, but is it just a case of getting lucky with the UB slot machine? I don't know. What should happen? I don't know that either. Unfortunately there is an even bigger issue lurking about.
Clash of File Name Clans
If you are compiling with Ninja (and you should) all compiler invocations are made from the same directory (the build tree root). GCC also does not seem to provide a compiler flag to change the location of the gcm.cache directory (or at least it does not seem to be in the docs). Thus if you have two targets that both use import std, their compiled modules get the same output file name. They would clobber each other, so Ninja will refuse to build them (Make probably ignores this, so the files end up clobbering each other and, if you are very lucky, only causes a build error).
Assuming that you can detect this and deduplicate building the std module, the end result still has a major limitation. You can only ever have one standard library module across all of your build targets. Personally I would be all for forcing this over the entire build tree, but sadly it is a limitation that can't really be imposed on existing projects. Sadly I know this from experience. People are doing weird things out there and they want to keep on weirding on. Sometimes even for valid technical reasons.
Even if this issue was fixed, it does not really help. As you could probably tell, this clashing will happen for all modules. So if your ever has two modules called utils, no matter where they are or who wrote them, they will both try to write gcm.cache/utils.gcm and either fail to build, fail on import or invoke UB.
Having the build system work around this by changing the working directory to implicitly make the cache directory go elsewhere (and repoint all paths at the same time) is not an option. All process invocations must be doable from the top level directory. This is the hill I will die on if I must!
Instead what is needed is something like the target private directory proposal I made ages ago. With that you'd end up with command line arguments roughly like this:
g++-15 <other args> --target-private-dir=path/to/foo.priv --project-private-dir=toplevel.priv
The build system would guarantee that all compilations for a single target (library, exe, etc) have the same target private directory and all compilations in the build tree get the same top level private directory. This allows the compiler to do some build optimizations behind the scenes. For example if it needs to build a std module, it could copy it in the top level private directory and other targets could copy it from there instead of building it from scratch (assuming it is compatible and all that).
No comments:
Post a Comment