c++templatesparameter-passingtemplate-specializationc++-concepts

C++: Template specialization of class member function, differing from another member function only in accepting parameters by reference, not by value


Within template class Bank<T>, my goal is to create an overload for type Account to member function void Bank<T>::makeTransfer(T, T, const double), of signature void Bank<Account>::makeTransfer(Account&, Account&, const double).

That is, makeTransfer function should accept reference parameters when invoked with Accounts, while it should be copying the arguments in for any other type.

What I went with, is creating another makeTransfer<T> function of similar signature, but accepting reference T& arguments instead of T. Then, I added a template specialization to this new function using Account as T.

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

#include <cstdio>
#include <functional>
#include <concepts>

class Account {
public:
  Account() = default;
  Account(const long id, const double balance): _id{id}, _balance{balance} {
    printf("Account no.%ld start balance is %f\n", _id, _balance);
  }

  const long getId() {
    return _id;
  }

  const double getBalance() {
    return _balance;
  }

  void addToBalance(const double sum) {
    _balance += sum;
  }

private:
  long _id;
  double _balance;
};

template<typename T> class Bank {
public:
  void makeTransfer(T from, T to, const double amount) {
    printf("ID %ld -> ID %ld: %f\n", from, to, amount);
  }
  
  void makeTransfer(T& from, T& to, const double amount) {}
};

template<> void Bank<Account>::makeTransfer(Account& from, Account& to, const double amount) {
  printf("ID %ld -> ID %ld: %f\n", from.getId(), to.getId(), amount);

  from.addToBalance(-amount);
  to.addToBalance(amount);
  printf("Account no.%ld balance is now %f\n", from.getId(), from.getBalance());
  printf("Account no.%ld balance is now %f\n", to.getId(), to.getBalance());    
}

int main() {
  // try with fundamental type as T
  Bank<long> bank;
  bank.makeTransfer(1000L, 2000L, 49.95);
  bank.makeTransfer(2000L, 4000L, 20.00);

  // now define a bank with Account instances
  Account a{500, 2600};
  Account b{1000, 10'000};
  Bank<Account> bank2;

  bank2.makeTransfer(a, b, 49.95);
  bank2.makeTransfer(b, a, 20.00);

  //bank2.makeTransfer(std::ref(a), std::ref(b), 49.95);
  //bank2.makeTransfer(std::ref(b), std::ref(a), 20.00);
}

Compiler protests of not being able to pick the correct template instantiation:

main.cpp:58:21: error: call of overloaded ‘makeTransfer(Account&, Account&, double)’ is ambiguous
   58 |   bank2.makeTransfer(a, b, 49.95);
      |   ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
main.cpp:31:8: note: candidate: ‘void Bank::makeTransfer(T, T, double) [with T = Account]’
   31 |   void makeTransfer(T from, T to, const double amount) {
      |        ^~~~~~~~~~~~
main.cpp:38:17: note: candidate: ‘void Bank::makeTransfer(T&, T&, double) [with T = Account]’
   38 | template<> void Bank<Account>::makeTransfer(Account& from, Account& to, const double amount) {
      |                 ^~~~~~~~~~~~~

I tried to inject references explicitly with std::ref, but that did not help.

Using something like std::same_as<T, Account> seems redundant, as all cues for the compiler should already be available in the template specialization.


Solution

    1. From the point of view of any bank, the Account cannot be copied, right? So, it must prevent copies:
        Account(const Account&) = delete;
        Account& operator=(const Account&) = delete;
      
    2. Update the Bank template
      #include <type_traits>
      
      template <typename T>
      class Bank {
       public:
        void makeTransfer(T from, T to, const double amount)
          requires(std::is_copy_constructible_v<T> || std::is_copy_assignable_v<T>)
        {
          printf("ID %ld -> ID %ld: %f\n", from, to, amount);
        }
      
        void makeTransfer(T& from, T& to, const double amount) {}
      };
      

    https://godbolt.org/z/9sEaov3K8