Recently, I was playing around with template type deduction in C++ and came across some behavior I don't understand. The behavior seems to be expected since it is consistent across three major compilers (tested on Windows using Clang 16.0.0, MinGW GCC 13.1.0 distributed by MSYS2, and MSVC 19.33.31630.0), so there must be a deduction rule I'm missing or don't understand.
In this example, I have a templated base class, another class which derives from the templated base instantiated with std::string, and two templated functions which make use of the derived class:
#include <string>
template <typename T>
class TemplatedBase
{
private:
T item;
public:
TemplatedBase(T item)
: item(std::move(item))
{ }
[[nodiscard]] const T& getItem() const
{
return item;
}
};
class Derived : public TemplatedBase<std::string>
{
public:
Derived()
: TemplatedBase("Derived")
{ }
};
// When an instance of 'Derived' is passed to this function as the Container, I expect
// 'ModelType' to be deduced as std::string because of the way we used std::derived_from
// to constrain Container. The actual behavior is that ModelType cannot be deduced.
template <typename ModelType, typename Container>
requires std::derived_from<Container, TemplatedBase<ModelType>>
const ModelType& getItemFromConstrained(const Container& container)
{
return container.getItem();
}
// When an instance of 'Derived' is passed to this function, both Container and ModelType
// are deduced just fine.
template <
typename ModelType,
template <typename> typename Container
>
const ModelType& getItemFromTemplated(const Container<ModelType>& container)
{
return container.getItem();
}
int main()
{
Derived derivedInstance{};
// I expect this to work, but it fails with the error:
// "Candidate template ignored: couldn't infer template argument 'ModelType'."
// auto thisFails = getItemFromConstrained(derivedInstance);
// This works, but the 'ModelType' has to be explicitly supplied. I expect the compiler to be able to
// deduce it, like in the above example.
auto thisWorks = getItemFromConstrained<std::string>(derivedInstance);
// This value is deduced as std::string.
auto thisAlsoWorks = getItemFromTemplated(derivedInstance);
return EXIT_SUCCESS;
}
When an instance of Derived is passed to getItemFromConstrained, I expect ModelType to be deduced as std::string in more or less the following steps:
Derivedis passed asContainer, so typeContainerisDerived.- The constraint
requires std::derived_from<Container, TemplatedBase<ModelType>>succeeds, so we knowContaineris a Derivative ofTemplatedBase<...>. - Knowing that
ContainerisDerived, which inherits fromTemplatedBase<std::string>, the innerModelTypeshould be deduced asstd::string.
However, this doesn't happen.
The second function getItemFromTemplated works as expected. I think this is because we explicitly use ModelType when specifying the container argument const Container<ModelType>& container.
What am I missing? In this situation, I expect the template arguments of both getItemFromConstrained and getItemFromTemplated to be deduced in more or less the same way. I thought the requires std::derived_from<Container, TemplatedBase<ModelType>> clause would help the compiler deduce ModelType as std::string when Container resolves to a type which inherits from TemplatedBase<std::string>, but that doesn't happen. Why not?