Suppose I have a program like this:
#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:
#include <iostream>
class Logger {
public:
Logger();
void log(char);
void set_name(char);
private:
char m_name;
};
#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;
}
#include "logger.hpp"
class Something {
public:
Something();
};
#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 ...
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:
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.
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.