11

I have been experimenting with modules implementation as provided by the MSVC lately and I've run into an interesting scenario. I have two classes that have a mutual dependency in their interfaces, which means that I'll have to use forward declarations to get it to compile. The following code shows an example:

Module interface

export module FooBar;

export namespace FooBar {
    class Bar;

    class Foo {
    public:
        Bar createBar();
    };

    class Bar {
    public:
        Foo createFoo();
    };
}

Module implementation

module FooBar;

namespace FooBar {
    Bar Foo::createBar() {
        return Bar();
    }

    Foo Bar::createFoo() {
        return Foo();
    }
}

Now I would like to split these two classes up into their own modules named Foo and Bar. However, each module needs to import the other as their interfaces depend on each other. And according to the modules proposal as it currently stands, circular interface imports are not allowed. This article suggests to use the proclaimed ownership declaration, but it seems that this is not yet implemented in the MSVC implementation of modules.

Therefore, am I correct in assuming that this situation currently cannot be resolved using the current implementation as provided by the MSVC? Or is there some alternative I am missing? In this scenario the situation is pretty trivial, however I encountered this problem while modularizing a library which has many classes that have such dependencies. I realize circular dependencies are often an indication of a bad design, however in certain instances they are unavoidable or difficult to refactor away.

1

2 Answers 2

4

You can create a third module which exports only forward declarations to each of your classes (could be many classes).
Then you import this module into both (or all) of your modules, where it provides the forward declarations needed to implement each module.

Unfortunately, MSVC has still (today is version 16.7) issues with modules; although this approach works, you get often completely wild error messages; for example, "cannot convert MyClass* to MyClass* - no conversion provided (this example happens when you directly add forward decalarations to the same class into several modules; the compiler considers them different animals).
Another issue is if you forget to import all the modules needed, the error message is either grossly misleading ('no such method in that class'), or the compiler aborts with an internal error.

Don't expect too much until they have completed their work.

5
  • Yeah I have been experimenting and managed to get a small sample running using module partitions for which support has recently been added to MSVC and GCC. Modules have come quite far since I asked the question but I still regularly run into internal compiler errors when using them so I'm waiting for the implementations to become more stable.
    – Qub1
    Commented Sep 9, 2020 at 10:53
  • 1
    I have encountered the "cannot convert MyClass* to MyClass*" case when attempting to use non-module header files in .cpp translation units with forward declarations of the foreign types in the .ixx file. My best solution, so far, is to create a thin header with the forward declarations in it and then #include that in the global module section, instead of putting the forward declarations in the .ixx file, itself, where they would only be allowed after the export module ... line. This feels clunky. Better would be allowing forward decls. in the global module section.
    – Xharlie
    Commented Aug 20, 2021 at 11:49
  • 1
    This approach doesn't work with the standardized modules (and perhaps not with MSVC's): you can't declare something in one module and define it in another. You can do so with different module partitions, though. Commented Feb 3, 2022 at 8:36
  • 1
    @Xharlie "instead of putting the forward declarations in the .ixx file, itself, where they would only be allowed after the export module ..." Don't know how it was in 2020, but in 2023 you can put forward declarations before export module ... and it actually fixes this issue with forward declarations.
    – KulaGGin
    Commented Nov 23, 2023 at 17:33
  • @KulaGGin This gives "warning C5201: a module declaration can appear only at the start of a translation unit unless a global module fragment is used" though. The "cannot convert MyClass* to MyClass*" bug is still a thing with forward declarations and modules in MSVC as of today (v17.9.5)...
    – Vinz
    Commented May 9 at 23:33
0

I just found 2 solutions to this problem.

You don't need a mutual dependency for this problem to occur. Just trying to do forward declarations in module interface units instead of imports to further try to improve compilation times is enough to get you on a goose chase like I just got an hour ago.

So, let's say we have 2 modules: MyType1.ixx:

export module MyType1;

namespace MyNamespace {
    class MyType2;

    export class MyType1 {
    public:
        MyType1(MyType2* MyType2);
    private:
        MyType2* MyType2Intance{};
    };
}

Module1.cpp:

module MyType1;

namespace MyNamespace {
    MyType1::MyType1(MyType2* MyType2) {
        MyType2Intance = MyType2;
    };
}

MyType2.ixx:

export module MyType2;

namespace MyNamespace {
    export class MyType2 {
    public:
    };
}

Then if you try to instantiate MyType1 in Main.ixx module:

export module Main;

import MyType1;
import MyType2;

using namespace MyNamespace;

int main() {
    MyType2* MyType2Ptr = nullptr;
    MyType1 MyType1(MyType2Ptr);
}

You'll get a Error C2665 'MyNamespace::MyType1::MyType1': no overloaded function could convert all the argument types. Which is absolutely bizarre, since there's enough information to resolve everything. We don't need imports in neither .ixx nor in the .cpp file of MyType1 module for a simple pointer reference. Even if we actually used MyType2 in MyType1.cpp file, we should only need to import MyType2 in the .cpp file and we shouldn't have to import MyType2 in the MyType1.ixx file.

So, what do you do to fix this annoying illogical error that shouldn't exist in the first place?

One solution is to also export the forward-declared class MyType2 in MyType1.ixx:

export module MyType1;

namespace MyNamespace {
    export class MyType2;

    export class MyType1 {
    public:
        MyType1(MyType2* MyType2);
    private:
        MyType2* MyType2Intance{};
    };
}

Another solution is to forward-declare the MyType2 class before the export module MyType1 line:

namespace MyNamespace {
    class MyType2;
}

export module MyType1;

namespace MyNamespace {
    export class MyType1 {
    public:
        MyType1(MyType2* MyType2);
    private:
        MyType2* MyType2Intance{};
    };
}

And if you use MyType2 in the module implementation file of MyType2, of course, you'll have to import it properly.

I'd show a running compiling example on godbolt or something, but I don't think there's a way to create multi-file solutions like that there.

Now, why this works, I don't really know as I'm only starting with modules myself. Visual Studio warned me yesterday in the build output log that the #include <Windows.h> after an export module declaration line export module MyModule; is probably erroneous(but it compiled the solution just fine) and that I should move my #include <Windows.h> before that line. I did that, it worked, and so that means that there are multiple sections of the module in the module interface file: before export module MyModule; line and after at the very least.

Also, why does the first solution with exporting the forward declaration fixes the compilation error? The Main module imports both modules, so it doesn't have problems having all information needed.

I did a few tutorials on modules, like this one by Microsoft: https://learn.microsoft.com/en-us/cpp/cpp/tutorial-named-modules-cpp?view=msvc-170

But I don't think any of them explained this forward declaration buggy quirk, and also how #including headers is different before and after the export module MyModule; line. Let me know if know why and even better if you have a link to video/article/book where it's explained how all this works and why all of this is happening.

Not the answer you're looking for? Browse other questions tagged or ask your own question.