Search code examples
c++templatesimplicit-conversiontemplate-argument-deduction

template deduction and implicit constructors: Is there a way to make template deduction work with implicit conversion?


Is there a way to make template deduction work with (implicit) conversion? Like the following example:

template<typename T> struct A {};

template<typename T> struct B
{
    B(A<T>); // implicit A->B conversion
};

template<typename... Ts> void fun(B<Ts>...);

int main()
{
    A<int> a;
    fun(B(a)); // works
    fun(a);    // does not work (deduction failure)
}

My thoughts:

  • If A is a subclass of B, everything works. That means that deduction can do implicit conversion using upcasting. So it seems weird that it can not do implicit conversion using a constructor.
  • Overloading fun for A and B is possible in principle, but for multiple parameters, there are just too many combinations
  • Adding a deduction guideline (template<typename T> B(A<T>)->B<T>;) does not change anything.

EDIT: some context: In my actual code, A is a (large) container, and B is a lightweight non-owning view object. The situation is similar to the fact that std::vector<T> can not be implicity converted to std::span<T> during deduction of T, even though for any concrete T, such a conversion exists.


Solution

  • Template argument deduction does not consider any potential type conversions - mainly because deduction happens before any such conversions can happen.

    Basically, when the compiler sees fun(a), it first gathers a set of foo functions that are eligible to service that call. If a foo function template is found, the compiler tries to generate a concrete function from it by substituting the template arguments with the types of arguments passed in the call statement. That's where the template argument deduction happens. No type conversion can happen here because there is no concrete type to convert to. In your example, B<T> is not a type to convert to because T is unknown and it cannot be discovered from an argument of type A<int>. It is also not known whether an arbitrary instance of B<T> will be constructible from A<int> because different specializations of B may have different sets of constructors. Hence the deduction fails in your case.

    If the deduction succeeds, the concrete function (with the deduced argument types) is added to the candidate set.

    When the set of candidates is gathered, then the best matching candidate is chosen. At this point argument conversions are considered. The conversions are possible since the candidates are no longer templates, and the target types for conversions are known.

    What you could do to work around it is to allow the compiler to deduce the template as well and then construct B<T> explicitly:

    template<typename... Ts> void fun_impl(B<Ts>...);
    
    template<template<typename> class X, typename... Ts>
    std::enable_if_t<(std::is_constructible_v<B<Ts>, X<Ts>> && ...)> fun(X<Ts>... args)
    {
        fun_impl(B<Ts>(args)...);
    }
    
    int main()
    {
        A<int> a;
        fun(B(a)); // works, instantiates fun<B, int>
        fun(a);    // works, instantiates fun<A, int>
    }