4

I am currently working on a project which will consist of multiple sub-modules. Each sub-module is developed and unit-tested in its own repository. All sub-modules have to be integrated in one main-project repository.

Furthermore I have some "support-functions" modules which are implementing for example a logging tool and a tool for connecting to a database. The support-function modules are used by most of the sub-modules. As the sub-modules are not necessaryly developed with the same life-cycle, it might be that not every sub-module uses the same version of the support-function modules. The support-function modules are also developed and unit-tested in it's own repository.

This means that I in total have a project structure like this :

  • The MainProject consists for example of three submodules (SubmoduleA, SubmoduleB, SubmoduleC)
  • SubmoduleA uses the support-function module "Logging-Tool" with version 4.0
  • SubmoduleB and SubmoduleC are using the support-function module "Logging-Tool" with version 5.0

enter image description here

In the past I would have created one main-project-repository and copied all header and source-files and testing-source-files of all submodules and of the support-functions module into this repo and compile it all together. If I would have wanted to support the different versions of the Logging-Tool I maybe would have renamed it to be able to compile both versions.

Now my question is : How do I do this with the state of the art-integration techniques of 2019? Do I have to compile each sub-module and support-function-tool to a library and do a binary-integration. Or can I do a source-code integration which allows me to easily debug the project with all it's sub-modules. But if I do a source-code integration how can I handle the duplicate support-functions modules with different versions?

Are there some cool techniques / best-practices or tools to use? I already know of the great possibilities of the "google repo" command. This would maybe be a nice help for the Main-Project repository.

I hope someone can help me, or can maybe give me a hint where to continue reading?

Best Regards, anon1234

P.S. : To show a very simple code-example : There is a Logger-class:

which looks in version 4.0 like this

#ifndef LOGGER_H
#define LOGGER_H

#include <iostream>

using namespace std;

class Logger {
    public:
        Logger() {}
        void log(string val_) {
            cout << "DEBUG: " << val_ << endl;
        }
};

#endif // LOGGER_H

and in version 5.0 like this

#ifndef LOGGER_H
#define LOGGER_H

#include <iostream>

using namespace std;

class Logger {
    public:
        Logger() {}
        void log(string val_) {
            cout << val_ << endl;
        }
};

#endif // LOGGER_H

A SubmoduleA could look like :

#ifndef ModuleA_H
#define ModuleA_H

#include "Logger.h"

class ModuleA {
    public:
        ModuleA() {}
        void doSmth() {
            Logger l;
            l.log("ModuleA is doing smth");
        }
};

#endif // ModuleA_H

A SubmoduleB could look like:

#ifndef ModuleB_H
#define ModuleB_H

#include "Logger.h"

class ModuleB {
    public:
        ModuleB() {}
        void doSmth() {
            Logger l;
            l.log("ModuleB is doing smth");
        }
};

#endif // ModuleB_H

A SubmoduleC could look like

#ifndef MODULEC_H
#define MODULEC_H

#include "Logger.h"

class ModuleC {
    public:
        ModuleC() {}
        void doSmth(){
            Logger l;
            l.log("ModuleC is doing smth");
        }
};

#endif // MODULEC_H

  • 1
    This is an excellent question! I don't know the answer. But more to the point, I'm afraid it's a little overwhelming to try to answer. If you don't get a good response, don't despair; maybe try asking a different question on the same topic but with less detail. E.g., "[background of what you know about the integration features in VS2019.] How do I apply integration when I have duplicate support-functions modules with different versions?" Thank you though for such a well thought out and thoroughly researched question! – catfood Aug 4 at 23:32
  • 1
    To view this in a wider context, you should question whether you want to have different sub-modules use different versions of support functions. One possible requirement for a sub-module to be integration-ready for a given release may be that the module uses a given version of the support functions. View the logging subsystem as just another sub-module to be integrated. Then it is clear that a change in its API requires that all users of the API must be changed correspondingly. – Hans-Martin Mosner Aug 5 at 6:18
  • 1
    @Hans-MartinMosner : Thanks a lot for the good suggestion. I think you are totally right, that this is how it should be. But this is also the reason why I am asking this question. I wanted to find out, if there is a good-non-hacky way to work like this. Or if it is necessary to always integrate always everything with the newest version. In my personal project this is maybe not the problem. But if you work in bigger teams with vacation times and long waiting periods this can lead to very long release cycles of the main-project. – mezorian Aug 5 at 7:12
  • 1
    @catfood : Thanks a lot for your answer. I will consider this and maybe later open another easier question. I think there is already one really good answer of amon, but I will keep this question open a bit longer to maybe get more input and more good ideas. By the way this question is not really limited to Visual Studio. For example I am not using it. But anyway I left the tag, as it is maybe good for getting more feedback. – mezorian Aug 5 at 7:16
  • 1
    @anon1234 The ideas behind semantic versioning might help to find ways modules can be developed while keeping their API backward compatible. For example, when a logging module should offer an option to log differently from the former version, you might add this new function under a different name and increment the minor version, or you might define a backward-compatibility-breaking major version. It doesn't necessarily make the decision easier, but it makes it more visible. – Hans-Martin Mosner Aug 5 at 8:58
