C++: Implicit conversion when templates are explicitly instantiated

121 views Asked by At

It is well known that when we want to have the declarations of templated class/functions in a header file and the their definition in a source cpp file, the must add explicit instantiation at the end of said cpp file - with the limitation that, then, our templates will only work for the explicitly defined cases.

For example, let's say we have the following header with a templated class that has an also templated member function:

// header classA.h

template<class T>
class ClassA
{
    /* template<class U> */
    /* friend class ClassA; */
public:
    template<typename U>
    void foo(U val);
};

extern template class ClassA<double>;
extern template void ClassA<double>::foo(double);

Here, extern tells the compiler that whenever this header is included, the compiler should not try to instantiate ClassA or its member function here - the definitions will come elsewhere.

In our case, they come from the following cpp source file:

// header classA.cpp

#include <stdio.h>
#include "classA.h"


template<class T>
template<typename U>
void ClassA<T>::foo(U val)
{
    // other code that uses val goes here
    printf("foo\n");
}

template class ClassA<double>;
template void ClassA<double>::foo(double);

And we test the code with this main.cpp:

#include <stdio.h>
#include "classA.h"

void test(double x) { printf("test\n"); }

int main()
{
    ClassA<double> a;
    float x = 1.0; // this being a float throws a linker error; it must be double
    a.foo(x);
    test(x);
    return 0;
}

The above code, as is, does not compile because x is float. The ld linker (with G++ 12.3.0) throws the link error:

/usr/bin/ld: /tmp/ccVYWPa2.o: in function `main':
main.cpp:(.text.startup+0x26): undefined reference to `void
ClassA<double>::foo<float>(float)' collect2: error: ld returned 1 exit
status bash: ./a.out: No such file or directory``

If we change x to be a double, then the program compiles as expected. That is, when x is float, the implicit conversion from float to double is not kicking in. What I don't understand is why. Surely, in general the compiler cannot unambiguously decide the type of a templated parameter. But since an explicit instantiation is being given - and just one in this simple example - I would expect the compiler to implicitly convert a float to a double when passing to foo - because there is no other option being explicitly instantiated as to create ambiguity.

That is, I could certainly understand the ambiguity when explicitly instantiate more cases like:

template class ClassA<double>;
template void ClassA<double>::foo(double);
template class ClassA<double>;
template void ClassA<double>::foo(long double);

So, my question is two-folded:

  1. why is the implicit conversion not happening eve if there are no ambiguous cases being explicitly instantiated?

  2. in examples like those, is there a way to enforce implicit conversion at least when T = U (i.e. perhaps based on T, analogous to this answer to a similar but different case)?

EDIT: Obviously, I need both T and U separately. Otherwise I could just drop the function template and use T instead of U.

2

There are 2 answers

0
Weijun Zhou On
  1. It is because U is deduced to be float and instantiation does not play a role at all in type deduction. Providing a explicit instantiation where the type is not a perfect match cannot prevent the compiler from generating an implicit instantiation where the type is a perfect match. It is not at all about overload resolution, so arguments about ambiguity simply do not apply here.
  2. You can make a simple forwarding function (a specialization) using SFINAE techniques. I am using concepts here but you can use other techniques suitable for your c++ version. For those forwarding functions, you need to provide in-class definitions. Something like
#include <concepts>
#include <utility>

template<class T>
class ClassA
{
    /* template<class U> */
    /* friend class ClassA; */
public:
    template<typename U>
    void foo(U val);
    
    template<typename U>
    requires (std::assignable_from<T&, U> && !std::same_as<std::remove_cvref_t<U>, T>)
    void foo(U&& val){
        return foo<T>(std::forward<U>(val));
    }
};

extern template class ClassA<double>;
extern template void ClassA<double>::foo(double);
4
Ben On
  1. It’s not linking because float matches U and then it’s looking for a definition which doesn’t exist.
  2. Take T as the function argument, so the function isn’t templated (just the class it’s a member of). Then a.foo will expect an int, which will lead a.foo(x) to do an implicit conversion. And then it won’t link because you only provided a double version.