Home > Back-end >  Call a non-default constructor for a concept
Call a non-default constructor for a concept

Time:09-14

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 with cout vs DatabaseAccessor 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.

  • Related