1

So I was trying to come up with a way to split a given parameter pack args... into two separate packs args1... and args2... (at some specified index, 3 in this case). I also wanted to minimize the overhead as much as possible (I rather want to avoid solutions involving the instantiation of tuples and std::apply if that would even help).

Say sizeof...(Ts) == N+M. The solution I came up with was to first create an index sequence of length N, allowing us to have access to a pack I1... of indices 0, 1, ..., N-1. I also put the Ts... into a tuple type and aliased it with tuple_t. Then using a second lambda, I can make use of the index sequence by putting std::tuple_element_t<I1, tuple_t>&&... args1 as the first arguments, which forces the first N elements of args... to match with args1... and the remainder with args2.... From here I can now access args1..., args2... and Ts2.... Lastly I wrapped things into a third lambda in order to also be able to access Ts1....

#include <iostream>

template <typename... Ts>
constexpr void test(Ts&&... args) {}

template <typename... Ts>
void split_pack(Ts&&... args) {
    using tuple_t = std::tuple<Ts...>;
    [&]<size_t... I1>(std::index_sequence<I1...>) {
        [&]<typename... Ts2>(std::tuple_element_t<I1, tuple_t>&&... args1, Ts2&&... args2) {
            std::cout << "hi\n"; // why does MSVC not execute this?? But if I remove the call to test it does???
            [&]<typename... Ts1>()
            {
                // now I have separate access to args1... and args2..., also Ts1... and Ts2...
                test(std::forward<Ts1>(args1)...); // try doing something with args1...
            }.template operator()<std::tuple_element_t<I1, tuple_t>...>();
        }(std::forward<Ts>(args)...);
    }(std::make_index_sequence<3>{});
}

int main() { split_pack(1, 3.0f, false, 5.0, 'a'); }

Now the problem I am facing is that while it compiles and runs just fine using Clang, it fails to compile using GCC and gives an unexpected result using MSVC.

GCC gives the following error:

<source>: In instantiation of 'split_pack<int, float, bool, double, char>(int&&, float&&, bool&&, double&&, char&&)::<lambda(std::index_sequence<_Inds ...>)>::<lambda(std::tuple_element_t<0, std::tuple<int, float, bool, double, char> >&&, std::tuple_element_t<1, std::tuple<int, float, bool, double, char> >&&, std::tuple_element_t<2, std::tuple<int, float, bool, double, char> >&&, Ts2&& ...)> [with Ts2 = {double, char}; std::tuple_element_t<0, std::tuple<int, float, bool, double, char> > = int; std::tuple_element_t<1, std::tuple<int, float, bool, double, char> > = float; std::tuple_element_t<2, std::tuple<int, float, bool, double, char> > = bool]':
<source>:10:9:   required from 'split_pack<int, float, bool, double, char>(int&&, float&&, bool&&, double&&, char&&)::<lambda(std::index_sequence<_Inds ...>)> [with long unsigned int ...I1 = {0, 1, 2}; std::index_sequence<_Inds ...> = std::integer_sequence<long unsigned int, 0, 1, 2>]'
   10 |         [&]<typename... Ts2>(std::tuple_element_t<I1, tuple_t>&&... args1,
      |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   11 |                              Ts2&&... args2) {
      |                              ~~~~~~~~~~~~~~~~~
   12 |             std::cout << "hi\n";  // why does MSVC not execute this?? But if I
      |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   13 |                                   // remove the call to test it does???
      |                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   14 |             [&]<typename... Ts1>() {
      |             ~~~~~~~~~~~~~~~~~~~~~~~~
   15 |                 // now I have separate access to args1... and args2..., also
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   16 |                 // Ts1... and Ts2...
      |                 ~~~~~~~~~~~~~~~~~~~~
   17 |                 test(std::forward<Ts1>(
      |                 ~~~~~~~~~~~~~~~~~~~~~~~
   18 |                     args1)...);  // try doing something with args1...
      |                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   19 |             }.template operator()<std::tuple_element_t<I1, tuple_t>...>();
      |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   20 |         }(std::forward<Ts>(args)...);
      |         ~
