Search code examples
c++objectlogginggtkgtkmm

How to share objects in c++ across libraries


Suppose I have a program like this:

File main.cpp

#include "something.hpp"

int main(int argc, char* argv[]) {
    some = new Something();
    return 0;
}

which will be linked to a .so library consisting of following files:

File logger.hpp

#include <iostream>

class Logger {
    public:
        Logger();
        void log(char);
        void set_name(char);
    private:
        char m_name;
};

File logger.cpp

#include "logger.hpp"

Logger::Logger() {}
void Logger::log(char msg) {
     std::cout << this->m_name << " : " << msg;
}
void Logger::set_name(char name) {
    this->m_name = name;
}

File something.hpp

#include "logger.hpp"

class Something {
    public:
        Something();
};

File something.cpp

#include "something.hpp"

Something::Something() {
    logger->log("hello !");
}

The code as it is now will fail in something.cpp at logger->log(), because logger has never been defined. I could solve this by adding logger = new Logger(). But I want to only create a new Logger instance, if none has been created in a program / library using this library. When an instance has been created already, I can use that by adding extern Logger logger;. But this will not work, when no instance has been created. Any suggestions (is it possible at all ?) ?

Note: I am using Gtkmm4 / Glibmm2.6 already, maybe there is a solution by using Gtk or Glib ...


Solution

  • First approach: Singleton

    As discussed in the comments, you could use the Singleton design pattern to acheive this. However, remember that this pattern has several drawbacks, two of which are:

    • Singletons allow global access.
    • Singletons are hard to unit test.

    Which are true problems when writing quality software. Also, for your particular case, make sure to read this answer which explains how to make sure everything is linked appropriately so you do not end up with multiple instances of your singleton.

    Second approach: dependency injection

    I decided to post an answer here to illustrate another way of doing things that solves the two issues mentionned above: dependency injection (DI). With DI, you do not create your dependencies, you inject them through parameters. For example, instead of:

    Something::Something() {
        auto logger = new Logger(); // Dependency creation (not injection)
        logger->log("hello !");
    }
    

    you would have something like:

    Something::Something(Logger* p_logger) { // Logger dependency injected through p_logger
        p_logger->log("hello !");
    }
    

    Note that DI does not solve the "one instance" issue by itself. Care must be taken to create your dependencies once (usually in your main) and then pass them around as parameters to use them. However, the global access issue is resolved.

    You can bring this to another level by abstracting your dependencies. For example, you could write an interface to your Logger class and use this instead:

    // Somewhere in your library:
    class ILogger
    {
    public:
        virtual ~ILogger() = default;
        virtual void log(const std::string& p_message) = 0;
        virtual void set_name(const std::string& p_name) = 0;
    };
    
    // In Logger.hpp:
    class Logger : public ILogger {
        public:
            Logger();
    
            void log(const std::string& p_message) override;
            void set_name(const std::string& p_name) override;
    
        private:
            std::string m_name;
    };
    
    // In something.hpp/cpp:
    Something::Something(ILogger* p_logger) { // Logger dependency injected through p_logger
        p_logger->log("hello !");
    }
    

    To acheive this your main could look like this:

    int main(int argc, char* argv[]) {
        // Here, you create your logger dependency:
        std::unique_ptr<ILogger> concreteLogger = std::make_unique<Logger>();
        concreteLogger->set_name("frederic");
    
        // Here, you inject it. From here on, you will inject it everywhere
        // in your code. The using code will have no idea that under the hood,
        // you really are using the Logger implementation:
        some = new Something(concreteLogger.get());
    
        // Note: if you use `new`, do not forget to use `delete` as well. Otherwise,
        //       check out std::unique_ptr, like above.
    
        return 0;
    }
    

    The advantage of this is that you can now change the implementation of your logger at any time whithout anything (except main) caring about it. You can also create mocks of your logger in case you want to unit test Something. This is highly more flexible that handling the singleton in your unit tests, which in term will create all sorts of (hard to investigate/resolve) problems. This, in terms, solves the second issue mentionned above.

    Note that a possible drawback of DI is that you may end up having lots of parameters, but in my opinion it is still superior to using singletons.