31

With code like

#include <stdio.h>

struct P2d {
    double x, y;
    P2d(double x, double y) : x(x), y(y) {}
    ~P2d() { printf("Destructor called\n"); }
};

P2d center() {
    return P2d(10, 10);
}

int main(int argc, const char *argv[]) {
    const double& x = center().x;
    printf("x = %.18g\n", x);
    return 0;
}

g++ (version 5.2.0) will destroy the P2d temporary instance before entering the printf in main, but the value will be preserved anyway (i.e. instead of binding x to the actual member of the temporary P2d instance it will create another temporary double to copy the value of the member).

clang++ (IMO correctly) instead extends the lifetime of the temporary P2d instance to the lifetime of the x reference and the destructor will be therefore called after the printf in main.

If instead of using plain double as type for the x and y members you make a class (e.g. Double) then both compilers agree and they extend the lifetime of the temporary P2d object to past the printf.

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

6502
  • 112,025
  • 15
  • 165
  • 265
  • Tested this program on `gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0` on 2021-02-24 and it prints `x = 10, Destructor called`, so must be "fixed". Will try MSVC too. – Andrew Tomazos Feb 24 '21 at 13:35
  • Tested with msvc `Microsoft (R) C/C++ Optimizing Compiler Version 19.26.28805 for x64` on same date, and msvc incorrectly prints `Destructor called, x = 10` even with `/std:c++17` – Andrew Tomazos Feb 24 '21 at 13:40

3 Answers3

16

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.

Community
  • 1
  • 1
Columbo
  • 60,038
  • 8
  • 155
  • 203
  • Is that wording going to get adopted? It's not in the latest working draft. – Barry Mar 11 '16 at 18:42
  • @Barry There are a few minor issues with the proposal as is; see the [discussion of CWG 1299](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1299). I'm very positive that a revised paper will be accepted. I'll message the author about it. – Columbo Mar 11 '16 at 18:45
  • So the general remarks in 8.5.3 about reference initialization doesn't apply? `center()` is AFAIK a `prvalue` and I assume that a subobject of a `prvalue` is also a `prvalue`. Given that in the example is not a "class prvalue" seems to me the wording in 8.5.3 dictates the creation of a temporary `double` and binding to it instead of binding to the member subobject. – 6502 Mar 13 '16 at 07:34
  • @6502 No, center().x is an xvalue. – Columbo Mar 13 '16 at 09:32
  • @Columbo: where is it stated that `center().x` is an `xvalue`? The definition says "An xvalue is the result of certain kinds of expressions involving rvalue references". Where is the rvalue reference in this case? – 6502 Mar 13 '16 at 10:29
  • @6502 Alright, your confusion is justified. I added the quote and an explanation for current wording. :-) – Columbo Mar 13 '16 at 11:48
  • Changed again the accepted answer. The part "If E1 is an lvalue, then E1.E2 is an lvalue; otherwise E1.E2 is an xvalue" really makes it clear that `center().x` is an xvalue. However I can't avoid thinking what a mess of an already too much complicated semantic made the adoption of move semantic in C++11. I'm thinking this can be classified as C++ "jumping the shark" in the context of programming languages. – 6502 Mar 13 '16 at 18:15
  • @Columbo Yeah, the normative definition of xvalue could use a lot of love. There's a note somewhere that actually details lots of cases, but why is it a note? Shrug. – Barry Mar 13 '16 at 20:10
  • @6502 I have done more research, and this appears to be more complicated than I thought. See my new answer. – Columbo Mar 14 '16 at 12:18
  • In C++17 the references are [expr.ref]/4.2 "If `E1` is an lvalue, then `E1.E2` is an lvalue; otherwise `E1.E2` is an xvalue.", and [class.temporary]/6 covers that binding to `E1.E2` actually extends E1 in this circumstance – M.M May 02 '18 at 01:42
  • @M.M Yeah, so as I mentioned in my answer, the wording in N3918 has been adopted. – Columbo May 02 '18 at 09:34
11

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.

Daniel Jour
  • 15,896
  • 2
  • 36
  • 63
  • In the table describing `xvalue` is stated that a function call returning a non-reference is a `prvalue`. So `center()` is a `prvalue` and, I assume, also `center().x` is a `prvalue`, not an `xvalue`. Apparently the intention is to relegate `xvalue`s only to move-construction cases... – 6502 Mar 13 '16 at 07:37
  • I doubt that: `center()` is a prvalue, which is an rvalue. Moreover `x` is a non-static data member. Thus the part I quoted applies. Maybe this helps: The double returned from `2.0 + 2.0` is a temporary, but it has no "identity"; it's just the value. `center().x` is also a temporary double, but it *has* an identity, it's one of the double data members of `P2d`. – Daniel Jour Mar 13 '16 at 07:49
  • N3242 is ancient, why would you quote that – M.M Mar 13 '16 at 20:36
  • @M.M The question wasn't tagged with a specific standard, so I wanted to "keep it low" and stay with C++11 (which also introduced the concept of an xvalue, so) – Daniel Jour Mar 13 '16 at 20:40
  • N3242 is a pre-c++11 draft. The C++11 text is N3337 (free). [See here](http://stackoverflow.com/a/10775520/1505939) – M.M Mar 13 '16 at 20:42
  • @M.M IIRC I just followed a link to "the latest publicity available working draft" on the committee's site. I bookmarked the newer document for future language lawyer fun, though :) – Daniel Jour Mar 13 '16 at 21:04
  • 1
    Preferably use C++14 draft (N4140) unless there's reason to use C++11 specifically; a lot of defects have been fixed since then – M.M Mar 13 '16 at 21:04
  • @6502 I just wanted to add: I was *right* all of the time! Even my explanation (in comments) was right. Sorry, but feels a lot better after pointing this out ;D – Daniel Jour Mar 13 '16 at 21:07
  • @DanielJour: That `g++` was in error was my first gut feeling and you can understand that from the question itself. However the point was describing exactly and clearly **why** it is a bug, citing or referring relevant parts of the standard haystack on where to find this specific needle. – 6502 Mar 14 '16 at 06:50
5

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.

Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977
  • But per 8.5.3, I believe the reference is not in fact bound to the subobject of the class temporary - see my answer. – aschepler Mar 11 '16 at 18:54