Data member and rvalue life-time

194 views Asked by At

Somehow inspired by the expression template code in Expression templates and C++11, written by Paul Preney, I decided to test the following:

template<typename T>
struct X
{
    X(T t) : t(std::forward<T>(t)) {}

    T t;
};

template<typename T>
auto CreateX(T&& t) -> X<decltype(std::forward<T>(t))>
{
    return X<decltype(std::forward<T>(t))>(std::forward<T>(t));
}

Then, I used it to produce an instance of X<const vector<int>&> and X<vector<int>&&> as follows:

int main()
{
    int vec = {1,2,3,4};

    auto x1 = CreateX(vec);
    auto x2 = CreateX(vector<int>{5,6,7,8});

    cout << "x1: "; for(auto x : x1.t) cout << x << " "; cout << endl;
    cout << "x2: "; for(auto x : x2.t) cout << x << " "; cout << endl;
}

The output is:

x1: 1 2 3 4 
x2: 0 0 33 0 0 0 7 8 

which shows that the life-time of the temporary vector<int>{5,6,7,8} is not being extended, and the rvalue-reference member X::t binds to something else.

Okay, from this answer What is the lifetime of the class data member which const reference to a rvalue?, I know this is the expected behaviour.

However, the question here is: what is different in Paul Preney' code in Expression templates and C++11 that permits the temporary vectors to exist as long as the rvalue-references members exist? See his Case 2, where temporaries are created.

Apparently, the same construct there is used here, but I am probably missing something.


Edit: Based on the answer of R. Martinho Fernandes below, I tried the following:

int main()
{
    using namespace std;

    auto expr = math_vector<3>{1.0, 1.1, 1.2} + math_vector<3>{2.0, 2.1, 2.2};

    cout << "vec1: "; for(int i = 0; i < 3; ++i) cout << expr.le()[i] << " "; cout << endl;
    cout << "vec2: "; for(int i = 0; i < 3; ++i) cout << expr.re()[i] << " "; cout << endl;
}

and it turns out that this is a valid code that outputs:

vec1: 1.0 1.1 1.2
vec2: 2.0 2.1 2.2

Therefore, apparently references stored in the expression template is not dangling. What is going on here?

2

There are 2 answers

2
R. Martinho Fernandes On BEST ANSWER

what is different in Paul Preney' code in Expression templates and C++11 that permits the temporary vectors to exist as long as the rvalue-references members exist?

Nothing permits such a thing.

The temporary vectors there exist until the end of the full expression, as any other temporary that isn't bound to a local reference variable. That is enough in Paul's code because the code immediately materialises the expression tree into an actual math_vector, and the temporaries are not needed anymore after that.

Paul's code does not store any expression template node (math_vector_expr) anywhere, while your code stores one (X) as x2. That's a known issue with auto: it does the wrong thing when you are using expression templates because it results in storing the expression tree, which is likely to contain references that will become dangling right away.

In order to make it totally clear, the following is fine.

math_vector<3> result =
    math_vector<3>{1.0, 1.1, 1.2} +
    math_vector<3>{2.0, 2.1, 2.2} +
    math_vector<3>{3.0, 3.1, 3.2} +
    math_vector<3>{4.0, 4.1, 4.2}
; // no references are held to any temporaries past this point

The following is not fine.

math_vector_expr<3> result =        // or auto
    math_vector<3>{1.0, 1.1, 1.2} +
    math_vector<3>{2.0, 2.1, 2.2} +
    math_vector<3>{3.0, 3.1, 3.2} +
    math_vector<3>{4.0, 4.1, 4.2}
; // result now holds references to those temporaries
0
Casey On

The problem - as R. Martinho Fernandes states in his answer - is that you capture references to rvalues in your "expression tree" which outlive their referents. The solution is to only store references to lvalues, and to capture objects passed by rvalue reference directly. In other words, store their values in the expression tree instead of by reference (Live code at Coliru):

template<typename T>
struct X
{
    X(T t) : t(std::move(t)) {}

    T t;
};

// Capture lvalue references
template<typename T>
X<const T&> CreateX(const T& t)
{
    return X<const T&>(t);
}

// Copy rvalue references
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value,X<T>>::type
CreateX(T&& t)
{
    return X<T>(std::move(t));
}

If the user passes you an lvalue reference, it is her responsibility to ensure that the referent outlives the expression tree object.