Search code examples
c++templatesinheritanceinterfacetemplate-specialization

How does template specialization work for hierarchies of template classes?


Below I have two separate templated class hierarchies (Account -> CheckingAccount and Logger -> ConsoleLogger) and a template Bank class that uses both.

The goal is to use different Loggers, one for fundamental types like long, and one tailored to Account objects, the latter taking a pointer argument because of the polymorphism of Account.

I suppose this could be achieved entirely with templates, however solving this should shed some light on how template specialization work for inheritance hierarchies of classes, if at all.

The MWE below does not compile successfully, see compiler link.

#include <cstdio>
//#include <concepts>
//#include <type_traits>

class Account {
public:
  virtual ~Account() {}

  virtual const long getId() = 0;
};

class CheckingAccount: public Account {
public:
  CheckingAccount() = default;
  CheckingAccount(const long id): _id{id} {}
  ~CheckingAccount() {}

  const long getId() {
    return _id;
  }

private:
  long _id;
};


////////////////////////////////////////////////////////////////
template<typename T> class Logger {
public:
  virtual void logTransfer(T, T, const double) = 0;
};

template<typename T> class ConsoleLogger : public Logger<T> {
public:
  void logTransfer(T from, T to, const double amount) override {
      printf("[console] %ld -> %ld: %f\n", from, to, amount);
  }
};

// template class specialization
template<> class ConsoleLogger<Account*> : public Logger<Account*> {
public:
  void logTransfer(Account* from, Account* to, const double amount) override {
    printf("[console] %ld -> %ld: %f\n", from->getId(), to->getId(), amount);
  }
};

//////////////////////////////////////////////////////////////////// 
template<typename T> struct Bank {
  void setLogger(Logger<T>* new_logger) {
    logger = new_logger;
  }
  
  void logTransfer(T from, T to, const double amount) {
    if(logger)
      logger->logTransfer(from, to, amount);
  }

private:
  Logger<T>* logger;
};

// template class specialization
template<> struct Bank<Account*> {
  void setLogger(Logger<Account*>* new_logger) {
    logger = new_logger;
  }
  
  void logTransfer(Account* from, Account* to, const double amount) {
    if(logger)
      logger->logTransfer(from, to, amount);
  }

private:
  Logger<Account*>* logger;
};


/////////////////////////////////////////////
int main() {
  // try with long input
  ConsoleLogger<long> console_logger;
  Bank<long> bank;
  bank.setLogger(&console_logger);
  bank.logTransfer(500L, 1000L, 23.56);
  
  // try with Account input
  CheckingAccount* a = new CheckingAccount{500};
  CheckingAccount* b = new CheckingAccount{1000};
  printf("Account no.%ld\n", a->getId());
  printf("Account no.%ld\n", b->getId());
  ConsoleLogger<CheckingAccount*> console_logger2;
  Bank<Account*> bank2;
  bank2.setLogger(&console_logger2);
  bank2.logTransfer(a, b, 42.81);
  delete a;
  delete b;
}

Compiler yields:

main.cpp: In function ‘int main()’:
main.cpp:94:19: error: cannot convert ‘ConsoleLogger*’ to ‘Logger*’
   94 |   bank2.setLogger(&console_logger2);
      |                   ^~~~~~~~~~~~~~~~
      |                   |
      |                   ConsoleLogger<CheckingAccount*>*
main.cpp:65:36: note:   initializing argument 1 of ‘void Bank::setLogger(Logger*)’
   65 |   void setLogger(Logger<Account*>* new_logger) {
      |                  ~~~~~~~~~~~~~~~~~~^~~~~~~~~~
main.cpp: In instantiation of ‘void ConsoleLogger<T>::logTransfer(T, T, double) [with T = CheckingAccount*]’:
main.cpp:35:8:   required from here
main.cpp:36:27: warning: format ‘%ld’ expects argument of type ‘long int’, but argument 2 has type ‘CheckingAccount*’ [-Wformat=]
   36 |       printf("[console] %ld -> %ld: %f\n", from, to, amount);
      |                         ~~^                ~~~~
      |                           |                |
      |                           long int         CheckingAccount*

So in short, compiler does not see the template specialization for ConsoleLogger<Account*>.

I have tried to specialize Logger<Account*> directly from Logger<T>, and derive ConsoleLogger<Account*> from it, but got the same error message from the compiler.


Solution

  • ConsoleLogger<CheckingAccount*> (what you pass to setLogger) is unrelated to Logger<Account*> (what setLogger needs). You can convert ConsoleLogger<CheckingAccount*> to Logger<CheckingAccount*>, but that's about it. Logger<CheckingAccount*> is unrelated to Logger<Account*> as well.

    In your example you don't really need ConsoleLogger<CheckingAccount*> because the logger doesn't use checking account specific things. So changing

    ConsoleLogger<CheckingAccount*> console_logger2;
    

    to

    ConsoleLogger<Account*> console_logger2;
    

    should work.