Home > Mobile >  Structuring a project with circular dependencies using C modules
Structuring a project with circular dependencies using C modules

Time:10-22

I have an existing C project, and although I'm being pretty experienced with C itself, I haven't dony anything with modules yet and I thought it was time to finally bite that bullet.

I have a small library which is currently structured in several source and header files, where each header describes a major class and some of its smaller satellite types and functions. I like this set up, and my initial take on retrofitting modules into this was simply to have each source file being a module part. But I quickly ran into the problem of circular dependencies.

For the sake of argument, let's say this is a simple JSON library that uses some variant-like type called a JsonValue, with a source setup like this:

// JsonValue.h

#include "JsonArray.h"

class JsonValue
{
    std::variant<double, JsonArray> data;
    // ...
};


// JsonArray.h

class JsonValue;

class JsonArray
{
    std::vector<JsonValue> array;
    // ...
};

You'll notice the circular dependency here. JsonValue depends on the definition of JsonArray, and JsonArray depends on the declaration of JsonValue. In practice, this works fine. Although you can't construct a JsonArray in a translation unit that doesn't know the definition of JsonValue (the std::vector<JsonValue> requires this), in reality you'll never just include JsonArray.h without including JsonValue.h.

So, naive as I was, I simply converted the header files to module interface files that respectively exported the modules json:value and json:array. I imported the json:array into json:value, and for json:array itself I didn't need to do anything else. Or so I thought.

// JsonValue.ixx

export module json:value;
import json:array;

export class JsonValue
{
    std::variant<double, JsonArray> data;
    // ...
};


// JsonArray.ixx

export module json:array;

export class JsonArray
{
    std::vector<JsonValue> array;
    // ...
};

(And all member function implementations go in their respective module implementation files)

However, as it turns out, the circular dependency is a problem with modules, as they're essentially separate translation units with their own compilation step. The json:array module gets compiled separately without knowing the definition of JsonValue. I can't import json:value, because json:value itself is dependent on json:array.

I think that the only solution here is to have both classes in the same module interface file. That certainly seems the gist of this answer, although that one doesn't suffer from my problem as they just use pointers.

But I don't want that. In reality, the classes are a lot bigger than just these simple classes, and I don't want to dump them all into the same module interface file. I guess I can still hack around this using includes (which then are not supposed to be included on their own), but that feels kind of dirty. I'm really hoping there's some other solution that I'm missing.

And, perhaps naively as well, I was hoping to do away with separate class definitions and implementations by having every function implemented inline in the class itself. That certainly doesn't seem to be the case, especially not in this particular example.

CodePudding user response:

What you need to do is make sure that the interface partition module json:array does not contain any code that requires JsonValue to be defined. Definitions of member functions or any other code which accesses it should not be part of that partition. They should be defined in an implementation unit, or a later interface unit, so long as they importsboth json:array and json:value.

Also, do not forget to export forward declarations of exported entities.

  • Related