How would auto&& extend the life-time of the tempo

2020-03-12 04:06发布

问题:

The code below illustrated my concern:

#include <iostream>


struct O
{
    ~O()
    {
        std::cout << "~O()\n";
    }
};

struct wrapper
{
    O const& val;

    ~wrapper()
    {
        std::cout << "~wrapper()\n";
    }
};

struct wrapperEx // with explicit ctor
{
    O const& val;

    explicit wrapperEx(O const& val)
      : val(val)
    {}

    ~wrapperEx()
    {
        std::cout << "~wrapperEx()\n";
    }
};

template<class T>
T&& f(T&& t)
{
    return std::forward<T>(t);
}


int main()
{
    std::cout << "case 1-----------\n";
    {
        auto&& a = wrapper{O()};
        std::cout << "end-scope\n";
    }
    std::cout << "case 2-----------\n";
    {
        auto a = wrapper{O()};
        std::cout << "end-scope\n";
    }
    std::cout << "case 3-----------\n";
    {
        auto&& a = wrapper{f(O())};
        std::cout << "end-scope\n";
    }
    std::cout << "case Ex-----------\n";
    {
        auto&& a = wrapperEx{O()};
        std::cout << "end-scope\n";
    }
    return 0;
}

See it live here.

It's said that auto&& will extend the life-time of the temporary object, but I can't find the standard words on this rule, at least not in N3690.

The most relevant may be section 12.2.5 about temporary object, but not exactly what I'm looking for.

So, would auto&& life-time extension rule apply to all the temporary objects involved in the expression, or only the final result?

More specific, is a.val guaranteed to be valid (non-dangling) before we reach the end-of-scope in case 1?

Edit: I updated the example to show more cases (3 & Ex).

You'll see that only in case 1 the lifetime of O is extended.

回答1:

In the same way that a reference to const does:

const auto& a = wrapper{O()};

or

const wrapper& a = wrapper{O()};

or also

wrapper&& a = wrapper{O()};

More specific, is a.val guaranteed to be valid (non-dangling) before we reach the end-of-scope in case 1?

Yes, it is.

There's (almost) nothing particularly important about auto here. It's just a place holder for the correct type (wrapper) which is deduced by the compiler. The main point is the fact that the temporary is bound to a reference.

For more details see A Candidate For the “Most Important const” which I quote:

Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself

The article is about C++ 03 but the argument is still valid: a temporary can be bound to a reference to const (but not to a reference to non-const). In C++ 11, a temporary can also be bound to an rvalue reference. In both cases, the lifetime of the temporary is extended to the lifetime of the reference.

The relevant parts of the C++11 Standard are exactly those referred in the OP, that is, 12.2 p4 and p5:

4 - There are two contexts in which temporaries are destroyed at a different point than the end of the full expression. The first context is [...]

5 - The second context is when a reference is bound to a temporary. [...]

(There are some exceptions in the bullet points following these lines.)

Update: (Following texasbruce's comment.)

The reason why the O in case 2 has a short lifespan is that we have auto a = wrapper{O()}; (see, there's no & here) and then the temporary is not bound to a reference. The temporary is, actually, copied into a using the compiler generated copy-constructor. Therefore, the temporary doesn't have its lifetime expanded and dies at the end of the full expression in which it appears.

There's a danger in this particular example because wrapper::val is a reference. The compiler generated copy-constructor of wrapper will bind a.val to the same object that the temporary's val member is bound to. This object is also a temporary but of type O. Then, when this latter temporary dies we see ~O() on the screen and a.val dangles!

Contrast case 2 with this:

std::cout << "case 3-----------\n";
{
    O o;
    auto a = wrapper{o};
    std::cout << "end-scope\n";
}

The output is (when compiled with gcc using option -fno-elide-constructors)

case 3-----------
~wrapper()
end-scope
~wrapper()
~O()

Now the temporary wrapper has its val member bound to o. Notice that o is not a temporary. As I said, a is a copy of the wrapper temporary and a.val also binds to o. Before the scope ends the temporary wrapper dies and we see the first ~wrapper() on the screen.

Then the scope ends and we get end-scope. Now, a and o must be destroyed in the reverse order of construction, hence we see ~wrapper() when a dies and finally ~O() when it's o's time. This shows that a.val doesn't dangle.

(Final remark: I've used -fno-elide-constructors to prevent a optimization related to copy-construction that would complicate the discussion here but this is another story.)