C++ partial template argument deduction for function with variadic pack produces ambiguous call in Clang and MSVC
Asked Answered
E

2

6

Consider the following snippet (available on compiler epxlorer):

template<typename T, typename... Args>
auto foo(Args&&... args) {}

template<typename... Args>
auto foo(Args&&... args) {}

int main() {
    foo<char>('a');
}

It compiles perfectly fine for GCC and fails for both Clang and MSVC (with compiler saying ambiguous call)

Why do Clang and MSVC fail to such seemingly obvious deduction?

EDIT: GCC provides me with the expected solution as a user, is there an easy way to push clang and msvc to choose the template without much change of the original code?

Eyetooth answered 8/7, 2019 at 23:14 Comment(2)
Personally I would expect the code to fail as it is ambiguous. I could be that template<typename T, typename... Args> auto foo(Args&&... args) is "more specialized" but I don't know how that interacts with variadic templates.Legibility
@Legibility Each function template is "at least as specialized" as the other, so neither is "more specialized". One key paragraph is [temp.deduct.partial]/12, saying that it's okay for a template parameter not to be used at all during the trial template argument deduction which determines function template partial ordering.Midge
E
1

After some tests, and using the mentioned reference to the standard: [temp.func.order], [temp.deduct.partial], I came to the following understanding of the situation.

Problem

Considering the example given in the question:

template<typename T, typename... Args> auto foo(Args&&... args) {} //#1

template<typename... Args>             auto foo(Args&&... args) {} //#2

#2 is a function with a variadic parameter pack that can be deduced. can be deduced, not have to. Thus, nothing prevents the user to explicitly specify the template arguments. Therefore, foo<char>('a') can be as much an explicit instantiation of #2 as an instantiation of #1, provoking the ambiguity. The standard does not favor a preferred match between the overload #1 and #2.

GCC went beyond the standard within its implementation by attributing a higher preference for #1 when a template argument is manually given, while Clang and MSVC kept it vanilla.

Furthermore, ambiguity appears only when the first arguments from the variadic pack and T resolve to the exact same type.

Solution

Here are the solutions that I found for my use case. (Forward object construction or a variadic pack of objects)

Variant 1

Declare an extra function specializing for one argument, this would take precedence over the variadic-based ones. (Does not scale or generalize)

template<typename T> auto foo(T&& args) {}
//or
template<typename T, typename Arg> auto foo(Arg&& arg) {}

Variant 2

Disable the overload when the first argument of the non-empty parameter pack is same as the given type T.

template<typename T, typename... Args>
constexpr bool is_valid() {
    if constexpr(sizeof...(Args)==0)
        return true;
    else
        return !std::is_same_v<T,std::tuple_element_t<0,std::tuple<Args...> > > ;
}

template<typename T, typename... Args, typename = std::enable_if_t<is_valid<T,Args...>()> >
auto foo(Args&&... args) {}
Eyetooth answered 11/7, 2019 at 21:48 Comment(0)
U
6

If you examine the additional diagnostic lines from the compiler, you'll see that it says

<source>(6): note: could be 'auto foo<char>(char &&)'
<source>(3): note: or       'auto foo<char,char>(char &&)'

(from MSVC; Clang is similar)

In this case, since the first (only) parameter to the function foo is a char, the compiler cannot distinguish between the one template parameter and two template parameter versions of the template.

If you change your function call to

foo<char>(10);

it will compile.

There is an example in the language spec ("Partial ordering of function templates", [temp.func.order]) very similar to your code:

template<class T, class... U> void f(T, U...); // #1
template<class T > void f(T); // #2

void h(int i) {
    f(&i); // error: ambiguous
}

Since GCC compiles it, this is a bug in GCC.

Unkenned answered 8/7, 2019 at 23:37 Comment(2)
Thanks for answering. I would argue that the snippet is not similar because the template argument is in the function parameter list, which obviously leads to problem when the parameter pack is empty. In my case, an empty parameter pack should lead either to A or B depending on whether the extra template parameter is explicitly stated by the user. Comments above mention also [temp.deduct.partial] as a potential source.Eyetooth
@Neelahn They're similar, because there are two different templates that can be used to generate code. Comment out either alternative (in your code or the example from the language spec) and it will compile, so the resolution is ambiguous. Explicitly stating the parameter doesn't have an effect on which template will be used.Unkenned
E
1

After some tests, and using the mentioned reference to the standard: [temp.func.order], [temp.deduct.partial], I came to the following understanding of the situation.

Problem

Considering the example given in the question:

template<typename T, typename... Args> auto foo(Args&&... args) {} //#1

template<typename... Args>             auto foo(Args&&... args) {} //#2

#2 is a function with a variadic parameter pack that can be deduced. can be deduced, not have to. Thus, nothing prevents the user to explicitly specify the template arguments. Therefore, foo<char>('a') can be as much an explicit instantiation of #2 as an instantiation of #1, provoking the ambiguity. The standard does not favor a preferred match between the overload #1 and #2.

GCC went beyond the standard within its implementation by attributing a higher preference for #1 when a template argument is manually given, while Clang and MSVC kept it vanilla.

Furthermore, ambiguity appears only when the first arguments from the variadic pack and T resolve to the exact same type.

Solution

Here are the solutions that I found for my use case. (Forward object construction or a variadic pack of objects)

Variant 1

Declare an extra function specializing for one argument, this would take precedence over the variadic-based ones. (Does not scale or generalize)

template<typename T> auto foo(T&& args) {}
//or
template<typename T, typename Arg> auto foo(Arg&& arg) {}

Variant 2

Disable the overload when the first argument of the non-empty parameter pack is same as the given type T.

template<typename T, typename... Args>
constexpr bool is_valid() {
    if constexpr(sizeof...(Args)==0)
        return true;
    else
        return !std::is_same_v<T,std::tuple_element_t<0,std::tuple<Args...> > > ;
}

template<typename T, typename... Args, typename = std::enable_if_t<is_valid<T,Args...>()> >
auto foo(Args&&... args) {}
Eyetooth answered 11/7, 2019 at 21:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.