11

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:

  • Some said to have a module with the forward declarations. As I said above, this doesn't work.
  • Some said to use proclaimed ownership declarations. However, they were removed from the final C++ standard.
  • Some said to use module partitions. However, this only works if A and B are in the same module. A and B shouldn't be in the same module, so this doesn'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;
sudgy
  • 491
  • 1
  • 3
  • 15
  • "*but this was still declaring and defining B in two different modules*" How do you know that? What errors were you getting? Also, can you show us the code for all of the module cases you tried? Were you using module partitions for these declarations or not? – Nicol Bolas Jun 12 '21 at 19:29
  • I was about to start test conversions from header files to modules, but this is a major show-stopper. Almost all of the things I want to have separated have dependencies that require forward declarations. – Sander Bouwhuis Jan 04 '22 at 21:42

3 Answers3

3

The solution for this depends on why you want to forward declare in the first place.

If you are doing that to break a circular dependency, then usually the solution is to simply put them in the same module. Since the components are so tightly coupled together, it make sense to have them in the same module.

If you're doing that to make compilation faster, you're better to simply import the module and use the type. Importing a module has almost no cost. Compiling the module has, and it's only done one time.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • I'm doing it to make files not be recompiled as much. The problem with importing is that this causes anything that uses A to have to be recompiled when B is changed. Importing modules is basically free, but recompiling files is not. – sudgy Jun 12 '21 at 21:47
  • @sudgy: If you know that nothing in module `A`’s interface depends on the implementation of the class `B`, you could probably skip recompilation of `A`’s clients, although that obviously might be inconvenient with automated build systems and some compilers might deliberately prevent that to detect errors. – Davis Herring Jun 12 '21 at 23:23
0

My guess is that this is related to module linkage. When you forward declare B, that name has module-linkage by default. It is therefore a different B compared to the one defined in your module B (hence the undefined type). If you export the forward declaration, you will end up with external rather than module linkage, which should fix your issue.

In practice, that means changing A.mpp in your 1st attempt to

export module A;

export class B;

export class A {
    public:
        void f(B& b);
};

vector-of-bool wrote an interesting post on the topic if you want to know more.

Touloudou
  • 2,079
  • 1
  • 17
  • 28
  • GCC says otherwise. It still gives the exact same error on this, saying that the reference to `B` is ambiguous. – sudgy Oct 15 '21 at 02:16
  • hmm, I was testing with MSVC 16.11. – Touloudou Oct 15 '21 at 09:55
  • It seems that GCC is correct in this situation. From http://eel.is/c++draft/basic.link#10: "If two declarations of an entity are attached to different modules, the program is ill-formed." And declarations are attached to the module they are in (http://eel.is/c++draft/module#unit-7), causing there to be two declarations of `B` in different modules, regardless of the presence of `export`. – sudgy Oct 17 '21 at 05:10
0

I can't believe there doesn't seem to be any nice workaround. Anyway one desperate solution for example to break a circular dependency is by using a template (this does not answer the question on how to forward declare, just how to avoid the need in one case):

// A_impl.cc

export module A_impl;

export template <typename B> class A_impl {
    public:
        void f(B& b) {}
};
// B.cc

export module B;

import A_impl;

export class B;

typedef A_impl<B> A;

export class B {
    public:
        void f(A& a) {}
};
// A.cc

export module A;

export import A_impl;
import B;

export typedef A_impl<B> A;
// main.cc

import A;
import B;

int main(void) {
    A a;
    B b;

    a.f(b);
    b.f(a);

    return 0;
}

At the moment clang doesn't support module partitions so with that toolchain this seems to be the only way to define A and B in different files (without #include) while placing them in modules.

jjrv
  • 4,211
  • 2
  • 40
  • 54