Which Standard Wording Tells Us That Ref-To-Const Temporary Lifetime Extension Only "Works Once"

Which standard wording tells us that ref-to-const temporary lifetime extension only works once ?

This is subject of two issue reports, http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1299 and http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1568 .

The former issue report, of which I am the reporter, was intended to cover all these cases where a reference is bound to a temporary object, but is not intended to be lifetime-extending. The description in the body of the issue only mentions prvalues being confused with temporary expressions (that actually decide whether lifetime of what they evaluate to is lengthened or not). But lvalue and xvalues are likewise confused with these in the Standard. An example where that happens in the context of static_cast is issue number #1568 (in which the use of "temporary variable" further confuses the matter).

Actually, this:

A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.

Contradicts the other rules in the same paragraph. Because the temporary is bound to both a reference parameter in a function call and to a local automatic reference variable.

lifetime extension for function taking parameter by const& and returning by const&

Yes, the lifetime of the temporary is not extended further more; after the full expression the reference v becomes dangled.

std::string const& v = get_string(std::string{"Hello"});
// v becomes dangled now

My understanding is that the temporary is bound to the lifetime of p and that only exists for the duration of the function

To be precisely, the temporary exists until the end of the full expression, not only the duration of the function.

  • a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference.

In general, the lifetime of a temporary cannot be further extended by "passing it on": a second reference, initialized from the reference to which the temporary was bound, does not affect its lifetime.

That means something like auto sz = get_string(std::string{"Hello"}).size(); is fine.

Why doesn't a const reference extend the life of a temporary object passed via a function?

It's by design. In a nutshell, only the named reference to which the temporary is bound directly will extend its lifetime.

[class.temporary]

5 There are three contexts in which temporaries are destroyed at a
different point than the end of the full-expression. [...]

6 The third context is when a reference is bound to a temporary.
The temporary to which the reference is bound or the temporary that is
the complete object of a subobject to which the reference is bound
persists for the lifetime of the reference except:

  • A temporary object bound to a reference parameter in a function call persists until the completion of the full-expression containing
    the call.
  • The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed
    at the end of the full-expression in the return statement.
  • [...]

You didn't bind directly to ref2, and you even pass it via a return statement. The standard explicitly says it won't extend the lifetime. In part to make certain optimizations possible. But ultimately, because keeping track of which temporary should be extended when a reference is passed in and out of functions is intractable in general.

Since compilers may optimize aggressively on the assumption that your program exhibits no undefined behavior, you see a possible manifestation of that. Accessing a value outside its lifetime is undefined, this is what return ref2; does, and since the behavior is undefined, simply returning zero is a valid behavior to exhibit. No contract is broken by the compiler.

Lifetime of temporary passed through function by reference

It's a dangling reference. From [class.temporary]/4-5:

There are two contexts in which temporaries are destroyed at a different point than the end of the fullexpression.
The first context is when a default constructor is called [ ... ]

The second context is when a reference is bound to a temporary. The temporary to which the reference is
bound or the temporary that is the complete object of a subobject to which the reference is bound persists
for the lifetime of the reference except:

  • A temporary bound to a reference member in a constructor’s ctor-initializer [ ...]
  • A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of
    the full-expression containing the call
    .
  • [...]

The 5 persists until the completion of the full-expression containing the call, which is to say:

const int &r = fun(5);
// <== no more 5

Why do const references extend the lifetime of rvalues?

It was proposed in 1993. Its purpose was to eliminate the inconsistent handling of temporaries when bound to references.

Back then, there was no such thing as RVO (return value optimization), so simply banning the binding of a temporary to a reference would have been a performance hit.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/1993/N0345.pdf

Life extension of temporary by const reference

Both behaviors are correct, certainly according to the C++03 standard (8.5.3 [dcl.init.ref] paragraph 5):

