Lets say you have a variadic class with a std::tuple
, that can be move constructed with args + 1 new arg. When constructed using std::apply()
and a raw curly brace constructor, that constructor doesn't return an rvalue. Which means the class isn't move constructed. An example follows to clarify.
#include <cstdio>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <vector>
template <class... Args>
struct icecream {
icecream() = default;
template <class... MoreArgs>
icecream(icecream<MoreArgs...>&& ice) {
std::apply(
[this](auto&&... ds) {
data = { std::move(ds)..., {} };
},
std::move(ice.data));
}
// This works :
// template <class... MoreArgs>
// icecream(icecream<MoreArgs...>&& ice) {
// std::apply(
// [this](auto&&... ds) {
// data = { std::move(ds)...,
// std::move(std::vector<double>{}) };
// },
// std::move(ice.data));
// }
std::tuple<std::vector<Args>...> data{};
};
int main(int, char**) {
icecream<int> miam;
std::get<0>(miam.data).push_back(1);
std::get<0>(miam.data).push_back(2);
icecream<int, double> cherry_garcia{ std::move(miam) };
printf("miam : \n");
for (const auto& x : std::get<0>(miam.data)) {
printf("%d\n", x);
}
printf("\ncherry_garcia : \n");
for (const auto& x : std::get<0>(cherry_garcia.data)) {
printf("%d\n", x);
}
return 0;
}
The output is :
miam :
1
2
cherry_garcia :
1
2
The example is a little dumbed down, but illustrates the point. In the first move constructor, {}
is used and the tuple copy constructs. If you uncomment the second constructor with a hardcoded std::move()
, then it works.
I test on VS latest, clang latest and gcc latest. All have the same result. (wandbox : https://wandbox.org/permlink/IQqqlLcmeyOzsJHC )
So the question is, why not return an rvalue? I'm obviously missing something with the curly constructor. This might have nothing to do with the variadic stuff, but I thought I might as well show the real scenario.
Why doesn't raw curly constructor {} return an rvalue?
The problem is another.
The problem is that
data = { std::move(ds)..., {} };
call the "direct constructor" (constructor (2) in this page),
constexpr tuple( const Types&... args ); (2)
not the "converting constructor" (constructor (3))
template< class... UTypes > constexpr tuple( UTypes&&... args ); (3)
that you expect.
The problem is that "{}
" isn't enough, for the compiler, to deduce a type (the last type for UTypes...
list in constructor (3)) so the constructor (3) is excluded and the compiler choose the constructor (2).
Whit constructor (2), "{}
" is acceptable to construct an object of the last type of the Types...
of the list because the Types...
is know and not to be deduced.
But constructor (2) is a copy constructor (from the point of view of the Types...
of the tuple), not a forward constructor as constructor (3), so the first vector is copied, not moved.
It's different when you call
data = { std::move(ds)..., std::move(std::vector<double>{}) };
or also
data = { std::move(ds)..., std::vector<double>{} };
because the last argument can be clearly deduced as std::vector<double>{} &&
so the compiler call the "converting constructor" (constructor (3)) and move the content of the first vector.
Off Topic: instead of using std::vector<double>{}
, that works only when double
is the last of the types in Args...
, I suggest to write a more generic code using std::tuple_element
.
Moreover, I suggest to SFINAE enable your constructor only when sizeof...(MoreArgs)+1u == sizeof...(Args)
.
Maybe also std::forward()
(enabling perfect forwarding) instead of std::move()
inside the lambda.
So I suggest the following constructor
template <typename ... MoreArgs,
std::enable_if_t<sizeof...(MoreArgs)+1u == sizeof...(Args)> * = nullptr>
icecream(icecream<MoreArgs...>&& ice) {
std::apply(
[this](auto && ... ds) {
data = { std::forward<decltype(ds)>(ds)...,
std::tuple_element_t<sizeof...(Args)-1u,
decltype(data)>{} };
},
std::move(ice.data));
}