Why C++ Selects a General Template Over a Base Class Template Overload
The Problem: Unexpected Template Overload Selection
In C++, when you overload function templates, you might expect the compiler to choose a more "specific" overload (like one taking a base class template) over a highly generic one (taking an arbitrary T). However, you might encounter a scenario where the general template is chosen instead.
Consider the following classic example:
#include <iostream>
template<typename T>
class BaseT {};
template<typename T>
class DerivedT : public BaseT<T> {};
// 1. General template
template<typename T>
void func(const T&) {
std::cout << "General template\n";
}
// 2. Base class template overload
template<typename T>
void func(const BaseT<T>&) {
std::cout << "Base overload\n";
}
int main() {
DerivedT<int> d;
func(d); // Outputs: "General template"
}
Why does the compiler choose the general template over the base class overload, even though DerivedT<int> inherits from BaseT<int>?
The Root Cause: Overload Resolution & Conversion Sequences
To understand why this happens, we have to look at how C++ performs Template Argument Deduction and Overload Resolution. The process happens in two distinct stages:
1. Template Argument Deduction
First, the compiler attempts to deduce the template parameters for all candidate functions:
- For the general template: Deducing
TfromDerivedT<int>is straightforward.Tis deduced asDerivedT<int>. The resulting function signature isfunc(const DerivedT<int>&). - For the base overload: The compiler successfully deduces
TasintbecauseDerivedT<int>inherits fromBaseT<int>. The resulting function signature isfunc(const BaseT<int>&).
2. Overload Resolution (The Deciding Factor)
Once the candidate signatures are determined, the compiler compares them using the standard overload resolution rules to find the best match. It evaluates the Implicit Conversion Sequence required for the argument (DerivedT<int>) to match the parameter types:
- For the general template candidate
func(const DerivedT<int>&), the conversion is an Exact Match (Identity conversion). - For the base overload candidate
func(const BaseT<int>&), the conversion requires a Derived-to-Base Conversion.
According to the C++ standard, an Exact Match is always better than a Derived-to-Base conversion. Therefore, the compiler selects the general template.
Note: Template partial ordering (which would favor the more specialized BaseT<T>) is only used as a tie-breaker if the conversion sequences are equally good. Because the conversion sequences here are different, partial ordering is never reached.
How to Fix It: Modern C++ Solutions
Depending on your C++ standard, there are several elegant ways to force the compiler to select the correct overload.
Solution 1: C++20 Concepts (Recommended)
If you are using C++20, you can use Concepts to constrain the general template so that it rejects any types derived from BaseT. This is the cleanest and most modern approach.
#include <iostream>
#include <concepts>
#include <type_traits>
template<typename T>
class BaseT {};
template<typename T>
class DerivedT : public BaseT<T> {};
// Helper trait to detect if a class derives from BaseT
template <typename T>
struct is_derived_from_base : std::false_type {};
template <typename T>
struct is_derived_from_base<BaseT<T>> : std::true_type {};
// For derived classes
template <typename T>
requires requires { []<typename U>(const BaseT<U>&){} (std::declval<T>()); }
struct is_derived_from_base<T> : std::true_type {};
// 1. Constrained General Template
template<typename T>
requires (!is_derived_from_base<T>::value)
void func(const T&) {
std::cout << "General template\n";
}
// 2. Base overload
template<typename T>
void func(const BaseT<T>&) {
std::cout << "Base overload\n";
}
int main() {
DerivedT<int> d;
int x = 42;
func(d); // Outputs: "Base overload"
func(x); // Outputs: "General template"
}
Solution 2: SFINAE with std::enable_if (C++11 / C++14 / C++17)
For older C++ standards, you can use SFINAE (Substitution Failure Is Not An Error) to disable the general template when the argument is a subclass of BaseT.
#include <iostream>
#include <type_traits>
// Helper to detect BaseT inheritance
template <typename T>
char is_base_t_impl(const BaseT<T>&);
template <typename T>
struct is_derived_from_base {
template <typename U>
static auto test(U* u) -> decltype(is_base_t_impl(*u), std::true_type{});
static std::false_type test(...);
using type = decltype(test((T*)nullptr));
static constexpr bool value = type::value;
};
// General template constrained via SFINAE
template<typename T,
typename std::enable_if<!is_derived_from_base<T>::value, int>::type = 0>
void func(const T&) {
std::cout << "General template\n";
}
template<typename T>
void func(const BaseT<T>&) {
std::cout << "Base overload\n";
}
---
Summary
- C++ prioritizes Exact Matches over Derived-to-Base conversions during overload resolution.
- Because the general template
const T&deduces to an exact match, it bypasses your base class overload. - To resolve this, use C++20 Concepts or SFINAE to constrain the general template from accepting types derived from your base template class.