Otherwise, the reference shall be to a non-volatile const type (i.e., cv1 shall be const). [Example: ...]

  • If the initializer expression is an rvalue, with T2 a class type, and “cv1 T1” is reference-compatible with “cv2 T2,” the reference is bound in one of the following ways (the choice is implementation-defined):

    — The reference is bound to the object represented by the rvalue (see 3.10) or to a sub-object within that object.

    — A temporary of type “cv1 T2” [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a sub-object within the temporary.

I think the definition of C++11 still allows the copy to be made but the wording doesn't as clearly allow the copy. In any case, VC++ doesn't claim to be fully C++11 compliant.

The lifetime of a temporary to which several references are bound in C++

This is a defect in that section that I reported as http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1299 .

The proposed resolution is to add a term "temporary expressions". Life-time extension only happens for objects referred to by temporary expressions.

Here's my original report which I privately emailed. I think it makes clear what it's about

In the model of the Standard, there appears to be a distinction about
temporary objects, and temporary expressions.

Temporary objects are created by certain operations operations, like
functional casts to class types. Temporary objects have a limited
specified lifetime.

Temporary expressions are expressions that are so attributed because
they are used to track whether or not an expression refers to a
temporary object for the purpose of determining whether or not the
lifetime of their referent is lengthened when bound by a reference.
Temporary expressions are compile time entities.

Several paragraphs refer to "temporaries", but do not explicitly
specify whether they refer to temporary objects referred to by
arbitrary expressions, or whether they refer only to temporary
expressions. The paragraphs about RVO (paragraph 12.8p31) use
"temporary" in the sense of temporary objects (they say such things
like "temporary class object that has not been bound to a reference").
The paragraphs about lifetime lengthening (sub-clause 12.2) refer to
both kinds of temporaries. For example, in the following, "*this" is
not regarded as a temporary, even though it refers to a temporary

struct A { A() { } A &f() { return *this; } void g() { } };

// call of g() is valid: lifetime did not end prematurely
// at the return statement
int main () { A().f().g(); }

As another example, core issue 462 handles about temporary expressions
(making a comma operator expression a temporary, if the left operand
was one). This appears to be very similar to the notion of "lvalue
bitfields". Lvalues that track along that they refer to bitfields at
translation time, so that reads from them can act accordingly and that
certain reference binding scenarios can emit diagnostics.

Lifetime of const reference variable not extended

In the article that you linked to, you will find:

(Note this only applies to stack-based references. It doesn’t work for references that are members of objects.)

That's why the references in a and b are not valid. They don't extend the life of the temporaries.

Aggregate reference member and temporary lifetime

The lifetime of temporary objects bound to references is extended, unless there's a specific exception. That is, if there is no such exception, then the lifetime will be extended.

From a fairly recent draft, N4567:

The second context [where the lifetime is extended] is when a
reference is bound to a temporary. The temporary to which the
reference is bound or the temporary that is the complete object of a
subobject to which the reference is bound persists for the lifetime of
the reference except:

  • (5.1) A temporary object bound to a reference parameter in a function call (5.2.2) persists until the completion of the
    full-expression containing the call.
  • (5.2) The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not extended; the temporary is
    destroyed at the end of the full-expression in the return statement.
  • (5.3) A temporary bound to a reference in a new-initializer (5.3.4) persists until the completion of the full-expression
    containing the new-initializer.

The only significant change to C++11 is, as the OP mentioned, that in C++11 there was an additional exception for data members of reference types (from N3337):

  • A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

This was removed in CWG 1696 (post-C++14), and binding temporary objects to reference data members via the mem-initializer is now ill-formed.


Regarding the examples in the OP:

struct S
{
const std::string& str_;
};

S a{"foo"}; // direct-initialization

This creates a temporary std::string and initializes the str_ data member with it. The S a{"foo"} uses aggregate-initialization, so no mem-initializer is involved. None of the exceptions for lifetime extensions apply, therefore the lifetime of that temporary is extended to the lifetime of the reference data member str_.



auto b = S{"bar"}; // copy-initialization with rvalue

Prior to mandatory copy elision with C++17:
Formally, we create a temporary std::string, initialize a temporary S by binding the temporary std::string to the str_ reference member. Then, we move that temporary S into b. This will "copy" the reference, which will not extend the lifetime of the std::string temporary.
However, implementations will elide the move from the temporary S to b. This must not affect the lifetime of the temporary std::string though. You can observe this in the following program:

#include <iostream>

#define PRINT_FUNC() { std::cout << __PRETTY_FUNCTION__ << "\n"; }

struct loud
{
loud() PRINT_FUNC()
loud(loud const&) PRINT_FUNC()
loud(loud&&) PRINT_FUNC()
~loud() PRINT_FUNC()
};

struct aggr
{
loud const& l;
~aggr() PRINT_FUNC()
};

int main() {
auto x = aggr{loud{}};
std::cout << "end of main\n";
(void)x;
}

Live demo

Note that the destructor of loud is called before the "end of main", whereas x lives until after that trace. Formally, the temporary loud is destroyed at the end of the full-expression which created it.

The behaviour does not change if the move constructor of aggr is user-defined.

With mandatory copy-elision in C++17: We identify the object on the rhs S{"bar"} with the object on the lhs b. This causes the lifetime of the temporary to be extended to the lifetime of b. See CWG 1697.


For the remaining two examples, the move constructor - if called - simply copies the reference. The move constructor (of S) can be elided, of course, but this is not observable since it only copies the reference.

About binding a const reference to a sub-object of a temporary

This is covered by CWG 1651:

The resolution of issues 616 and 1213, making the result of a member
access or subscript expression applied to a prvalue an xvalue, means
that binding a reference to such a subobject of a temporary does not
extend the temporary's lifetime. 12.2 [class.temporary] should be
revised to ensure that it does.

The status quo is that only prvalues are treated as referring to temporaries - thus [class.temporary]/5 ("The second context is when a reference is bound to a temporary.") is not considered applicable. Clang and GCC have not actually implemented issue 616's resolution, though. center().x is treated as a prvalue by both. My best guess:

  • GCC simply didn't react to any DRs yet, at all. It doesn't extend lifetime when using scalar subobjects, because those are not covered by [dcl.init.ref]/(5.2.1.1). So the complete temporary object doesn't need to live on (see aschelper's answer), and it doesn't, because the reference doesn't bind directly. If the subobject is of class or array type, the reference binds directly, and GCC extends the temporary's lifetime. This has been noted in DR 60297.

  • Clang recognizes member access and implemented the "new" lifetime extension rules already - it even handles casts. Technically speaking, this is not consistent with the way it handles value categories. However, it is more sensible and will be the correct behavior once the aforementioned DR is resolved.

I'd therefore say that GCC is correct by current wording, but current wording is defective and vague, and Clang already implemented the pending resolution to DR 1651, which is N3918. This paper covers the example very clearly:

If E1 is a temporary expression and E2 does not designate a
bit-field, then E1.E2 is a temporary expression.

center() is a temporary expression as per the paper's wording for [expr.call]/11. Thus its modified wording in the aforementioned [class.temporary] /5 applies:

The second context is when a reference does not bind directly (8.5.3
dcl.init.ref) or is initialized with a temporary expression (clause 5). The corresponding temporary
object (if any) persists for the lifetime of the reference
except: [...inapplicable exceptions...]

Voilà, we have lifetime extension. Note that "the corresponding temporary object" is not clear enough, one of the reasons for the proposal's deferment; it will assuredly be adopted once it gets revised.


is an xvalue (but not a bit-field), class prvalue, array prvalue or function lvalue and “cv1 T1” is reference-compatible with “cv2 T2”, or […]

Indeed, GCC respects this fully and will extend lifetime if the subobject has array type.



Related Topics



Leave a reply



Submit