Template deduction, is this conforming?

Viewed 130

It seems like this code is not correct as I get compiler errors for it. I'm trying to understand why:

template <class ... Ts>
struct type_list{};

template <class ... Ts, class T_, class ... Ts_>
auto foo(type_list<Ts...>, Ts&&..., T_&&t, Ts_&&...) {
    return sizeof(T_);
}

int main() {
  std::cerr << foo(type_list<int>{}, 5, 5.0, 3);
  return 0;
}

clang produces the follow error:

example.cpp:16:16: error: no matching function for call to 'foo'
  std::cerr << foo(type_list<int>{}, 5, 5.0, 3);
               ^~~
example.cpp:11:6: note: candidate template ignored: deduced conflicting types for parameter 'Ts'
      (<int> vs. <>)
auto foo(type_list<Ts...>, Ts&&..., T_&&t, Ts_&&...) {

It seems to me that in my call Ts should be deduced to int (since that's the only way to make the first argument work out), and then everything else will be forced as a result. Why doesn't this work?

2 Answers

Clang deduces Ts from two conflicting sources: type_list<Ts...> and Ts&&.... Because you call foo with type_list<int>{}, Ts is first deduced as {int}. However, because the last argument Ts_&&... has the type of a parameter pack, it catches all that it can; in this case, the last two arguments (while T_&&t is passed the 5), which deduces Ts_ as {double, int} (and T_ as int). This leaves Ts with no argument, so it's deduced as {}.

Hence Clang's error message: Ts is deduced both as {} and {int} (the (<int> vs. <>) in the error message).

Note that GCC does things differently:

error: too few arguments to function 'auto foo(type_list, Ts&& ..., T_&&, Ts_&& ...) [with Ts = {int}; T_ = int; Ts_ = {double, int}]'

It tries to keep Ts deduced as {int} but still deduces Ts_ as {double, int}, and so there is no way to give the correct number of arguments.

To explain a bit further, consider the following:

template <class ... Ts>
struct type_list{};

template <class ... Ts>
auto foo(type_list<Ts...>, Ts...) {
    return sizeof...(Ts);
}

int main() {
  std::cerr << foo(type_list<int>{}, 5, 5.0, 3);
}

Clang correctly calls this out because the two deductions are conflicting:

note: candidate template ignored: deduced conflicting types for parameter 'Ts' (<int> vs. <int, double, int>)

Trouble is, you can't catch 2 of the arguments with another parameter pack, because it will either catch everything if placed after (which is your case), or nothing if placed before:

template <class ... Ts>
struct type_list{};

template <class... Ts, class... Ts_>
auto foo(type_list<Ts...>, Ts_..., Ts...) {
    return sizeof...(Ts);
}

int main() {
  std::cerr << foo(type_list<int>{}, 5, 5.0, 3);
}

Here, Clang produces the same error message because Ts is still deduced as both {int} from the type_list, and {int, double, int} by catching all the remaining arguments.

I think your only choices are to somehow deal with tuples or be more explicit with the call: foo<int>(type_list<int>{}, 5, 5.0, 3) (and you can probably remove the type_list in this case). That way, Ts isn't deduced anymore: you explicitly make it {int}.

C++ doesn't try every possibility and find the only one that is legal. It follows very specific rules, and if those rules don't work, it generates an error.

In general, a deduced ... pack must be last if it is deduced. Attempts to work around it will fail.

Here is a solution that avoids your problem:

template <class ... Ts, class...Us>
auto foo(type_list<Ts...>, Us&&...us) {
    return [](Ts&&...ts, auto&& t, auto&&...) {
        return sizeof(t);
    }(std::forward<Us>(us)...);
}

int main() {
  std::cerr << foo(type_list<int>{}, 5, 5.0, 3);
  return 0;
}
Related