Necessity of forward-declaring template functions

spraff picture spraff · Aug 31, 2011 · Viewed 11.8k times · Source

I recently created this example code to illustrate C++11 variadic template function usage.

template <typename Head, typename... Tail> void foo (Head, Tail...);
template <typename... Tail> void foo (int, Tail...);
void foo () {}

template <typename... Tail>
void foo (int x, Tail... tail)
{
    std :: cout << "int:" << x;
    foo (tail...);
}

template <typename Head, typename... Tail>
void foo (Head x, Tail... tail)
{
    std :: cout << " ?:" << x;
    foo (tail...);
}

foo (int (123), float (123)); // Prints "int:123 ?:123.0"

If the first two lines which forward-declare foo are omitted then this prints int:123int:123 instead. This surprised a certain experienced and knowledgeable C++ programmer.

He was convinced the forward-declarations shouldn't be necessary because the body won't be instantiated until the second phase of two-phase lookup. He thinks the compiler (gcc 4.6) has a bug.

I believe the compiler is right because the two foo are different base template functions and the choice of base template needs to be locked-in during the first phase or else you could violate the one-definition rule by instantiating foo before all versions of it have been defined and then again afterwards (consider how the linker assumes that redundant template function definitions are identical, interchangeable, and discardable).

So, who is right?


The above-linked GOTW nicely explains how and why function templates don't partially specialise, but the existence of variadic template functions seems to add to the confusion -- the intuition that foo<int,Tail...> should be a partial specialisation of foo<Head,Tail...> is stronger than that intuition for non-variadic functions, at least to me.

Answer

Matthieu M. picture Matthieu M. · Aug 31, 2011

GCC (and Clang) are right. MSVC would get it wrong because it does not implement the look-up correctly.

There is, it seems, a misunderstanding from your colleague. The rules for look-up are:

  • the Base template function need be declared before it is called from a definition
  • the Specialized template function need be declared before it is instantiated

Note: those rules apply for free-functions, within a class no forward declaration is required

Note that because a definition also acts as a declaration it is unnecessary, in your example, to forward declare the int version.

Correct example:

template <typename T> void foo(T);             // declare foo<T>

template <typename T> void bar(T t) { foo(t); }// call foo<T> (dependent context)

template <> void foo<int>(int);                // declare specialiaztion foo<int>

void bar(int i) { foo(i); }                    // instantiate foo<T> with int
                                               // which is the specialization

If there is base template available, this is an error. If the specialization is not declared prior to the instantiation, it won't be used, and this may, subsequently, mean a violation of the ODR rule (if another instantiation uses the specialization).

From the Standard (C++0x FDIS):

14.6.4.2

1. For a function call that depends on a template parameter, the candidate functions are found using the usual lookup rules (3.4.1, 3.4.2, 3.4.3) except that:

— For the part of the lookup using unqualified name lookup (3.4.1) or qualified name lookup (3.4.3), only function declarations from the template definition context are found.

— For the part of the lookup using associated namespaces (3.4.2), only function declarations found in either the template definition context or the template instantiation context are found.

If the function name is an unqualified-id and the call would be ill-formed or would find a better match had the lookup within the associated namespaces considered all the function declarations with external linkage introduced in those namespaces in all translation units, not just considering those declarations found in the template definition and template instantiation contexts, then the program has undefined behavior.

Note that the paragraphs referred to are for regular functions.