Overloaded non-type template is ambiguous while non-templated function is ok
Asked Answered
A

4

13

If we have a template function which takes a non-type parameter of type int or short the compiler complains about the ambiguity of the following call:

// Definition
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }
template <short S> void foo() { std::cout << "S: " << S << '\n'; }

// Usage
foo<0>(); // Ambiguous, int or short?

At first I wasn't surprised with this behaviour, the literal 0 could be an int or a short, but if we try this:

// Definition
void foo(int i)   { std::cout << "i: " << i << '\n'; }
void foo(short s) { std::cout << "s: " << s << '\n'; }

// Usage
foo(0); // "i: 0"!

The call to foo is unambiguous! it takes the int overload (even when the template version did not). Well, after thinking a little, this isn't a surprising behaviour, after all there's no way to specify a short literal so the compiler thinks that 0 is an int (this is the default behaviour AFAIK), in order to unambiguously call the short version of non-templated foo we can explicitly instantiate a short:

foo(0);        // "i: 0"
foo(short{0}); // "s: 0"

So i thought that this would unambiguate the templated version, but it did not:

foo<int{0}>();   // Ambiguous call, candidates are int and short versions
foo<short{0}>(); // Ambiguous call, candidates are int and short versions
call of overloaded 'foo()' is ambiguous
 foo<int{0}>();
note: candidates are:
void foo() [with int I = 0]
void foo() [with short int S = 0]

call of overloaded 'foo()' is ambiguous
 foo<short{0}>();
note: candidates are:
void foo() [with int I = 0]
void foo() [with short int S = 0]

The last thing I've tried was to use instances instead of literals:

template <int I>   void foo() { std::cout << "I: " << I << '\n'; }
template <short S> void foo() { std::cout << "S: " << S << '\n'; }

void foo(int i)   { std::cout << "i: " << i << '\n'; }
void foo(short s) { std::cout << "s: " << s << '\n'; }

constexpr int i{1};
constexpr short s{5};

int main()
{
    foo(i); // "i: 1"
    foo(s); // "s: 5"
    foo<i>(); // Ambiguous! (expected "I: 1")
    foo<s>(); // Ambiguous! (expected "S: 5")
    return 0;
}

Without success, as you can se... So, what's the question?

  • Why the call to templated foo is ambiguous? (note that the no templated foo takes int version so is unambiguous).
  • Why the call to templated foo remains ambiguous even after specifying the type on the call? (note that the no templated foo works fine).

Thanks.

Affirmation answered 5/3, 2015 at 10:57 Comment(4)
plugging your code into here you get some nice error messages. My interpretation of them is when using foo(i) the compiler uses the type of i for template deduction. When using foo<i>() the compiler is no longer using the type of i but its value since the template is <int I>. Since the value of i can satisfy both templates you are getting the ambiguity error.Jazminejazz
Indeed it is because now the value become part of the type of f(); you're asking the compiler to differentiate between foo<0>() and foo<0>() (where one is a zero of type int and the other a zero of type short).Puddle
@Jazminejazz as you can see, the error messages are indeed the ones I've pasted on the question (in fact I've used ideone to test all the cases) So evei if the error messages are nice I'm unable to understand why is it failing. And, what about specifing explicitly the type int or short, why is ambiguous in this case?Affirmation
@PaperBirdMaster: its ambiguous because it is using the value of the provided argument not the type. check this out where I is clearly an int value. What is happening is akin to not being able to overload a function based solely on return type. How is the compiler to know what you want to use?Jazminejazz
B
5

Here's what happens when you write f<0>().

  1. The compiler looks up f, and finds two function template declarations:

    template <int I>   void foo();
    template <short S> void foo();
    
  2. The compiler sees the explicit template argument list and attempts to substitute it into each function template declaration:

    template <int I>   void foo(); // with I = 0
    template <short S> void foo(); // with S = 0
    

    Substitution succeeds in both cases because 0 is an int, and can be converted to short, and the conversion is an allowed conversion in this context.

  3. After substitution, two candidate function specializations are produced. Both are viable. Overload resolution is then performed - and since the signature is identical and no tiebreaker applies, overload resolution fails and the call is ambiguous.

