0

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:

  1. Derived is passed as Container, so type Container is Derived.
  2. The constraint requires std::derived_from<Container, TemplatedBase<ModelType>> succeeds, so we know Container is a Derivative of TemplatedBase<...>.
  3. Knowing that Container is Derived, which inherits from TemplatedBase<std::string>, the inner ModelType should be deduced as std::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?

scupit
  • 704
  • 6
  • 6
  • 1
    Please don't spam with irrelevant tags. C++17 has no concepts. – 273K May 11 '23 at 16:04
  • 4
    deduction happens from parameters, not from return type or constraint. – Jarod42 May 11 '23 at 16:05
  • [Why can't the compiler deduce template parameter from return type?](https://stackoverflow.com/questions/45075703/why-cant-the-compiler-deduce-template-parameter-from-return-type) – Jason May 11 '23 at 16:08
  • Think about other constraints than specific `std::derived_from` (`convertible_to`, `my_is_addable_with`, ...) to better see how `ModelType` and `Container` are unrelated. – Jarod42 May 11 '23 at 16:10
  • 1
    Got it, I knew template parameters couldn't be deduced from return type but didn't realize they would only be deduced from function arguments. Must have missed this sentence from the [template argument deduction cppreference page](https://en.cppreference.com/w/cpp/language/template_argument_deduction): `When possible, the compiler will deduce the missing template arguments from the function arguments.` Thanks for that. – scupit May 11 '23 at 16:21

1 Answers1

2

ModelType is not in deducible context. You can however make it so by making it a template template where ModelType is included:

template <template<class> class Container, typename ModelType>
    requires std::derived_from<Container<ModelType>, TemplatedBase<ModelType>>
const ModelType& getItemFromConstrained(const Container<ModelType>& container) {
    return container.getItem();
}

Demo

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108