hynner
hynner

Reputation: 1352

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?

Upvotes: 1

Views: 1070

Answers (2)

Casey
Casey

Reputation: 42554

As Yakk says in his answer, there's and "easy" way and a "perfect forwarding" way to do this. They are not identical, but I think he's overcomplicated the situation by fixating on forward_as_tuple and tuple's conversion constructors. To wit (DEMO):

template<typename ... Ts>
class A {
public:
  // "Simple" method. Does not perfect forward.
  // Caller's expressions are copied/moved/converted at the callsite
  // into Ts, and then moved (for non-reference types) or copied
  // (reference types) into the vector.
  void simple_method(Ts... ts) {
    // some code 
    std::vector<std::tuple<Ts...>> vec;
    vec.emplace_back(std::forward<Ts>(ts)...);
    // some other code
  }

  // "Perfect forwarding" method.
  // Caller's expressions are perfectly forwarded into the vector via
  // emplace.
  template <typename...Us>
  void perfect_forwarding_method(Us&&...us) {
    // some code 
    std::vector<std::tuple<Ts...>> vec;
    vec.emplace_back(std::forward<Us>(us)...);
    // some other code
  }

  // Constraint alias.
  template <typename...Us>
  using Constructible = typename std::enable_if<
    std::is_constructible<std::tuple<Ts...>, Us...>::value
  >::type;

  // "Constrained Perfect forwarding" method.
  // Caller's expressions are perfectly forwarded into the vector via
  // emplace. Substitution failure if tuple<Ts...> cannot be constructed
  // from std::forward<Us>(us)...
  template <typename...Us, typename = Constructible<Us...>>
  void constrained_perfect_forwarding_method(Us&&...us) {
    // some code 
    std::vector<std::tuple<Ts...>> vec;
    vec.emplace_back(std::forward<Us>(us)...);
    // some other code
  }
};

All three methods properly handle lvalue/rvalue reference types in Ts.

Upvotes: 2

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275260

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.

Upvotes: 3

Related Questions