The point here is that the normal overload resolution rules do not apply to template arguments. The conversions for template arguments are applied in an earlier stage, before the regular overload resolution takes place.

Bigg answered 5/3, 2015 at 15:57 Comment(2)
But, what about using explicit integer and explicit short (constexpr int i{1}; and constexpr short s{5}; foo<i>(); and foo<s>();) there's no ambiguity because they're instances (no literals) and there's no conversions.Affirmation
@Affirmation It doesn't matter. The conversion takes place in step 2, and substitution still succeeds for both. The compiler doesn't realize that there's an ambiguity until it gets to step 3 and actually performs overload resolution.Bigg
R
5

The compiler error message is missleading*. You would intuitively think that it means that "The function invocation is ambigious!", but in reality the compilation fails in an earlier stage, the definition of the specialized function is not even generated at that point.

What it really means is this: "The function specialization is ambigious!"

Lets see how this compiles:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

The first step of the compilation is the template specialization.

Step 1: The compiler realizes that foo<0> is a template specialization, and generates a function declaration accordingly:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

Step 2: The compiler relizes that the function is actually invoked (This seems obvious in this case, but it is less obvious when you have a class template.), and generates a definition:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

template<>
void foo<0>(){
    std::cout << "S: " << 0 << '\n';
}

Step 3: Now you have a callable function, the compilation continues normally.

Lets try to follow the same steps in your case:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

Step 1: Generating the function declaration:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    foo<0>();
    return 0;
}

And at this point the compilation fails, because the specialized declaration of foo is ambigious. If you want proof, try compiling this code:

template <short S> void foo() { std::cout << "S: " << S << '\n'; }
template <int I>   void foo() { std::cout << "I: " << I << '\n'; }

template<>
void foo<0>();

int main(int argc, char* argv[])
{
    return 0;
}

You will get the same error message without the function invocation!

Update

So the takeaway is that everything translates to specialized function declarations. So no matter if you write foo<int{0}> of foo<short{0}>, the compiler will generate template<> void foo<0>(); for both. The explicit types will be ignored. (Thats why its really important that they are constexpr-s.)

Update As T.C. pointed out in his comment, in the standard (413rd page in the PDF) there is a very similar example:

[Example: In the following example, assuming a signed char cannot represent the value 1000, a narrowing conversion (8.5.4) would be required to convert the template-argument of type int to signed char, therefore substitution fails for the second template (14.3.2).

 template <int> int f(int);
 template <signed char> int f(int);
 int i1 = f<1000>(0); // OK
 int i2 = f<1>(0); // ambiguous; not narrowing

—end example]


*The error message is completely correct. Might not be particularly intuitive, but it reflects the procedure specified in the standard. – T.C.

Renettarenew answered 5/3, 2015 at 14:19 Comment(9)
That's an explanation I want to believe in but I think tat it doesn't explain why with explicit values foo<int{0}>() is still ambiguous.Affirmation
@Affirmation Both foo<int{0}>() and foo<short{0}>() translates to template<> void foo<0>();. I have updated my answer accordingly.Renettarenew
Sorry for bothering you again Gábor but, what about the EXPLICIT type inferred from the constexpr int i{1} and the constexpr short s{5} when used as foo<i>() and foo<s>()? In that case why the type of an existing and clearly defined item doesn't disambiguate the template?Affirmation
And the error message is completely correct. Might not be particularly intuitive, but it reflects the procedure specified in the standard.Bigg
Also, template<> void foo<int{0}>(); is perfectly valid.Bigg
@Bigg It is indeed valid, I removed that part. N4296 [temp.deduct]/p9 is a good point. Thank you!Renettarenew
@Affirmation As the types are implicit convertible to each other (there is no narrowing) the compiler would try to generate both method, but both method can not be generated.Renettarenew
It's a shame that I cannot accept two answers, this answer have more information than the one from @Bigg but without the help of T.C. it could have been incomplete.Affirmation
(Comment updated to avoid confusion for future readers.) Saying that the compiler does the same thing as you writing template<> void foo<0>(); and that it causes some sort of ambiguity is incorrect. Look at [14.8.3] in the Standard for the overload resolution process. For what happens when an explicit specialization declaration is encountered, check out [14.8.2.6]. Having two templates that generate specializations that have the same function type is perfectly fine ([14.5.6.1]); just generating them doesn't cause any ambiguity - they're still distinct functions. cc @AffirmationAbdomen
B
5

