Reputation: 3569
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:
ri
modifiable by conforming code? (obviously code involving UB could try to change it and the compiler isn't required to make effort to allow that)ri
that is sizeof (int)
?Upvotes: 2
Views: 603
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 ofthe 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
”, whereT
is a cv-unqualified non-class, non-array type, the type of the expression is adjusted toT
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
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".
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