I have recently been trying to convert a codebase to C++20 modules using GCC 11. However, I got stuck on the following situation. First, here is how it was done using headers:
A.h
class B;
class A {
public:
void f(B& b);
};
A.cpp
#include "A.h"
#include "B.h"
void A::f(B& b)
{
// do stuff with b
}
(the contents of B.h is not important here)
The thing to note is the forward declaration of B. Not everybody using A should care about B, so I used a forward declaration to stop recompiling from happening. With headers, this situation works perfectly.
The issue is when trying to convert this code to modules. The main issue is that entities are tied to the module they are declared in, so forward declaring in A.h is impossible. I tried forward declaring in the global module, but then the compiler still complained that the definition of B was in a different module than its declaration. I also tried having a third module that just contained the forward declaration of B, but this was still declaring and defining B in two different modules. So, my main question is: how can I forward declare something from a module outside of it? I would also be satisfied with a way that ends up producing the same effect: that users of A don't need to be recompiled when B changes.
While searching, I found a few places talking about similar situations, but they all didn't work for some reason. The reasons they didn't work:
Edit: in response to a comment, here are the details for a few of the things I have tried:
Attempt 1: Forward declare B in A.mpp
A.mpp
export module A;
class B;
export class A {
public:
void f(B& b);
};
B.mpp
export module B;
export class B {};
A.cpp
module A;
import B;
void A::f(B& b)
{
// do stuff with b
}
When doing this, gcc errors with
A.cpp:4:11: error: reference to ‘B’ is ambiguous
4 | void A::f(B& b)
| ^
In module B, imported at A.cpp:2:
B.mpp:3:14: note: candidates are: ‘class B@B’
3 | export class B {};
| ^
In module A, imported at A.cpp:1:
A.mpp:3:7: note: ‘class B@A’
3 | class B;
Attempt 2: Forward declare in new module
B_decl.mpp
export module B_decl;
export class B;
A.mpp
export module A;
import B_decl;
export class A {
public:
void f(B& b);
};
B.mpp
export module B;
import B_decl;
class B {};
A.mpp
module A;
import B;
void A::f(B& b)
{
// do stuff with b
}
When doing this, gcc errors with
B.mpp:5:14: error: cannot declare ‘class B@B_decl’ in a different module
5 | class B {};
| ^
In module B_decl, imported at B.mpp:3:
B_decl.mpp:3:14: note: declared here
3 | export class B;
Attempt 3: Forward declare in header, define in module
B_decl.h
class B;
A.mpp
module;
#include "B_decl.h"
export module A;
export class A {
public:
void f(B& b);
};
B.mpp
module;
#include "B_decl.h"
export module B;
class B {};
A.cpp
module A;
import B;
void A::f(B& b)
{
// do stuff with b
}
When doing this, gcc errors with
B.mpp:7:7: error: cannot declare ‘class B’ in a different module
7 | class B {};
| ^
In file included from B.mpp:3:
B_decl.h:1:7: note: declared here
1 | class B;
The rule that is preventing the use of forward declarations here has to do with module ownership of declarations. Every declaration is attached to some module (either a named module or the global module). It is forbidden for the declarations to be attached to multiple modules. Almost all declarations, even forward declarations, attach to the module that contains the declaration. To my knowledge, there is no way to (forward) declare something attached to a named module in another named module. However, you can forward declare entities attached to the global module, by using an extern "C++" language linkage specification. That particular rule is specified in [module.unit] in the C++23 standard.
Even if an entity is declared attached to the global module, it may still be defined and exported in a named module. As far as I can tell, an extern "C++" entity is not really any different from a normal declaration in a named module, other than the ability for other modules (or even non-module code) to forward declare it (also with the extern "C++" specification). You'll still need to import the module to get the definition of the entity.
The following code should work:
B.ixx
export module B;
export extern "C++" class B {
// Class Definition
};
A.ixx
export module A;
extern "C++" class B;
class A {
public:
void f(B& b);
};
A.cpp
module A;
import B;
void A::f(B& b) {
// Function Definition
}
This should prevent cascading recompilation of all of A's users when the interface of B is changed. To be clear, there is still a dependency of module A on module B, so this technique alone can't break circular dependencies.
Here's a godbolt link demonstrating that the above code works. It works on clang 21.1.0 and gcc 15.2 (and likely earlier, I haven't checked).