c++language-lawyerobject-lifetimeconst-cast

Lifetime extension of temporary by non-const reference using const-cast


This is something that came up recently and which I feel shouldn't work as it apparently does:

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int>& ptr = const_cast<std::shared_ptr<int>&>(
        static_cast<const std::shared_ptr<int>&>(
            std::shared_ptr<int>(
                new int(5), [](int* p) {std::cout << "Deleting!"; *p = 999;  delete(p); }
            )
        )
    );
    std::cout << "I'm using a non-const ref to a temp! " << *ptr << " ";
}

The use of shared_ptr isn't necessary here, but the custom deleter allows for an easy demonstration of the lifetime of the resulting object. There resulting output from Visual Studio, Clang and GCC is the same:

I'm using a non-const ref to a temp! 5 Deleting!

Meaning the lifetime of the resulting shared_ptr has, through some mechanism, been extended to match that of the std::shared_ptr<int>& ptr.

What's Happening?

Now, I'm aware that the lifetime of a temporary will be extended to that of the reference for the case of a constant reference. But the only named object is a non-const reference, all other intermediate representations I would expect to have a lifetime equal only to the initialization expression.

Additionally, Microsoft have an extension which allows non-const references to extend the lifetime of a bound temporary, but this behaviour appears to be present even when that extension is disabled and, additionally, also appears in Clang and GCC.

According to this answer I believe the temporary is implicitly being created as const, so attempting to modify the object referenced by ptr is probably undefined behaviour, but I'm not sure that knowledge tells me anything about why the lifetime is being extended. My understanding is that it is the act of modifying a const that is UB, not simply taking a non-const reference to it.

My understanding of what should be happening is as follows:

  1. Type() creates a prvalue with no cv-specification.

  2. static_cast<const Type&>(...) materializes that prvalue into a const xvalue with a lifetime equal to the interior expression. We then create a const lvalue reference to that const xvalue. The lifetime of the xvalue is extended to match that of the const lvalue reference.

  3. const_cast<Type&>(...) produces an lvalue reference which is then assigned to ptr. The const lvalue reference then expires, taking the materialized xvalue with it.

  4. I try to read dangling reference ptr and bad things happen.

What's wrong in my understanding? Why don't the bits in italics happen?

As an extra bonus question, am I correct in thinking that the underlying object is const, and that any attempt to modify it through this path will result in undefined behaviour?


Solution

  • Any reference can extend the lifetime of an object. However, a non-const reference cannot bind to a temporary as in your example. The Microsoft extension you refer to is not "Extend lifetime by non-const references," rather "Let non-const references bind to temporaries." They have that extension for backward compatibility with their own previous broken compiler versions.

    By a cast you have forced the binding of a non-const reference to a temporary, which does not appear to be invalid, just unusual because it cannot be done directly. Once you've accomplished that binding, lifetime extension occurs for your non-const reference the same as it would for a const reference.

    More information: Do *non*-const references prolong the lives of temporaries?