Reputation: 13298
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?
foo
is ambiguous? (note that the no templated foo
takes int
version so is unambiguous).foo
remains ambiguous even after specifying the type on the call? (note that the no templated foo
works fine).Thanks.
Upvotes: 13
Views: 687
Reputation: 336
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 foo
s 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:
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.
Upvotes: 1
Reputation: 1690
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.
Upvotes: 1
Reputation: 2255
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.
Upvotes: 5
Reputation: 137404
Here's what happens when you write f<0>()
.
The compiler looks up f
, and finds two function template declarations:
template <int I> void foo();
template <short S> void foo();
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.
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.
Upvotes: 5