<source>:21:6:   required from 'void split_pack(Ts&& ...) [with Ts = {int, float, bool, double, char}]'
    9 |     [&]<size_t... I1>(std::index_sequence<I1...>) {
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   10 |         [&]<typename... Ts2>(std::tuple_element_t<I1, tuple_t>&&... args1,
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   11 |                              Ts2&&... args2) {
      |                              ~~~~~~~~~~~~~~~~~
   12 |             std::cout << "hi\n";  // why does MSVC not execute this?? But if I
      |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   13 |                                   // remove the call to test it does???
      |                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   14 |             [&]<typename... Ts1>() {
      |             ~~~~~~~~~~~~~~~~~~~~~~~~
   15 |                 // now I have separate access to args1... and args2..., also
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   16 |                 // Ts1... and Ts2...
      |                 ~~~~~~~~~~~~~~~~~~~~
   17 |                 test(std::forward<Ts1>(
      |                 ~~~~~~~~~~~~~~~~~~~~~~~
   18 |                     args1)...);  // try doing something with args1...
      |                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   19 |             }.template operator()<std::tuple_element_t<I1, tuple_t>...>();
      |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   20 |         }(std::forward<Ts>(args)...);
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   21 |     }(std::make_index_sequence<3>{});
      |     ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:24:24:   required from here
   24 | int main() { split_pack(1, 3.0f, false, 5.0, 'a'); }
      |              ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:17:21: error: 'args1#0' is not captured
   17 |                 test(std::forward<Ts1>(
      |                 ~~~~^~~~~~~~~~~~~~~~~~~
   18 |                     args1)...);  // try doing something with args1...
      |                     ~~~~~~~~~~
<source>:17:21: note: 'std::tuple_element_t<0, std::tuple<int, float, bool, double, char> >& args1#0' declared here
<source>:17:21: error: 'args1#1' is not captured
<source>:17:21: note: 'std::tuple_element_t<1, std::tuple<int, float, bool, double, char> >& args1#1' declared here
<source>:17:21: error: 'args1#2' is not captured
<source>:17:21: note: 'std::tuple_element_t<2, std::tuple<int, float, bool, double, char> >& args1#2' declared here
Compiler returned: 1

It tells me that args1... is not captured, but clearly I used implicit captures & everywhere?

Trying to run it on MSVC, it does not want to execute the std::cout statement for some reason, but if I remove the call to test, it now executes the std::cout just fine?

So I am wondering if there are any mistakes in this code, or if this somehow triggered two different compiler bugs in two different compilers.

9
  • First step is to make it simpler! Try to separate every lambda so they are defined separately and initialize a variable (e.g. auto lambda1 = []() { ... }). If that doesn't help rewrite them as actual functions. Call the functions one by one, storing their result in a variable, passed to the next function. Etc. Worst case, throw away what you have and rewrite it, without lambdas and templates and functions. Write and build one single line at a time, as simple and straightforward as possible. Commented Jul 18 at 15:08
  • What I find strange is that packs are about types, and you seem to want to use variables. What do you want split_pack to return? Two tuples with the values assigned to them, or do you want a template programming technique just to split into to tuples? Commented Jul 18 at 15:58
  • Maybe this can help you on your way : godbolt.org/z/7h7fs9s3P Commented Jul 18 at 16:03
  • You seems to over complicate things, You might create from the start the 2 index_sequence Demo Commented Jul 18 at 16:03
  • Notice also that std::tuple_element_t<I1, tuple_t>&& is no longer a forwarding-reference, but a r-value-reference. Commented Jul 18 at 16:07

2 Answers 2

3

Not sure why g++ doesn't accept your code, but... if you need the inner lambda to get the Ts1... variadic list of types for the first group of arguments, you can avoid it.

If you write the mid lambda accepting two variadic list of parameters...

[&]<typename ... Ts1, typename... Ts2>(Ts1 &&... args1, Ts2&&... args2) 

... you can explicit the types of the first list, with template operator<>, and the types of the second list are deduced from the arguments

.template operator()<std::tuple_element_t<I1, tuple_t>...>(std::forward<Ts>(args)...);

The following is a full compiling and working (clang++ and g++; and also MSVC (the OP confirm it)) example

#include <tuple>
#include <cstddef>
#include <utility>
#include <iostream>

template <typename... Ts>
constexpr void test (Ts && ... args)
{ ((std::cout << std::forward<Ts>(args) << std::endl), ...); }

