Search code examples
c++templateslanguage-lawyer

Why are function template specializations distinct, despite no difference in return type or parameters?


Why does the following code work?

#include <iostream>
#include <cstdint>

template <class D>
void whichD() {
    std::cout << "D is " << sizeof(D) << " byte(s)\n";
}

int main(int argc, char **argv)
{
    if (argv[1][0] == '1') {
        whichD<uint8_t>();
    }
    else {
        whichD<uint16_t>();
    }
}

Program Output for ./temp 1 && ./temp 2
D is 1 byte(s)
D is 2 byte(s)

I did not expect it to work, for the same reason that you can't have:

void foo() { /* ... */ } 
void foo() { /* ... */ } // error: re-definition

int bar() { /* ... */ }
double bar() { /* ... */ } // error: re-definition

Solution

  • It's true that functions have to have a different signature. Otherwise, they are re-definitions of each other. However, what constitutes the signature depends on the kind of entity.

    Signatures of Non-Template Functions

    For non-template functions, the signature is defined as:

    signature

    ⟨function⟩ name, parameter-type-list, and enclosing namespace

    - [defns.signature]

    For example:

    • void foo() and void bar() have a different signature, because they have a different name
    • void foo(int) and void foo(float) have a different parameter-type-list
    • void a::foo() and void b::foo() have a different enclosing namespace
    • void foo() and int foo() are re-definitions of each other; the return type is not part of the signature, so these functions aren't distinct.

    Signatures of Function Template Specializations

    A function template (e.g. whichD) with given template arguments (e.g. whichD<uint8_t>) is called a function template specialization. The signature of a function template specialization is:

    signature

    ⟨function template specialization⟩ signature of the template of which it is a specialization and its template arguments (whether explicitly specified or deduced)

    - [defns.signature.templ.spec]

    In your example, whichD<uint8_t> and whichD<uint16_t> are specializations of the same function template but their template arguments are different, making them distinct functions.

    The compiler doesn't just drop all the templatey-ness and use the template to produce:

    void whichD() { /* ... */ }
    void whichD() { /* ... */ } // error: re-definition
    

    Instead, the compiler will instantiate these specializations, arguments included, and produce something like:

    void whichD<uint8_t>() { /* ... */ }
    void whichD<uint16_t>() { /* ... */ } // OK, different from above
    

    The signatures of these two specializations would get mangled, so uint8_t and uint16_t are included (in some mangled form) in the symbol that the linker sees.

    Reason Behind these Rules

    Keep in mind that these rules exist to distinguish cases where it's possible to call functions separately, and where it isn't. If whichD<uint8_t> and whichD<uint16_> weren't specializations, but regular functions with the name whichD, you would be unable to call one of the two. Overload resolution wouldn't be able to distinguish them.

    However, as you have demonstrated in your code, it is easily possible to call whichD<uint8_t> and whichD<uint16_t> separately by providing the template arguments explicitly. It would be an arbitrary restriction to say that these two have the same signature.