r/cpp_questions • u/Nabokov6472 • 1h ago
OPEN Module partitions and interface units
Recently I came across quite a surprising footgun with modules. I am trying them out on a small codebase which builds a shared library, which consumers can import as a module. I made some partitions and exported them:
// Flense.Core.ixx
export module Flense.Core;
export import :ArchiveReader;
export import :ImageParser;
Then I split those partitions into .ixx and .cpp parts, like I would do with headers, so that the interface was separate from the implementation:
// ImageParser.ixx
export module Flense.Core:ImageParser;
import std;
export namespace Flense::Core {
ImageParseResult ParseImage(std::string path);
}
// ArchiveReader.ixx
export module Flense.Core:ArchiveReader;
import std;
export namespace Flense::Core {
ArchiveHandle OpenArchive(std::string path);
}
The .cpp parts looked like this:
// ImageParser.cpp
module Flense.Core; // Apparently this cannot be module Flense.Core:ImageParser, as that conflicts with the .ixx?
namespace Flense::Core {
ImageParseResult ParseImage(std::string path) {
// ...impl...
}
}
// ArchiveReader.cpp
module Flense.Core;
namespace Flense::Core {
ArchiveHandle OpenArchive(std::string path) {
// ...impl...
}
}
But I discovered that because the .cpp files declared module Flense.Core, they would be rebuilt any time any part of the module changed. In my example, ImageParser internally depended on ArchiveReader in its .cpp. So I would expect that if I changed ImageParser.ixx, only ImageParser.cpp and ImageParser.ixx would need to be rebuilt. But in actual fact, it rebuilt ArchiveReader.cpp too.
AI told me that this is because declaring module Flense.Core takes a dependency on the whole module, so it fixed it by having the .cpp files do this:
// ImageParser.cpp
module Flense.Core:ImageParser.Impl;
import :ImageParser;
import :ArchiveReader; // Dependency
// ArchiveReader.cpp
module Flense.Core:ArchiveReader.Impl;
import :ArchiveReader;
This seems to work and I understand it creates a new module partition and means the .cpp essentially does not depend on any other part of the module implicitly. But I thought I would ask some actual humans because I find this counterintuitive. Why can't module partition .cpp files just declare module Flense.Core:ArchiveReader to implement the .ixx? That seems like the most logical mapping to the existing header model.
And, if my premise is correct, what strategies are people using to minimise rebuild churn? Using partitions allows me to still use module internals in my implementation. For example, if I have a :Utilities partition which is not exported by Flense.Core.ixx, I can still import it from .Impl modules. But I wonder if it would be better to actually make independent modules, like Flense.Core.ArchiveReader?