I am making a dependency injection system via static polymorphism with C 20 concepts, so I have this example
I have an interface contract for a Logger
:
template <typename TLogger>
concept ILogger = requires(TLogger engine) {
{ engine.LogInfo(std::declval<std::string>()) } -> std::same_as<void>;
{ engine.LogError(std::declval<int>(), std::declval<std::string>()) } -> std::same_as<void>;
};
A particular implementation of that interface with 2 ctors, a default and a non-default:
class MyLoggerImplementation
{
public:
MyLoggerImplementation() : x(0)
{
}
MyLoggerImplementation(int code) //non default constructor
{
x = code;
}
//Interface methods
public:
void LogInfo(std::string errorMessage)
{
std::cout << std::format("Info: {}!\n", errorMessage);
}
void LogError(int errorCode, std::string errorMessage)
{
std::cout << std::format("Error: {}! with code: {}\n", errorMessage, errorCode);
}
// private members
private:
int x;
};
Then my DatabaseAccessor
has a dependency to a Logger
:
template<ILogger Logger>
class DatabaseAccessor
{
public:
DatabaseAccessor() { }
public:
bool QueryAll()
{
logger.LogInfo("querying all data");
//...
return true;
}
// Injected services
private:
Logger logger;
};
And then when I try to instantiate a DatabaseAccessor
and I inject the Logger
dependency with MyLoggerImplementation
it will call the default constructor:
int main()
{
DatabaseAccessor<MyLoggerImplementation> db; // this will instantiate with the default constructor
// DatabaseAccessor<MyLoggerImplementation(10)> db; // ???
db.QueryAll();
}
How can I instantiate MyLoggerImplementation
with that non-default constructor?
CodePudding user response:
You would have to pass the required parameters to the DatabaseAccessor
constructor, so its initializer list can then pass the parameters to the Logger
's constructor, eg:
template<ILogger Logger>
class DatabaseAccessor
{
public:
template<typename... Params>
DatabaseAccessor(Params&&... params) : logger(std::forward<Params>(params)...) { }
...
private:
Logger logger;
};
DatabaseAccessor<MyLoggerImplementation> db(12345);
CodePudding user response:
You can add a forwarding constructor to DatabaseAccessor
that will pass on the parameters to construct the wrapped log object. That would look like
template<ILogger Logger>
class DatabaseAccessor
{
public:
DatabaseAccessor() { }
template <typename... Ts, std::enable_if_t<std::is_constructible_v<Logger, Ts...>, bool> = true>
DatabaseAccessor(Ts&&... ts) : logger(std::forward<Ts>(ts)...) { }
public:
bool QueryAll()
{
logger.LogInfo("querying all data");
//...
return true;
}
// Injected services
private:
Logger logger;
};
DatabaseAccessor<MyLoggerImplementation> db(10);
The std::enable_if_t<std::is_constructible_v<Logger, Ts...>, bool> = true
part is a SFINAE technique that only allows this constructor to be called if the Logger
type can be constructed by the passed in parameters. This is needed as otherwise this template could be called when you want a copy or move to happen instead.
CodePudding user response:
You could take an instance of the logger as a parameter:
template <ILogger L>
class DatabaseAccessor
{
L logger;
public:
explicit DatabaseAccessor(L logger) : logger(std::move(logger)) { }
};
If you need something more involved than this (like your logger isn't move-constructible), then instead of taking an L
you take a function that returns an L
:
template <class F, class T>
concept FactoryFor = std::invocable<F> and std::same_as<std::invoke_result_t<F>, T>;
template <ILogger L>
class DatabaseAccessor
{
L logger;
public:
template <FactoryFor<L> F>
explicit DatabaseAccessor(F factory) : logger(f()) { }
};
This approach has advantages over just forwarding arguments into the logger:
- it works just fine when you add another member you need to initialize
- it allows for using braced-init-lists to initialize the logger
- it's clearer on the call-site (consider
DatabaseAccessor<OstreamLogger> da(std::cout);
- what's the accessor doing withcout
vsDatabaseAccessor da(OstreamLogger(std::cout));
)
Note that this:
template <typename TLogger>
concept ILogger = requires(TLogger engine) {
{ engine.LogInfo(std::declval<std::string>()) } -> std::same_as<void>;
{ engine.LogError(std::declval<int>(), std::declval<std::string>()) } -> std::same_as<void>;
};
could really be:
template <typename TLogger>
concept ILogger = requires (TLogger engine, int i, std::string s) {
{ engine.LogInfo(s) } -> std::same_as<void>;
{ engine.LogError(i, s) } -> std::same_as<void>;
};
The two aren't exactly identical (I'm passing in lvalues while you're passing in rvalues), but I doubt that distinction will matter in your use-case.