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:
Derived
is passed asContainer
, so typeContainer
isDerived
.- The constraint
requires std::derived_from<Container, TemplatedBase<ModelType>>
succeeds, so we knowContainer
is a Derivative ofTemplatedBase<...>
. - Knowing that
Container
isDerived
, which inherits fromTemplatedBase<std::string>
, the innerModelType
should 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?