Eliding copy/move when taking members out of a temporary

Viewed 262

I'd like to take out members of a temporary without unnecessary moving or copying.

Suppose I have:

class TP {
    T _t1, _t2;
};

I'd like to get _t1, and _t2 from TP(). Is it possible without copying/moving members?

I've tried with tuples and trying to "forward" (I don't think it's possible) the members, but the best I could get was a move, or members dying immediately.

In the following playground using B::as_tuple2 ends up with members dying too soon, unless the result is bound to a non-ref type, then members are moved. B::as_tuple simply moves is safe with auto on client side.

I suppose this should be technically possible, since the temporary dies immediately, and the member do die while they could bound to variables on the calling site (Am I wrong?), and structured binding of a similar struct works as intended.

Is it possible to extend/pass life of the member onto an outside variable, or elide the move/copy? I need it with c++14 version, but I couldn't get it to work on c++17 either, so I am interested in both.

Playground:

#include <tuple>
#include <iostream>

using std::cout;
class Shawty {
/**
 * Pronounced shouty.
 **/
    public:
    Shawty() : _id(Shawty::id++) {cout << _id << " ctor\n"; }
    Shawty(Shawty && s) : _id(Shawty::id++) { cout << _id << " moved from " << s._id << "\n"; }
    Shawty(const Shawty & s) : _id(Shawty::id++) { cout << _id << " copied from " << s._id << "\n"; }
    Shawty& operator=(Shawty && s) { cout << _id << " =moved from " << s._id << "\n"; return *this;}
    Shawty& operator=(Shawty & s) { cout << _id << " =copied from " << s._id << "\n"; return *this;}
    ~Shawty() {cout << _id << " dtor\n"; }
    int _id;
    static int id;
};
int Shawty::id = 0;

class B {
public:
    auto as_tuple() && {return std::make_tuple(std::move(_s1), std::move(_s2));}
    auto as_tuple2() && {return std::forward_as_tuple(std::move(_s1), std::move(_s2));}

private:
    Shawty _s1, _s2;
};

struct S {
    Shawty _s1, _s2;
};

int main() {
    std::cout << "----------\n";
    auto [s1, s2] = B().as_tuple2();
    std::cout << "---------\n";
    auto tpl1 = B().as_tuple2();
    std::cout << "----------\n";
    std::tuple<Shawty, Shawty> tpl2 = B().as_tuple2();
    std::cout << "----------\n";

    std::cout << std::get<0>(tpl1)._id << '\n';
    std::cout << std::get<1>(tpl1)._id << '\n';
    std::cout << std::get<0>(tpl2)._id << '\n';
    std::cout << std::get<1>(tpl2)._id << '\n';
    std::cout << s1._id << '\n';
    std::cout << s2._id << '\n';

    std::cout << "--struct--\n";
    auto [s3, s4] = S{};
    std::cout << s3._id << '\n';
    std::cout << s4._id << '\n';
    std::cout << "----------\n";

    return 0;
}
2 Answers

No. It is not possible to extend the lifetime of more than one member beyond the lifetime of the super object.

So, the only way to "get" members without copying is to keep the super object alive, and refer to them:

// member function
auto as_tuple3() & {
    return std::make_tuple(std::ref(_s1), std::ref(_s2));
}

// usage
B b;
auto [s1, s2] = b.as_tuple3();

An example of extending lifetime of the object by binding a reference to a single member. Note that this requires the member to be accessible from where the reference is bound (not the case in your example, where the member is private):

auto&& s1 = B{}._s1;

Add support for structured binding to your B type.

class B {
public:
    template<std::size_t I, class Self,
        std::enable_if_t< std::is_same_v<B, std::decay_t<Self>>, bool> = true
    >
    friend constexpr decltype(auto) get(Self&& self) {
        if constexpr(I==0)
        {
            using R = decltype(std::forward<Self>(self)._s1)&&;
            return (R)std::forward<Self>(self)._s1;
        }
        else if constexpr(I==1)
        {
            using R = decltype(std::forward<Self>(self)._s2)&&;
            return (R)std::forward<Self>(self)._s2;
        }
    }
private:
    Shawty _s1, _s2;
};

namespace std {
    template<>
    struct tuple_size<::B>:std::integral_constant<std::size_t, 2> {};
    template<std::size_t N>
    struct tuple_element<N, ::B>{using type=Shawty;};
}

Test code:

int main() {
    std::cout << "----------\n";
    {
        auto&& [s1, s2] = B();
    }
}

output:

 ----------
 0 ctor
 1 ctor
 1 dtor
 0 dtor

Live example.

This is the best I can do. Note that s1 and s2 are references into a lifetime-extended version of B.

Related