Why is template parameter deduction not working with a variadic template class where only the first two parameters are specified?

Viewed 66

I have a variadic template class which takes two fixed template parameters and additionally a variable list of parameters. When I create an instance I want to specify the first two parameters and have the rest deduced from the arguments passed to the ctor. But it does not work, the variadic part seems always to be empty. I can only create an instance when I specify all the types (including the ctor arguments).

Here is the code I used for testing:

#include <iostream>
#include <tuple>
#include <string>

class Service
{
public:
  virtual void Serve() = 0;
};


class InterfaceA : public Service {};
class InterfaceB : public Service {};
class InterfaceC : public Service {};


class ImplementationA : public InterfaceA
{
  virtual void Serve() override
  {
    std::cout << "Implementation A: <null>";
  }
};


class ImplementationB : public InterfaceB
{
public:
  ImplementationB(int x)
    : m_x(x)
  {}

  virtual void Serve() override
  {
    std::cout << "Implementation B: " << std::to_string(m_x);
  }

private:
  int m_x = 0;
};


class ImplementationC : public InterfaceC
{
public:
  ImplementationC(std::string str)
    : m_str(str)
  {}

  virtual void Serve() override
  {
    std::cout << "Implementation C: " << m_str;
  }

private:
  std::string m_str;
};


template <typename Interface, typename Implementation, typename... CtorArgs>
class Wrapper
{
public:
  Wrapper(CtorArgs&&... args)
    : m_ctorArgs(std::make_tuple(std::forward<CtorArgs>(args)...))
  {}

  Service& GetService()
  {
    m_service = std::apply([](CtorArgs ... ctorArgs)
    {
      return std::make_unique<Implementation>(ctorArgs...);
    },
      m_ctorArgs);

    return *m_service;
  }

private:
  std::tuple<CtorArgs ...> m_ctorArgs;
  std::unique_ptr<Service> m_service;
};

// deduction guide, not working...
template <typename Interface, typename  Implementation, typename... CtorArgs>
Wrapper(int x)->Wrapper<Interface, Implementation, int>;


int main()
{
    Wrapper<InterfaceA, ImplementationA> wrapperA;
    wrapperA.GetService().Serve();
    std::cout << "\n";

//    Wrapper<InterfaceB, ImplementationB> wrapperB(7);       // NOT OK
    Wrapper<InterfaceB, ImplementationB, int> wrapperB(7);    // OK
    wrapperB.GetService().Serve();
    std::cout << "\n";
}

I want to specify services, but create them on demand, when they are needed (due to dependencies between services). I already use factory methods in production code (wrappers which know what parameters to pass to service ctor), but in test code, I want to be able to quickly create a wrapper for mocks and dummy services, which might need different parameters as the production service.

I also tried to specify a deduction guide, but it seems to have no effect...

2 Answers

You might use template constructor, and std::function as factory:

template <typename Interface, typename Implementation>
class Wrapper
{
public:
  template <typename... CtorArgs>
  Wrapper(CtorArgs&&... args)
    : m_factory([=](){return std::make_unique<Implementation>(ctorArgs...);})
  {}

  Service& GetService()
  {
    m_service = m_factory();
    return *m_service;
  }

private:
  std::function<std::unique_ptr<Service>()> m_factory;
  std::unique_ptr<Service> m_service;
};

Deduction guide is useless as it should be used to deduce all parameters.

It is all or nothing for providing template parameters.

But you could do:

Wrapper<InterfaceB, ImplementationB> wrapperB(7); // Ok

The deduction guide "should" be

template<typename Interface, typename Implementation, typename... CtorArgs>
Wrapper(CtorArgs&&... x)->Wrapper<Interface, Implementation, CtorArgs...>;

but this doesn't work, since Interface and Implementation are non-deducible.

I'd recommend following the standard library and using a factory function instead:

template<typename Interface, typename Implementation, typename... Args>
Wrapper<Interface, Implementation, Args...> make_wrapper(Args&&... args) {
    return Wrapper<Interface, Implementation, Args...>(std::forward<Args>(args)...);
}

int main() {
    auto wrapperA = make_wrapper<InterfaceA, ImplementationA>();
    wrapperA.GetService().Serve();
    std::cout << "\n";
}

Another solution is to add dummy parameters to Wrapper::Wrapper

template<typename T>
struct type_t { };
template<typename T>
constexpr inline type_t<T> type{};

template<typename Interface, typename Implementation, typename... CtorArgs>
class Wrapper {
public:
  Wrapper(type_t<Interface>, type_t<Implementation>, CtorArgs&&... args)
    : m_ctorArgs(std::make_tuple(std::forward<CtorArgs>(args)...))
  {}
// ...
};

// not needed anymore, is implicit
// template<typename Interface, typename Implementation, typename... CtorArgs>
// Wrapper(type_t<Interface>, type_t<Implementation>, CtorArgs&&... x)->Wrapper<Interface, Implementation, CtorArgs...>;

int main() {
    Wrapper wrapperB(type<InterfaceB>, type<ImplementationB>, 7);
    wrapperB.GetService().Serve();
    std::cout << "\n";
}

There's also this OCaml inspired thing:

template<typename Interface, typename Implementation>
struct Wrapper {
    template<typename... Args>
    class type {
    public:
        type(Args&&... args)
          : m_ctorArgs(std::make_tuple(std::forward<Args>(args)...))
        {}
    // ...
    };
};

int main() {
    std::string s("Hello!");
    // There's a spot of weirdness here: passing s doesn't work because then you end up trying to store a reference to s in the tuple
    // perhaps the member tuple should actually be std::tuple<std::remove_cvref<Args>...>
    Wrapper<InterfaceC, ImplementationC>::type wrapperC(std::move(s));
    wrapperC.GetService().Serve();
    std::cout << "\n";
}

Side note: Service::~Service() should probably be virtual.

Related