Language Lawyer
Language Lawyer

Reputation: 3569

C++17: is the temporary object (and storage) created by the compiler for (static storage duration) const reference binding modifiable?

Lets compile the following top-level declaration

const int& ri = 5;

with clang++. With -std=c++14 and below it places the temporary object (and the pointer representing the reference) into the .rodata section:

        .type   _ZGR2ri_,@object        # @_ZGR2ri_
        .section        .rodata,"a",@progbits
        .p2align        2
_ZGR2ri_:
        .long   5                       # 0x5
        .size   _ZGR2ri_, 4

        .type   ri,@object              # @ri
        .globl  ri
        .p2align        3
ri:
        .quad   _ZGR2ri_
        .size   ri, 8

But if we change the standard version to -std=c++17 (or above), the object will be placed into the .data section (the pointer is still in the .rodata, though):

        .type   _ZGR2ri_,@object        # @_ZGR2ri_
        .data
        .p2align        2
_ZGR2ri_:
        .long   5                       # 0x5
        .size   _ZGR2ri_, 4

        .type   ri,@object              # @ri
        .section        .rodata,"a",@progbits
        .globl  ri
        .p2align        3
ri:
        .quad   _ZGR2ri_
        .size   ri, 8

What is the reason of such behavior? Is it a bug? The fact that it still replaces all uses of ri in the same TU by its initial value 5 suggests that it is a bug.

My hypothesis is that in [dcl.init.ref]/5.2

If the converted initializer is a prvalue, its type T4 is adjusted to type “cv1 T4” ([conv.qual]) and the temporary materialization conversion is applied.

it naïvely discards (or rather do not add) the cv1-qualifier from (to) the prvalue type.

The funny thing is that if replace the initializer expression with a prvalue of non-reference-related, but convertible type

const int& ri = 5.0;

it starts to put the object with the value 5 into the .rodata section again.

Is there anything in the standard that now requires such mutability? In other words:

Upvotes: 2

Views: 603

Answers (2)

mada
mada

Reputation: 1985

Given this example

const int& ri = 5;

The applicable wording from the standard is [dcl.init.ref]/5

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

  • (5.1) [..]
  • (5.2) [..]
  • (5.3) Otherwise, if the initializer expression
    • (5.3.1) is an rvalue (but not a bit-field) or function lvalue and “cv1 T1” is reference-compatible with “cv2 T2, or
    • (5.3.2) [..]

then the value of the initializer expression in the first case and the result of the conversion in the second case is called the converted initializer. If the converted initializer is a prvalue, its type T4 is adjusted to type “cv1 T4” ([conv.qual]) and the temporary materialization conversion ([conv.rval]) is applied. In any case, the reference is bound to the resulting glvalue (or to an appropriate base class subobject).

It's already known that const int (cv1 T1) is reference-compatible with int (cv2 T2). And the converted initializer here is of type int (T4); then it's adjusted to const int (cv1 T4); then temporary materialization ([conv.rval]) gets applied. But before applying it, [expr.type]/2 kicks in:

If a prvalue initially has the type “cv T”, where T is a cv-unqualified non-class, non-array type, the type of the expression is adjusted to T prior to any further analysis.

Further analysis here includes temporary materialization conversion.

So the adjusted prvalue has type int not const int. At this point you can enter [conv.rval]:

A prvalue of type T can be converted to an xvalue of type T. This conversion initializes a temporary object ([class.temporary]) of type T [..]

So you have an xvalue denoting a temporary of type int. And the reference ri is bound to the resulting glvalue (i.e, ri is binding to a xvalue denoting a temporary of type int not const int).


Let's try to analyze the second example:

const int& ri2 = 5.0;

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

  • (5.1) [..]
  • (5.2) [..]
  • (5.3) [..]
  • (5.4) Otherwise,
    • (5.4.1) [..]
    • (5.4.2) Otherwise, the initializer expression is implicitly converted to a prvalue of type “T1”. The temporary materialization conversion is applied, considering the type of the prvalue to be “cv1 T1”, and the reference is bound to the result.

