Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Forward declaration to break cyclic dependency in C++20 modules doesn't work

I've been banging my head on this problem for days, I read a lot of documentation and posts about new C++20 modules among which this official one, this one and this other one on Stackoverflow, but I really cannot solve this problem.

I'm using MSVC compiler delivered with Visual Studio Preview 16.6.0 2.0. I know it is not a stable release yet, but I'd like to mess around with new features to start learning them.

Basically I wrote a module (myModule) and 2 partitions of this module (mySubmodule1 and mySubmodule2) and I implemented them in two module implementation files (mySubmodule1Impl.cpp and mySubmodule2Impl.cpp).

mySubmodule1 have a dependency on mySubmodule2, and vice-versa. Here is the source:

mySubmodule1.ixx

export module myModule:mySubmodule1;

export namespace myNamespace{

class MyClass2;

class MyClass1{
    public:
    int foo(MyClass2& c);
    int x = 9;
};
}

mySubmodule2.ixx

export module myModule:mySubmodule2;
import :mySubmodule1;

export namespace myNamespace{

class MyClass2 {
    public:
    MyClass2(MyClass1 x);
    int x = 14;
    MyClass1 c;
};
}

mySubmodule1Impl.cpp

module myModule:mySubmodule1;
import :mySubmodule2;

int myNamespace::MyClass1::foo(myNamespace::MyClass2& c) {
    this->x = c.x-14;
    return x;
}

mySubmodule2Impl.cpp

module myModule:mySubmodule2;
import :mySubmodule1;

myNamespace::MyClass2::MyClass2(myNamespace::MyClass1 c) {
    this->x = c.x + 419;
}

myModule.ixx

export module myModule;

export import :mySubmodule1;
export import :mySubmodule2;

As you can see I can forward declare MyClass2 in mySubmodule1, but I cannot forward declare MyClass1 in mySubmodule2, because in MyClass2 I use a concrete object of type MyClass1.

I compile with this line: cl /EHsc /experimental:module /std:c++latest mySubmodule1.ixx mySubmodule2.ixx myModule.ixx mySubmodule1Impl.cpp mySubmodule2Impl.cpp Source.cpp where Source.cpp is just the main.

I get the infamous error C2027: use of undefined type 'myNamespace::MyClass2' in mySubmodule1Impl.cpp and mySubmodule2Impl.cpp at the lines where I use MyClass2. Moreover the compiler tells me to look at the declaration of MyClass2 in mySubmodule1.ixx where there is the forward declaration.

Now, I really do not understand where is my mistake. I checked over and over but the logic of the program seems perfect to me. The order of compilation of the files should define MyClass2 before it is even used in the implementation!

I tried to compile this exact program using the "old" .h and .cpp files instead of modules, and it compiles and run fine. So I guess I'm missing something regarding these new modules.

I checked on the first official proposal of modules (paragraph 10.7.5), and in the first one there was a construct named proclaimed ownership declaration which seemed to be perfect in such cases. Basically it allows you to import an entity owned by another module in the current module, but without importing the module itself. But in later revisions of the proposal there is no sign of it. Abslolutely nothing. And in the "changelog" section of the new proposal it isn't even cited.

Please don't tell me cyclic dependencies are bad. I know often they are bad, but not always. And even if you think they are always bad I'm not asking for a rule of the thumb. I'm asking why my code compiles with "old" .h + .cpp but not with new modules. Why the linker doesn't see the definition of MyClass2.


EDIT 1

Here is the new design suggested in the answer, but it still doesn't work. I get the exact same errors:

mySubmodule1Impl.cpp

module myModule;

int myNamespace::MyClass1::foo(myNamespace::MyClass2& c) {
    this->x = c.x-14;
    return x;
}

mySubmodule2Impl.cpp

module myModule;

myNamespace::MyClass2::MyClass2(myNamespace::MyClass1 c) {
    this->x = c.x + 419;
}

All of the other files are unchanged.

like image 350
Lapo Avatar asked Mar 27 '20 18:03

Lapo


People also ask

How can I remove cycle dependency?

To remove the cycle using DI, we remove one direction of the two dependencies and harden the other one. To do that we should use an abstraction: in the order module, we add the DiscountCalculator interface. Thus, the Order class depends on this new type and the order module no longer depends on the customer module.

How would you fix a cyclic dependency Java?

A simple way to break the cycle is by telling Spring to initialize one of the beans lazily. So, instead of fully initializing the bean, it will create a proxy to inject it into the other bean. The injected bean will only be fully created when it's first needed.

What is cyclic dependency in C++?

Circular Dependencies in C++Its goal is to draw the “include” dependencies between classes in a C++ project. In particular, it allows to detect circular dependencies very easily or to check the architecture of a project.

How is cyclic dependency detected?

Analyze cyclic dependenciesFrom the main menu, select Code | Analyze Code | Cyclic Dependencies. In the Specify Cyclic Dependency Analysis Scope dialog, select the scope of files that you want to analyze. Select the Include test sources option if you want to analyze your test code together with the production code.


2 Answers

The immediate problem is that you can’t have an “interface file” and an “implementation file” for a single module partition (as if it were a header file and source file pair). There are interface partitions and implementation partitions, but each must have its own name because each exists to be imported. Of course, it is also one of the purposes of modules to allow a single file where header/source pairs were needed: you can often include the implementation in the same file as the interface file but use export and/or inline only with the latter. This does come with the usual header-only downside of causing more frequent downstream rebuilds.

The metaproblem is that there is no circularity here: you’ve already addressed it with the forward declaration of MyClass2. That’s the right thing to do: modules don’t change the basic semantics of C++, so such techniques remain applicable and necessary. You can still divide the classes into two files for the usual organizational reasons, but there’s no need for the method definitions to be in partitions at all (nor in separate module myModule; implementation units, which automatically import all of the interface). The import :mySubmodule1 that remains (in the interface partition mySubmodule2) is then unambiguous and correct.

As for proclaimed-ownership-declarations, they appeared in the Modules TS that didn’t have module partitions, such that cases like this could not be handled otherwise (since you can use a normal forward declaration for an entity from another partition but not another module).

like image 196
Davis Herring Avatar answered Oct 08 '22 00:10

Davis Herring


Try exporting the forward declarations. e.g.

// A.cc

export module Cyclic:A;

export class B;
export class A {
public:
    char name() { return 'A'; }
    void f(B& b);
};
// B.cc

export module Cyclic:B;

export class A;
export class B {
public:
    char name() { return 'B'; }
    void f(A& a);
};
// A_impl.cc

module Cyclic;

import Cyclic:A;
import Cyclic:B;

import <iostream>;

void A::f(B& b) {
  std::cout << name() << " calling " << b.name() << std::endl;
}
// B_impl.cc

module Cyclic;

import Cyclic:B;
import Cyclic:A;

import <iostream>;

void B::f(A& a) {
  std::cout << name() << " calling " << a.name() << std::endl;
}
// Cyclic.cc

export module Cyclic;
export import :A;
export import :B;
like image 1
claz78 Avatar answered Oct 07 '22 23:10

claz78