Search code examples
c++c++11variadic-templatesperfect-forwarding

Perfect forwarding class variadic parameters


I have a class with variadic type parameters. Inside that class I have a method that takes arguments of those types, makes a tuple of them and stores them in a vector. What I want is to use perfect forwarding to avoid unnecessary copies. I solved it by prefixing the method with another variadic template and I forward these new types instead of old ones, but I wonder if there is a better way.

Let me show you an example of my code:

template<typename ... Tlist>
class A{
public:
    template<typename ... Xlist>
    void method(Xlist && ... plist){
        // some code 
        std::vector<std::tuple<Tlist...>> vec;
        vec.push_back(std::make_tuple(std::forward<Xlist>(plist)...));
        // some other code
    }

};

This works with correct types and it doesn't compile with incorrect types anyway so I guess it's ok. But what I'd like is to somehow use the Tlist types in method header, something like this:

template<typename ... Tlist>
class A{
public:
    void method(Tlist && ... plist){
        // some code 
        std::vector<std::tuple<Tlist...>> vec;
        vec.push_back(std::make_tuple(std::forward<Tlist>(plist)...));
        // some other code
    }

};

But that only works with rvalues.

So is there a way to avoid using another template while still making perfect forwarding possible?


Solution

  • The easiest way to solve the problem is simply to take a pack of values, and move from them:

    template<class...Ts>
    struct A{
      void method(Ts...ts){
        // some code 
        std::vector<std::tuple<Ts...>> vec;
        vec.emplace_back(std::forward_as_tuple(std::move(ts)...));
        // some other code
      }
    };
    

    the above doesn't behave well if Ts contain references, but neither did your original code. It also forces a redundant move, which for some types is expensive. Finally, if you didn't have a backing vec, it forces your types to be moveable -- the solutions below do not.

    This is by far the simplest solution to your problem, but it doesn't actually perfect forward.


    Here is a more complex solution. We start with a bit of metaprogramming.

    types is a bundle of types:

    template<class...>struct types{using type=types;};
    

    conditional_t is a C++14 alias template to make other code cleaner:

    // not needed in C++14, use `std::conditional_t`
    template<bool b, class lhs, class rhs>
    using conditional_t = typename std::conditional<b,lhs,rhs>::type;
    

    zip_test takes one test template, and two lists of types. It tests each element of lhs against the corresponding element of rhs in turn. If all pass, it is true_type, otherwise false_type. If the lists don't match in length, it fails to compile:

    template<template<class...>class test, class lhs, class rhs>
    struct zip_test; // fail to compile, instead of returning false
    
    template<
      template<class...>class test,
      class L0, class...lhs,
      class R0, class...rhs
    >
    struct zip_test<test, types<L0,lhs...>, types<R0,rhs...>> :
      conditional_t<
        test<L0,R0>{},
        zip_test<test, types<lhs...>, types<rhs...>>,
        std::false_type
      >
    {};
    
    template<template<class...>class test>
    struct zip_test<test, types<>, types<>> :
      std::true_type
    {};
    

    now we use this on your class:

    // also not needed in C++14:
    template<bool b, class T=void>
    using enable_if_t=typename std::enable_if<b,T>::type;
    template<class T>
    using decay_t=typename std::decay<T>::type;
    
    template<class...Ts>
    struct A{
      template<class...Xs>
      enable_if_t<zip_test<
        std::is_same,
        types< decay_t<Xs>... >,
        types< Ts... >
      >{}> method(Xs&&... plist){
        // some code 
        std::vector<std::tuple<Tlist...>> vec;
        vec.emplace_back(
          std::forward_as_tuple(std::forward<Xlist>(plist)...)
        );
        // some other code
      }
    };
    

    which restricts the Xs to be exactly the same as the Ts. Now we probably want something slightly different:

      template<class...Xs>
      enable_if_t<zip_test<
        std::is_convertible,
        types< Xs&&... >,
        types< Ts... >
      >{}> method(Xs&&... plist){
    

    where we test if the incoming arguments can be converted into the data stored.

    I made another change forward_as_tuple instead of make_tuple, and emplace instead of push, both of which are required to make the perfect forwarding go all the way down.

    Apologies for any typos in the above code.

    Note that in C++1z, we can do without zip_test and just have a direct expansion of the test within the enable_if by using fold expressions.

    Maybe we can do the same in C++11 using std::all_of and constexpr initializer_list<bool>, but I haven't tried.

    zip in this context refers to zipping up to lists of the same length, so we pair up elements in order from one to the other.

    A significant downside to this design is that it doesn't support anonymous {} construction of arguments, while the first design does. There are other problems, which are the usual failurs of perfect forwarding.