Here, the initialzier expression 5.0 gets implicitly converted to prvalue of type int via floating-integral standard conversion ([conv.fpint]). Then temporary materialization is applied but in this case it's said that "considering the type of the prvalue to be “cv1 T1”". So this means that the temporary materialization conversion results in an xvalue denoting a temporary of type const int. And the reference ri2 is bind to that result.

Upvotes: 0

curiousguy
curiousguy

Reputation: 8268

Let's analyse

const int& ri = 5;

From the C++ draft: initialization of references [dcl.init.ref]/5

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

Here cv1 = const, T1 = int, cv2 = "", T2 = int

skipping the unapplicable clauses, we get here [dcl.init.ref]/5.3:

Otherwise, if the initializer expression (5.3.1) is an rvalue (but not a bit-field) (...) and “cv1 T1” is reference-compatible with “cv2 T2”, or (...) then the value of the initializer expression (...) is called the converted initializer.

The converted initializer is 5 a prvalue.

If the converted initializer is a prvalue, its type T4 is adjusted to type “cv1 T4” ([conv.qual]) and the temporary materialization conversion ([conv.rval]) is applied. In any case, the reference is bound to the resulting glvalue (...)

cv1 T4 = const int

So an object of type const int is created and the reference is bound to it.

"Temporary materialization conversion" is new concept explained here [conv.rval]:

A prvalue of type T can be converted to an xvalue of type T. This conversion initializes a temporary object ([class.temporary]) of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object. T shall be a complete type.

So we have a conversion prvalue -> xvalue -> lvalue.

The lifetime of the temporary is described in [class.temporary]/6:

The temporary object to which the reference is bound or (...) persists for the lifetime of the reference if the glvalue to which the reference is bound was obtained through one of the following:

(6.1) a temporary materialization conversion ([conv.rval]), (...)

So this is the case and the lifetime of the temporary "persists for the lifetime of the reference".

[basic.life]/5

A program may end the lifetime of any object by reusing the storage which the object occupies

but not every object storage can be used that way: [basic.memobj]/10

Creating a new object within the storage that a const complete object with static, thread, or automatic storage duration occupies, or within the storage that such a const object used to occupy before its lifetime ended, results in undefined behavior.

Storage duration is defined here [basic.stc]

The storage duration is the property of an object that defines the minimum potential lifetime of the storage containing the object. The storage duration is determined by the construct used to create the object and is one of the following: (1.1) static storage duration (1.2) thread storage duration (1.3) automatic storage duration (1.4) dynamic storage duration 2 Static, thread, and automatic storage durations are associated with objects introduced by declarations ([basic.def]) and implicitly created by the implementation.

But then the text only mentions variables, not objects. I don't see where the storage duration of a temporary is defined!

EDIT: @LanguageLawyer points me to this core defect:

1634. Temporary storage duration

The apparent intent of the reference to 15.2 [class.temporary] is that a temporary whose lifetime is extended to be that of a reference with one of those storage durations is considered also to have that storage duration.

(...) the specification of lifetime extension of temporaries (also in 15.2 [class.temporary] paragraph 5) does not say anything about storage duration. Also, nothing is said in either of these locations about the storage duration of a temporary whose lifetime is not extended.

So there is indeed a missing part in the specification; the lifetime of these objects created by the implementation is not well specified. The specification of lifetime in C++ is difficult as you can see from the many additions in the specification of lifetime, unions, subobjects, and "nested" in the more recent standard; some of these new clauses even apply to code that uses no new C++ feature, code that was intended to be supported (but not well described) in the pre-standard the time of the ARM, such as code doing nothing more than changing the "active member" of a union.

If the specification is interpreted the way the DR claims is the intent, the lifetime of the const int temporary with value 5 would have static storage duration; its memory wouldn't be legally modifiable and could be placed in read-only section.

(Another solution: the committee could also make up a specific storage class for temporaries.)

Upvotes: 1

Related Questions