Here's what happens when you write f<0>().

  1. The compiler looks up f, and finds two function template declarations:

    template <int I>   void foo();
    template <short S> void foo();
    
  2. The compiler sees the explicit template argument list and attempts to substitute it into each function template declaration:

    template <int I>   void foo(); // with I = 0
    template <short S> void foo(); // with S = 0
    

    Substitution succeeds in both cases because 0 is an int, and can be converted to short, and the conversion is an allowed conversion in this context.

  3. After substitution, two candidate function specializations are produced. Both are viable. Overload resolution is then performed - and since the signature is identical and no tiebreaker applies, overload resolution fails and the call is ambiguous.

The point here is that the normal overload resolution rules do not apply to template arguments. The conversions for template arguments are applied in an earlier stage, before the regular overload resolution takes place.

Bigg answered 5/3, 2015 at 15:57 Comment(2)
But, what about using explicit integer and explicit short (constexpr int i{1}; and constexpr short s{5}; foo<i>(); and foo<s>();) there's no ambiguity because they're instances (no literals) and there's no conversions.Affirmation
@Affirmation It doesn't matter. The conversion takes place in step 2, and substitution still succeeds for both. The compiler doesn't realize that there's an ambiguity until it gets to step 3 and actually performs overload resolution.Bigg
H
1

Template function here is parametrized by value, and only by value, not by type! update: now I am not sure. At the other hand, not templated version is parametrized by type, (and one can enjoy polymorphic calls).

update: Well, looks like instantiated functions mangled name actually depends on type of numerical template parameter.

Howdoyoudo answered 5/3, 2015 at 13:28 Comment(0)
L
1

As a supplement for the existing answers, here aims to provide an explanation strictly aligning with the C++20 Standard rules, yet more intuitive one.

In short, this is related to the internal/behind-the-scence process of turning "Templates" into "Normal C Functions"(done by compilers, restricted by the rules mandated by C++ Standard), which leads to the result that the behavior of "Select from two normal function candidates" is way different from that of "Select from two template function candidates".

In your scenario, when you compile:

int main () { foo<0>(); }

against the foos definitions:

template <int I>   void foo() { ... << "I: " << ... } // ft1
template <short S> void foo() { ... << "S: " << ... } // ft2

the function foo is overloaded due to its two template definitions(ft1 and ft2), when a function template is overloaded like this, an important property Partial Ordering(13.7.6.2) among its all definitions will be determined, with this ordering, the compiler is confident enough to select most appropriate one from them, so that there is only one definition being invoked at runtime, which is exactly the behavior we want when using normal C function.

Okay then, what the heck is "Partial Ordering"? The most detail of it is present in C++20 Standard's 13.7.6.2. In human language terms, it is a result of two steps:

  1. Turn each function template into the more specific one.
  2. Compare the generated one at step1 to the others' original form.

Since these are parts of the underlying algorithm, the picture might not be much clear at this point, just keep in mind that they are used by compiler to robustly determine "Which function template should be selected". And elaborating the whole algorithm is kind of stuff that the Standard would do, not here. C++20 Standard's 13.7.6.2, 13.10.2.4, 13.5.4 contain the detail of it, check it if you're curious about.

