c++templateslambdavariadic-functions

Trying to split parameter pack into two smaller packs using an index sequence and nested lambdas leads to weird compiler behaviors


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.


Solution

  • 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