Reputation: 1352
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
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
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