6

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?

R2RT
  • 2,061
  • 15
  • 25
Steelbadger
  • 135
  • 6
  • if your understanding is correct, then there is UB and the output is just a symptom of UB. Out of all the "bad things" the worst is "appears to work as expected". Anyhow, I hope there can be an answer that does not need the x/p/r-mumbo-jumbo ;) – 463035818_is_not_an_ai Dec 04 '19 at 12:24
  • @formerlyknownas_463035818: I ran the code using UB Sanitizer (also Address Sanitizer) and it did not complain. That doesn't mean there's no UB, but nothing jumps out. – John Zwinck Dec 04 '19 at 12:36
  • @JohnZwinck actually I cannot follow OPs reasoning completely, I dont think there is anything `const` here, but I really have no idea what is actually going on – 463035818_is_not_an_ai Dec 04 '19 at 12:38
  • _What's wrong in my understanding?_ You believe that cast operators somehow "produce" references as if references were objects which are created or destroyed. – Language Lawyer Dec 04 '19 at 14:29
  • See http://eel.is/c++draft/class.temporary#6. Lifetime extension in your code is the correct behavior, because the initializer of the reference is `const_cast` (6.6.1) applied to `static_cast` (6.6.2) which triggered temporary materialization (6.1) – Language Lawyer Dec 04 '19 at 14:57

2 Answers2

2

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?

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • Interesting. So the Microsoft extension is to allow binding of temporaries to non-const references only, rather than changing how lifetime extension itself works. What about the const-ness of the underlying object, is my assumption that it is effectively const correct? – Steelbadger Dec 04 '19 at 13:15
  • Your object is not const. You can call `ptr.reset(...)` for example. – John Zwinck Dec 04 '19 at 13:26
  • @JohnZwinck The object materialized by `static_cast&>` **is** const. – Language Lawyer Dec 04 '19 at 19:45
  • @LanguageLawyer: You have lived up to your name. Yes, the object materialized by the cast you mention is const. But what I would call the "underlying object" (the shared_ptr itself, rather than the reference) is not const, nor is its outermost reference. – John Zwinck Dec 05 '19 at 09:39
  • @JohnZwinck You mean that on a common implementation this object is created in a "writeable" storage so you may modify it as if the object wasn't const. – Language Lawyer Dec 05 '19 at 11:58
0

The linked article is plainly wrong. A temporary is not (necessarily) a const object. The fact that it cannot bind to a non-const refetence is of no importance. It can bind to a rvalue reference with no need for a cast, and be modified through such reference. There is no UB in doing that. One can also call non-const member functions on a temporary. The entire concept of move semantics is based on this fact.

Binding to a normal non-const reference and modifying through it is a different way to accompish the same thing. It requires a cast but otherwise it is very similar to the above.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243