6

Your incompatible Logger implementations cannot be linked because they have the same name. This leads to a simple solution that will solve all your problems:

Incompatible versions should have different names.

C++ makes following this approach comparatively easy because you can put those into separate namespaces, e.g. logging4::Logger vs logging5::Logger. At the usage site you can do a namespace alias like using logging = logging4 which lets the submodule do a logging module version upgrade by changing a single line of code, as long as the changes between versions are source-compatible.

As far as possible, dependencies should only be referred to from implementation files, not header files. Otherwise, those upstream dependencies become a dependency of the downstream software as well, which only has a chance of working if the include paths are versioned as well.

Also, all of your declarations should be in a namespace, never at the top level. Your include guards also need to use a versioned name. Where possible, use the non-standard #pragma once.

Using a single monorepo where you directly include the source code for all your dependencies suggests a different – but arguably simpler – version management strategy: If you change something in a dependency, you can immediately also change the dependent code so that the project as a whole always works. There are no separate development cycles, but everything is more or less developed together. At most, you might use feature toggles or compatibility interfaces while a migration is in progress.

Example project illustrating how multiple versions can be managed in C++ without conflicts.

How you compile this (static linking, dynamic linking) is irrelevant. The important part is versioning your file paths and namespaces properly.

Logger version 4

logger4/Logger.h:

#pragma once
#include <iostream>

namespace logger4 {
  class Logger {
  public:
    Logger() {}
    void log(string msg) {
      std::cout << "DEBUG: " << msg << std::endl;
    }
  };
}

Logger version 5

logger5/Logger.h:

#pragma once
#include <iostream>

namespace logger5 {
  class Logger {
  public:
    Logger() {}
    void log(string msg) {
      std::cout << msg << std::endl;
    }
  };
}

Submodule A

Depends on Logger version 4 directly.

submodule_a/Module.h:

#pragma once
#include "logger4/Logger.h"

namespace submodule_a {
  using logger = logger4;

  class Module {
  public:
    Module() {}
    void doSmth() {
      logger::Logger l;
      l.log("ModuleA is doing smth");
    }
  };
}

Submodule B

Depends on Logger version 5, but only specifies the dependency inside .cpp files in order to remove the dependency from the public interface.

submodule_b/Module.h:

#pragma once

namespace submodule_b {
  class Module {
  public:
    Module() {}
    void doSmth();
  };
}

submodule_b/Module.cpp:

#include "Module.h"
#include "logger5/Logger.h"

// only visible in current compilation unit
using Logger = logger5::Logger;

namespace submodule_b {
  void Module::doSmth() {
    Logger l;
    l.log("ModuleA is doing smth");
  } 
}
  • Hi amon, thanks a lot for this super fast and also super detailed reply! I am not completely sure how everything of your suggestions works. Because maybe you sometimes have to declare a logger-variable in the header file. But it looks like a very good idea to do it like this and I will try it now!! What I also don't understand yet is how the compilation of the Logger-classes with the same version will happen.Will the compiler compile them only once even though the header files are existing in every submodule?This would be really great. I hope there will be more interesting replies like urs. – mezorian Aug 5 at 7:02
  • @anon1234 Doing stuff in header files is usually fine, but it's important to understand the One Definition Rule (ODR): any object or function is only allowed to have a single definition in your entire software. That's why I suggest using different names, so that you can have both versions side by side. Each logger version should be compiled once, and then linked. If the logger library is header-only, the definitions will be implicitly inline (opt-out from ODR) and the linker will discard duplicates. – amon Aug 5 at 19:21
  • 1
    C++11 has support for inline namespaces, these are especially useful if you have to create template specializations with your versioned classes as template parameters. – Korusef Aug 14 at 3:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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