Home > Mobile >  Multiple parameter pack workarounds
Multiple parameter pack workarounds

Time:11-16

Sometimes you just find yourself wanting to use multiple parameter packs in C .

Consider the following pseudo-code:

template<class...T1, int, class...T2>
struct bar
{
  tuple<vector<T1>...> v1;
  tuple<vector<T2>...> v2;
};

int main()
{
  bar<int, bool, char, 0, long int, char> my_bar;
  return 0;
}

Theoretically a compiler for some language could compile this code, because the int serving as a delimiter between the two parameter packs could allow the compiler to parse the template parameter list successfully and recognize two unique parameter packs. However, this is not supported in C , and this makes sense considering that the technology to do such a thing simply does not exist.

Consider the following pseudo-code:

template<class...T1, class...T2>
struct bar
{
  tuple<vector<T1>...> v1;
  tuple<vector<T2>...> v2;
  constexpr bar(const tuple<T1...>, const tuple<T2...>) { };
};

int main()
{
  bar my_bar(tuple<int, bool, char>(), tuple<long int, char>());
  return 0;
}

Theoretically a compiler for some language could compile this code, because the parameter packs could be automatically deduced based on the arguments passed to the constructor. A compiler error could be thrown if the object is constructed in such a way so as to make it impossible to deduce the template arguments. However, this is not supported in C , and this makes sense considering that the technology to do such a thing simply does not exist.

Here is a workaround. Consider the following valid C code:

template<class...T1>
struct bar
{
  template<class...T2>
  struct dummy
  {
    tuple<vector<T1>...> v1;
    tuple<vector<T2>...> v2;
  };
};

int main()
{
  bar<int, bool, char>::dummy<long int, char> my_bar;
  return 0;
}

This works as a way to separate the parameter packs, but it's just not elegant. Who wants to write "dummy" in the middle of their code?

Are there any other alternatives to multiple parameter packs that you know of?

CodePudding user response:

I have come up with another alternative. Consider the following utility code:

namespace util
{
  namespace
  {
    template<template<class...> class T1, template<class...> class T2, class...T3>
    T1<T2<T3>...> foo(std::tuple<T3...>);
  }

  template<template<class...> class T1, template<class...> class T2, class T3>
  using composition = decltype(foo<T1, T2>(T3()));
};

The way it works is by using a private dummy function that never gets called, foo. The entire purpose of that function is to automatically deduce the underlying parameter pack inside of a tuple type. The type computation using this parameter pack is written in the return type of foo. Then we have composition, which deduces the return type of foo when foo is passed a parameter whose type is presumed to be a specialization of std::tuple. We also pass a few template arguments to foo which are required for the type computation, but leave out the ones which need to be automatically deduced.

Usage of composition: Let T1 be a variadic template. Let T2 be a single-argument template. Let T3 be a specialization of std::tuple which only serves as a wrapper for a parameter pack. The result is that util::composition<T1, T2, std::tuple<int, char, bool>> is the same thing as T1<T2<int>, T2<char>, T2<bool>>. Essentially composition "composes" a T1 out of a bunch of T2s, each T2 made from a type in T3.

This allows me to do some cool things. Consider the following code:

template<class T1, class T2>
struct bar
{
  util::composition<tuple, vector, T1> v1;
  util::composition<tuple, vector, T2> v2;
};

int main()
{
  bar<tuple<int, bool, char>, tuple<long int, char>> my_bar;
  return 0;
}

In the main function, I separate the parameter packs by nesting each one inside a tuple. Then inside my class template bar, I use composition to create tuples of vectors.


Here is the full code if you would like to test it.

#include <tuple>
#include <vector>
#include <iostream>
using namespace std;

namespace util
{
  namespace
  {
    template<template<class...> class T1, template<class...> class T2, class...T3>
    T1<T2<T3>...> foo(std::tuple<T3...>);
  }

  template<template<class...> class T1, template<class...> class T2, class T3>
  using composition = decltype(foo<T1, T2>(T3()));
};

template<class T1, class T2>
struct bar
{
  util::composition<tuple, vector, T1> v1;
  util::composition<tuple, vector, T2> v2;
};

int main()
{
  bar<tuple<int, bool, char>, tuple<long int, char>> my_bar;

  cout << std::is_same<decltype(my_bar.v1), tuple<vector<int>, vector<bool>, vector<char>>>::value << endl;
  cout << std::is_same<decltype(my_bar.v2), tuple<vector<long int>, vector<char>>>::value << endl;

  return 0;
}

Expected Output:

1
1

CodePudding user response:

You can all that what you expect even deducing the type from the constructor parameters without workarounds and helper classes.

You need two additional steps:

  1. use a partial specialization for your class definition
  2. use user defined deduction guide to get the type of the template parms from the constructor parameters.
// first define the template, we will have 2 parameters for it
template < typename PACK_ONE, typename PACK_TWO >
class bar;

// Specialize it for two tuples with a different parameter pack for each
template< class ... T1, class...T2 >
struct bar< std::tuple< T1...>, std::tuple< T2...>>
{
    std::tuple<std::vector<T1>...> v1; 
    std::tuple<std::vector<T2>...> v2; 
    constexpr bar(const std::tuple<T1...>, const std::tuple<T2...>) { }
};

// use a user defined deduction guide to deduce the type of the class from the constructor parameters
template< class ... T1, class...T2 >
bar( const std::tuple<T1...>, const std::tuple<T2...>) -> bar< std::tuple<T1...>, std::tuple<T2...> >;

int main()
{
    // and simply use the class by giving the parameters as wanted
    bar my_bar(std::tuple<int, bool, char>{}, std::tuple<long int, char>{});
    return 0;
}

Hint: You should use const ref parameters as you have less copies. I left the example as near as possible to your given question.

CodePudding user response:

Just take std::tuple (or custom type-list) as grouping, and use partial specialization:

template <class Pack1, class Pack2> struct bar;

template <class... T1, class... T2>
struct bar<std::tuple<T1...>, std::tuple<T2...>>
{
  std::tuple<std::vector<T1>...> v1;
  std::tuple<std::vector<T2>...> v2;
};

usage would be similar to

bar<std::tuple<int, bool, char>, std::tuple<long int, char>> my_bar;
  • Related