11

I don't understand how this code works. Could anyone please enlighten me a bit. I was pretty much sure "the parameter pack should be the last argument"

void foo(auto&&...args1, auto&&... args2, auto&&... args3) {
    std::cout << "args1:\n", ((std::cout << args1 << " "), ...);
    std::cout << "args2:\n", ((std::cout << args2 << " "), ...);
    std::cout << "args3:\n", ((std::cout << args3 << " "), ...);
}

int main(int argc, char** argv)
{
    foo(1,2,3,4,5,6);
}

If it's allowed how can I split arg1, args2 and args3?

The compiler (g++-11) assumes all parameters pack except args3 are empty, so the output is

args1:
args2:
args3:
1 2 3 4 5 6
Dmitry
  • 1,065
  • 7
  • 15
  • 1
    `args1` and `args2` are indeed non deducible. Whereas `args1` could be specified (`foo(1, 1, 3, 3)`), `args2` would always be empty. – Jarod42 Oct 28 '22 at 09:57
  • 1
    I see, still that's kinda weird to me. Does it make any sense to not give a compilation error here? – Dmitry Oct 28 '22 at 10:00
  • Related: https://stackoverflow.com/q/71149128/7325599 – Fedor Oct 28 '22 at 18:01

3 Answers3

10

The program is ill-formed and gcc and clang are wrong in accepting the code. You can also confirm this by slightly modifying your code to as shown below. There is also an old gcc bug for this.

Basically, in case of function templates(in the modifed program shown below) multiple template parameter packs are permitted, as long as each template parameter following a template parameter pack has either a default value or it can be deduced. But since neither of these requirements is satisfied for T2 and T3(as they cannot be unambiguously deduced and cannot have default arguments), the program is ill-formed.

The exact same reasoning applies to your given example as foo(in your original example) is a generic function(aka abbreviated function template).

GCC and Clang shows the same incorrect behavior for the following modified program: Demo

template<typename... T1, typename... T2, typename... T3>
void foo(T1&&...args1, T2&&... args2, T3&&... args3) {
    std::cout << "args1:\n", ((std::cout << args1 << " "), ...);
    std::cout << "args2:\n", ((std::cout << args2 << " "), ...);
    std::cout << "args3:\n", ((std::cout << args3 << " "), ...);
}

int main(int argc, char** argv)
{
    foo(1,2,3,4,5,6); //gcc and clang compiles this while msvc correctly rejects this
}

Here is the gcc bug:

GCC accepts invalid program involving multiple template parameter packs

Here is the clang bug:

Clang accepts invalid program involving multiple template parameter packs


The same can also be seen from temp.param:

A template parameter pack of a function template shall not be followed by another template parameter unless that template parameter can be deduced from the parameter-type-list ([dcl.fct]) of the function template or has a default argument ([temp.deduct]).

// U can be neither deduced from the parameter-type-list nor specified
template<class... T, class... U> void f() { }   // error

(emphasis mine)

Jason
  • 36,170
  • 5
  • 26
  • 60
  • 1
    it's kinda weird to see both having the same bug – phuclv Oct 28 '22 at 14:53
  • 1
    @Actually this is a very old bug in all the three compilers(gcc, clang and msvc). Here is the [old gcc bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69623). Also, note that even msvc compiles the ill-formed code if we remove one template parameter. [Demo](https://godbolt.org/z/xozorPn76). – Jason Oct 28 '22 at 14:55
1

If it's allowed how can I split arg1, args2 and args3?

You can't. What you can do is collect template packs into a single argument, with std::tuple

template <typename... Args1, typename... Args2, typename... Args3>
void foo(std::tuple<Args1...> args1, std::tuple<Args2...> args2, std::tuple<Args3...> args3) {
    auto out = []<std::size_t... Is>(std::string name, auto tup, std::index_sequence<Is...>)
    {
        std::cout << name << " ";
        ((std::cout << get<Is>(tup) << " "), ...);
        std::cout << std::endl;
    };
    
    out("args1", args1, std::index_sequence_for<Args1...>{});
    out("args2", args2, std::index_sequence_for<Args2...>{});
    out("args3", args3, std::index_sequence_for<Args3...>{});
}

See it live

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • [`std::apply`](https://en.cppreference.com/w/cpp/utility/apply) might be used to avoid `index_sequence`: `std::apply([](const auto&...args){((std::cout << args << " "), ...) < – Jarod42 Oct 28 '22 at 10:25
  • Is there a reason you changed the newlines to `std::endl`, inserting a manual flush? – Deduplicator Oct 28 '22 at 18:13
0

I wrote a chapter about this in the book "Embracing Modern C++ Safely" and discuss this and related matters in my talk at https://www.youtube.com/watch?v=va9I2qivBOA.

Andrei Alexandrescu
  • 3,214
  • 19
  • 17
  • 1
    I noted a mistake/typo in one of the slide in [this talk](https://youtu.be/va9I2qivBOA?t=1691). Note, in the above link, the last function call should be `f(1,2,3, 4);` instead of `f(1,2,3, "4");`. I don't know if the same typo is there in the book or not as I've not read the book yet. Note there should not be any double quotes around the last argument `4`. Here is the [demo](https://godbolt.org/z/6fzb767j4) where I've commented the part where the typo is. – Jason Oct 29 '22 at 15:58