Trying out C++20's modules with Clang and Make
Edit (26/01/2024): As some people have pointed out on Hacker News, the Makefile I show below fails to properly track dependencies in several scenarios. Lexi Winter has a repository on SourceHut showing how to implement correct dependency tracking with BSD Make.
Header files are a relic from the past. No modern programming language has an equivalent concept.
As far as I know, header files came to be because when C was built disk space was at a prime and storing libraries with their full source code would be expensive. Header files included just the necessary to be able to call into the library while being considerably smaller.
C++20’s modules are C++’s attempt to do away with header files and implement traditional modules like most programming languages. Both MSVC and GCC implement modules, at least partially, but I usually reach for Clang so let’s give it a try.
Here’s some annotated source code for a module.
// mod1.ccm - A module. Notice the extension.
// Needed because we're importing headers in this module.
module;
#include <iostream>
// Export the module with a name that matches the filename.
export module mod1;
// Modules are orthogonal to namespaces.
namespace demo {
// We're exporting this class with the 'export' keyword.
export class Printer {
public:
void Print() { std::cout << "Hi from the module\n"; }
};
}
And here’s a regular source file that calls the module.
// main.cc - A regular C++ file
// And here we're importing the module we defined above.
import mod1;
auto main() -> int {
demo::Printer printer;
printer.Print();
return 0;
}
Finally, a Makefile
to pull both files together.
CXX := clang++
CXXFLAGS := -std=c++20 -fprebuilt-module-path=.
MODS := mod1.ccm
SRCS := main.cc
OBJS := $(MODS:.ccm=.o) $(SRCS:.cc=.o)
exe: $(OBJS)
$(CXX) -o $@ $^
%.o: %.ccm
$(CXX) $(CXXFLAGS) -fmodule-output -c $< -o $@
%.o: %.cc
$(CXX) $(CXXFLAGS) -c $< -o $@
And now let’s compile and run.
$ make
clang++ -std=c++20 -fprebuilt-module-path=. -fmodule-output -c mod1.ccm -o mod1.o
clang++ -std=c++20 -fprebuilt-module-path=. -c main.cc -o main.o
clang++ -o exe mod1.o main.o
$ ./exe
Hi from the module
We passed a couple of unfamiliar options to Clang.
The -fmodule-output
option makes it generate a “Built Module Interface” as part of the compilation. This is the *.pcm
file you now see in the build directory.
The -fprebuilt-module-path=.
option passes the path to the directory that holds the *.pcm
files.
Not having to duplicate a bunch of code between header and implementation files is certainly a productivity boost. And not having to deal with #pragma once
and include guards is very nice.
The compilation time for modules seems to be worse that regular translation units. This is to be expected as it’s now generating two files. I do wonder why there’s a need to generate .pcm
files. Couldn’t their content be added to the object file in a backwards compatible manner? Still, I wouldn’t be surprised that this improves significantly as this process gets optimized.
What I think is a bit weird is that we now have modules and namespaces. I sort of expected that we wouldn’t need namespace anymore but it’s not the case. Modules really only handle the “import” part and not the “name disambiguation” part. It would be better if it worked more like Python’s modules.
Another weird part is that the standard library is currently lacking modules so you need to keep importing headers. MSVC has support for doing import std.core
but it’s marked as experimental and doesn’t seem to work on Clang. General support for this is bound to come later.
Overall, despite these weird bits, C++’s modules are certainly an improvement.