Let's try to apply the mechanism described above to your scenario.

After the 1st step, the two function templates will be tranformed, by substituting one by one: the actual type for Type Template Parameter, the non-type value for the Non-type Template Parameter, the class template for the Template Template Parameter. Here, we substitue 0 for S/I, resulting in two transformed ones Tft1 and Tft2:

template <0> void foo() { ... << "I:" << ... } // Tft1
template <0> void foo() { ... << "S:" << ... } // Tft2

I emphasize the "value" because your scenario only covers the Non-type Template Parameter.

As you can see, each of two transformed function templates is equivalent to each other, except the function body, which has nothing to do during this compile-time process.

Let's continue and go step 2. What happens here are effectively some comparisons. Specifically, we compare Tft1 to ft2, and deduce that the S could be 0, which means deduction succeeds, which means our 0 is as specialized as original S, in other words, when we use 0 in place of a type short, the semantics is more specific, realizing this is important because what we are doing is making our template from a generic form to a specialized form. With this relationship, Standard(13.10.2.4-10) tells us that Tft1 is at least as specialized as Tft2. Obviously the same holds when comparing Tft2 to ft1, so Tft2 is also as specialized as Tft1. Recall that our goal is to find the exact one that is more specialized than all the others. Unfortunately, combining these two, we cannot determine which one (of Tft1, Tft2) is more specialized than the other one, since they are both as specialized as each other (there is a formal definition for at least as specialized as in 13.10.2.4-8, more specialized than in 13.10.2.4-10). Hence we consider both function templates as valid candidates after step2, since there are no C++20's "Constraints" involved in our scenario, they both are final valid candidates.

After all of these mess, the compiler got not one, but two valid function template candidates, which leads to an ambiguous result. And the compiler should complain about that. Here answers your 1st question.

The most noteworthy point in your scenario is that, when transforming the very orginal function templates, for their non-type template parameters, the compiler should synthesize the values for them, not the type. In other words, the synthesized value contains no information about its original type. Here answers your 2nd question: no matter how you cast your 0 into (int)0 or (short)0, only the eventual value will be used, which is a plain 0.

As long as the value fits in the type of the parameter, it should be fine and everything in this process should go on, in other words, the corresponding function template should be a valid candidate. We can prove it by slightly changing your example:

template <int I>   void foo() { ... << "I: " << ... } // ft1
template <short S> void foo() { ... << "S: " << ... } // ft2

int main () { foo<32769>(); } // #1 Good, 1 candidate 'ft1'
int main () { foo<32768>(); } // #2 Good, 1 candidate 'ft2'
int main () { foo<32767>(); } // #3 ambiguous, 2 candidates 'ft1' and 'ft2'
int main () { foo<32766>(); } // #4 ambiguous, 2 candidates 'ft1' and 'ft2'
// ...
int main () { foo<0>(); }     // #5 ambiguous, 2 candidates 'ft1' and 'ft2', your scenario
// ... 

On a machine where short resides in [-32768, 32768) and int resides in [-2147483648, 2147483648), #1~2 can compile and ft1 will be selected, this is because the value we supplied cannot fit in short, thus ft1 will be the only candidate, nothing ambiguous! It is not true for the rest of them, since the values suppiled by them can fit in both short and int, following the steps we described above, two identical candidates are waiting for the pick, thus an ambiguous result.

If you doubt this, try compile and test '#1~5' on your machine. That gives you a more intuitive sense.

This is not an exhaustive explanation for "Partial Ordering", see C++20 Standard N4849 sections 13.7.6.2, 13.10.2.4, 13.5.4 and other related parts for details. For "Specialized Than" terms, check another comprehensive answer.

Landan answered 22/9, 2022 at 7:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.