c++templatestemplate-specializationmember-functionsdefault-implementation

Is it safe to place definition of specialization of template member function (withOUT default body) in source file?


Here's what I mean:

// test.h
class cls
{
public:
    template< typename T >
    void f( T t );
};

-

// test.cpp
template<>
void cls::f( const char* )
{
}

-

// main.cpp
int main()
{
    cls c;

    double x = .0;
    c.f( x ); // gives EXPECTED undefined reference (linker error)

    const char* asd = "ads";
    c.f( asd ); // works as expected, NO errors

    return 0;
}

This is completely fine, right?

I started doubting this, because I just ran over the specialization of '...' after instantiation error, which was new to me. So, I "worked around" this error and everything seems to work fine now, but still..

Is this well-defined behavior?


edit: And the same for non-member template functions (forward declared non-member template functions).


Solution

  • Lightness Races in Orbit cited why it's not compliant parts from the Standard. There might be some others, in the vicinity.

    I will try to explain in simpler terms what the Standard verbiage means, and hopefully I'll get it correctly, and finally explain the linker errors (or absence of error):

    1. What is the point of instantiation ?
    2. How does the compiler select a specialization ?
    3. What is necessary at the point of instantiation ?
    4. Why a linker error ?

    1/ What is the point of instantiation ?

    The point of instantiation of a template function is the point where it is called or referred to (&std::sort<Iterator>) with all the template parameters fleshed out (*).

    template <typename T>
    void foo(T) { std::cout << typeid(T).name() << "\n"; }
    
    int main() { foo(1); } // point of instantiation of "foo<int>(int)"
    

    It can be delayed though, and thus not match the exact call site, for templates called from other templates:

    template <typename T>
    void foo(T) { std::cout << typeid(T).name() << "\n"; }
    
    template <typename T>
    void bar(T t) { foo(t); } // not a point of instantiation, T is still "abstract"
    
    int main() { foo(1); } // point of instantiation of "bar<int>(int)"
                           // and ALSO of "foo<int>(int)"
    

    This delay is very important as it enables writing:

    (*) Roughly speaking, there are exceptions such as non-template methods of a template class...


    2/ How does the compiler select a specialization ?

    At the point of instantiation, a compiler need to be able to:

    This old GotW shows off the woes of specializations... but in short:

    template <typename T> void foo(T);   // 1
    template <typename T> void foo(T*);  // 2
    

    are overloads, and each spawns a distinct family of possible specializations of which they are the base.

    template <> void foo<int>(int);
    

    is a specialization of 1, and

    template <> void foo<int*>(int*);
    

    is a specialization of 2.

    In order to resolve the function call, the compiler will first pick the best overload, while ignoring template specializations, and then, if it picked a template function, check if it has any specialization that could better apply.


    3/ What is necessary at the point of instantiation ?

    So, from the way a compiler resolve the call, we understand why the Standard specifies that any specialization should be declared before its first point of instantiation. Otherwise, it simply would not be considered.

    Thus, at the point of instantiation, one needs to have already seen:

    But what of the definition ?

    It is not needed. The compiler assumes it will either be provided later on in the TU or by another TU entirely.

    Note: it does burden the compiler because it means it needs to remember all the implicit instantiations it encountered and for which it could not emit a function-body so that when it finally encounters the definition it can (at last) emit all the necessary code fo all the specializations it encountered. I wonder why this particular approach was selected, and also wonder why even in the absence of an extern declaration the TU may end with undefined function-bodies.


    4/ Why a linker error ?

    Since no definition is provided, gcc trusts you to provide it later and simply emits a call to an unresolved symbol. If you happen to link with another TU that provides this symbol, then everything will be fine, and otherwise you'll get a linker error.

    Since gcc follows the Itanium ABI we can simply look up how it mangles the symbols. It turns out that the ABI makes no difference in mangling specializations and implicit instantiations thus

    cls.f( asd );
    

    calls _ZN3cls1fIPKcEEvT_ (which demangles as void cls::f<char const*>(char const*)) and the specialization:

    template<>
    void cls::f( const char* )
    {
    }
    

    also produces _ZN3cls1fIPKcEEvT_.

    Note: it is not clear to me whether an explicit specialization could have been given a different mangling.