About binding a const reference to a sub-object of

2020-01-25 00:19发布

问题:

With code like

#include <iostream>

struct P {
    int x;
    P(int x) : x(x) {}
    ~P() { std::cout << "~P()\n"; }
};

int main() {
    auto const& x = P{10}.x;
    std::cout << "extract\n";
}

GCC prints ~P() extract, indicating that the temporary's lifetime is not extended by the reference.

By contrast, Clang (IMO correctly) extends the lifetime of the temporary to the lifetime of the reference x and the destructor will be therefore called after the output in main.

Note that GCC suddenly shows Clang's behavior if we, instead of int, use some class type (e.g. string).

Is this a bug in GCC or something allowed by the standard?

回答1:

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.



回答2:

I would argue for a bug in g++, because, quoting draft N3242, §12.2/5:

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:

So its lifetime must be extended, except when:

A temporary bound to a reference member in a constructor’s ctor-initializer [..]

A temporary bound to a reference parameter in a function call [..]

The lifetime of a temporary bound to the returned value in a function return statement [..]

A temporary bound to a reference in a new-initializer [..]

Our case doesn't fit any of these exceptions, thus it must follow the rule. I'd say g++ is wrong here.

Then, regarding the quote aschepler brought up from the same draft §8.5.3/5 (emphasis mine):

A reference to type "cv1 T1" is initialized by an expression of type "cv2 T2" as follows:

  1. If the reference is an lvalue reference and the initializer expression

    a. is an lvalue (but is not a bit-field) and "cv1 T1" is reference-compatible with "cv2 T2", or

    b. has a class type ...

    then ...

  2. Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be const), or the reference shall be an rvalue reference.

    a. If the initializer expression

    • i. is an xvalue, class prvalue, array prvalue or function lvalue and "cv1 T1" is reference-compatible with "cv2 T2", or

    • ii. has a class type ...

    then the reference is bound to the value of the initializer expression in the first case....

    b. Otherwise, a temporary of type "cv1 T1" is created and initialized from the initializer expression using the rules for a non-reference copy-initialization (8.5). The reference is then bound to the temporary.

Looking at what an xvalue is, this time quoting http://en.cppreference.com/w/cpp/language/value_category ...

An xvalue ("expiring value") expression is [..]

a.m, the member of object expression, where a is an rvalue and m is a non-static data member of non-reference type;

... the expression center().x should be an xvalue, thus case 2a from §8.5.3/5 applies (and not the copy). I'll stay with my suggestion: g++ is wrong.



回答3:

Just read Columbo's answer.


This is a gcc bug. The relevant rule is in [class.temporary]:

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

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 object bound to a reference parameter in a function call (5.2.2) 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 (6.6.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
— 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.

We're binding a reference to a subobject of a temporary, so the temporary should persist for the lifetime of the reference. None of those three exceptions to this rule apply here.