Search code examples
c++templatesc++17variadic-templatesgeneric-lambda

Why doesn't raw curly constructor {} return an rvalue?


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.


Solution

  • 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));
    }