Search code examples
c++templatesvariadic-templates

C++ − Can a single parameter pack be expanded more than once in a single expression?


I have a function which takes 3 template parameters : two types and an integer constant (used for Eigen storage requirements). It looks like the following template :

template <typename VertexType, typename IndexType, Eigen::StorageOptions Options>
void my_big_function(/* 3 Eigen parameters depending on template args... */) {
    /* Do stuff with matrices... */
}

And since this function needs to be bound to python with multiple template "triplets", I thought this could be a nice use of template parameter packs.

I want to achieve the following :

// binds to python :
// - my_big_function<float, int, ColMajor>(...)
bind_my_big_function<float, int, Eigen::ColMajor>(m); // (1)
// binds to python :
// - my_big_function<float, int, ColMajor>(...)
// - my_big_function<float, int, RowMajor>(...)
// - my_big_function<double, int, RowMajor>(...)
bind_my_big_function<float, int, Eigen::ColMajor, float, int, Eigen::RowMajor, double, int, Eigen::RowMajor>(m); // (2)

Where bind_my_big_function is simple enough :

template <typename MatVertexType, typename MatIndexType, Eigen::StorageOptions MatOptions, typename...RemainingArgs>
void bind_my_big_function(pybind11::module_& m) {
    /* bind function taking MatVertexType, MatIndexType & MatOptions and go onto binding other variants of my_big_function */
}

And while I can definitely produce some code that handles case (1), I cannot figure out how to write code that handles case (2). The closest I got was something like this :

template <typename MatVertexType, typename MatIndexType, Eigen::StorageOptions MatOptions>
void bind_my_big_function(pybind11::module_& m) {
    m.def("my_big_function_in_python", &my_big_function<MatVertexType, MatIndexType, MatOptions>, /* python arguments...*/);
}

template <
    typename MatVertexType, typename MatIndexType, Eigen::StorageOptions MatOptions,
    typename...RemainingArgs, typename = typename std::enable_if_t<sizeof...(RemainingArgs) != 0>
>
void bind_my_big_function(pybind11::module_& m) {
    m.def("my_big_function_in_python", &my_big_function<MatVertexType, MatIndexType, MatOptions>, /* python arguments...*/);
    bind_my_big_function<RemainingArgs...>(m);
}

Are parameter packs supposed to be used in this way ? Only examples I could find of their use was either :

  • with templated function arguments,
  • with a single pack expansion "usage" at a time (the called function required only one type and a variadic template pack)

Solution

  • You can't (yet) capture mixed type (MatVertexType, MatIndexType) and non-type (MatOptions) template parameters in a single pack, since as of C++23 a pack has to be of a single kind: type (typename/class), template, or value (concrete or auto).

    There are a couple of possible solutions:

    1. Lift the non-type template parameter to type space, using e.g. std::integral_constant<Eigen::StorageOption>, so that it can be passed inside a type pack alongside the type parameters;
    2. Combine each triple of related parameters into a single type e.g. template<class MatVertexType, class MatIndexType, Eigen::StorageOption MatOptions> struct MatParameters; (this is the AOS option);
    3. (as in your solution https://stackoverflow.com/a/77488183/567292) Pass three homogeneous packs, using e.g. std::tuple for type packs and std::integer_sequence (in type space) or std::array (in value space) for value packs (this is the SOA variant of option 2);
    4. Wait for "Universal Template Parameters", which has been proposed for C++26.