template <std::size_t Dim1, typename... Ts>
void split_pack (Ts && ... args) {
  using tuple_t = std::tuple<Ts...>;

  [&]<std::size_t... I1> (std::index_sequence<I1...>) {

    [&]<typename... Ts1, typename... Ts2> (Ts1 && ... args1, Ts2 && ... args2) {

      std::cout << "hi" << std::endl;
      std::cout << "sizeof...(Ts1) = " << sizeof...(Ts1) << std::endl;
      std::cout << "sizeof...(Ts2) = " << sizeof...(Ts2) << std::endl;
      std::cout << "Test for Ts1 arguments" << std::endl;

      test(std::forward<Ts1>(args1)...);

      std::cout << "Test for Ts2 arguments" << std::endl;

      test(std::forward<Ts2>(args2)...);

    }.template operator()<std::tuple_element_t<I1, tuple_t>...>(std::forward<Ts>(args)...);

  }(std::make_index_sequence<Dim1>{});
}

int main()
{
  split_pack<3u>(1, 3.0f, false, 5.0, 'a');
}

Demo

Sign up to request clarification or add additional context in comments.

3 Comments

oh very nice, this does indeed work on MSVC too
can I ask why it even works? Taking a simple example cpp template <typename... Ts1, typename... Ts2> void test(Ts1... args1, Ts2... args2) {} and calling it with test<int>(1, 2.0f), why does it know that the int belongs to Ts1... and float to Ts2...? Couldn't it also have picked Ts1... to be both int and float? Or Ts2... to be both and Ts1... to be empty? It seems a bit ambiguous to me, unless there is some specific rule in the standard that demands it to behave that way, that explicitely specified types match with the first pack and the rest with the second
Good question. How it's works... the Ts2... parameter pack can be deduced from the arguments because it's the trailing parameter pack (there is a rule for it). Another rule say that Ts1..., that isn't in last position (so is in "non-deduced context") can't be deduced from the arguments. But given an explicit list of template arguments, you can explicit the Ts1... list (another specific rule). But I'm not a language layer and trying to extract the exact rules from the standard I got some doubts about some edge cases. I add one or more question for language layers to understand bett
0

It is possible to generalize the split process by allowing to split the input pack into N smaller packs. For instance, one could have the following API

auto main() -> int {

    auto fct = [] <std::size_t I> (auto&&...args) {
        std::cout << "[" << I << "] " ; ((std::cout << args << " "),...); std::cout << "\n";
    };

    split_pack <1,3,5> (fct, "a", 'b','c', 3.14, 1,2,3);
}

where one provides as NTTP the indexes where the pack has to be split. The provided functor will be called four times in this example with the possible output

[0] a 
[1] b c 
[2] 3.14 1 
[3] 2 3 

The function split_pack can be defined this way

template<std::size_t...IdxSeq>
void split_pack (auto fct, auto&&...args)
{
    // we will access the input arguments through a tuple
    auto t = std::forward_as_tuple(std::forward<decltype(args)>(args)...);

    // we define a lambda that calls the provided functor with one specific sub pack
    auto inner = [&] <std::size_t Idx, std::size_t Begin, std::size_t End>  {
        [&]<std::size_t... Ks>(std::index_sequence<Ks...>)  {
            fct.template operator() <Idx> (std::get<Begin+Ks>(std::forward<decltype(t)>(t))...);
        }(std::make_index_sequence<End-Begin>{});
    };

    // we call 'inner' with the begin/end indexes for each possible pack.
    // we also provide Idx as the index of the current pack
    [&]<std::size_t...Idx, std::size_t...Begin, std::size_t...End>(
        std::index_sequence<Idx...>,
        std::integer_sequence<std::size_t, Begin...>,
        std::integer_sequence<std::size_t, End...>
    )  {
         (inner.template operator()<Idx,Begin,End>(), ...);
    } (
        std::make_index_sequence <1+sizeof...(IdxSeq)>{},
        std::integer_sequence<std::size_t, 0, IdxSeq...>{},
        std::integer_sequence<std::size_t, IdxSeq..., sizeof...(args)>{}
    );
}

Note how 3 integer sequences are used:

  1. one for the index of the current sub pack being processed.
  2. the beginning index in the input pack
  3. the ending index in the input pack

Demo

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.