Cv-qualifications of prvalues (revisited)

1.7k Views Asked by At

This is a followup to my previous question, where the apparent consensus was that the change in treatment of cv-qualifications of prvalues was just a fairly minor and inconsequential change intended to solve some inconsistencies (e.g. functions returning prvalues and declared with cv-qualified return types).

However, I see another place in the standard that appears to rely on prvalues having cv-qualified types: initialization of const references with prvalues through temporary materialization conversion. The relevant wording can be found in multiple spots in 9.3.3/5

[...] 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 [...]

[...] Otherwise, the initializer expression is implicitly converted to a prvalue of type “cv1 T1”. The temporary materialization conversion is applied and the reference is bound to the result.

The intent is obviously to make sure that when we get to the actual temporary materialization conversion

7.3.4 Temporary materialization conversion
1 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. [...]

the type T that it receives as input includes the required cv-qualifications.

But how does that cv-qualification survive the 7.2.2/2 in case of non-class non-array prvalue?

7.2.2 Type
2 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.

Or does it?

E.g. what kind of temporary do we get in this example

const int &r = 42;

Is the temporary const or not? Can we do

const_cast<int &>(r) = 101; // Undefined or not?

without triggering undefined behavior? If I'm not mistaken, the original intent was to obtain a const int temporary in such cases. Is it still true? (For class types the answer is clear - we get a const temporary.)

2

There are 2 best solutions below

0
On

Why are you doubting the language of 7.2.2? This seems pretty unambiguous that cv qualifiers are discarded on non-class, non-array prvalues, so the type T in temporary materialization is a non-const, non-volatile type.

If that weren't the case, then you wouldn't be able to bind prvalues to non-const rvalue references. Yet it seems overwhelmingly likely that the standard was intended to accept programs such as this:

#include <type_traits>

template<typename T> void
f(T &&t)
{
  static_assert(std::is_same_v<decltype(t), int&&>);
  ++t;
}

int
main()
{
  f(5);
}
0
On

E.g. what kind of temporary do we get in this example const int &r = 42; Is the temporary const or not?

Let's analyze your example to see whether it's const or not. Given this example

const int &r = 42;

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 the initializer expression 42 is a prvalue, and 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, via [conv.qual], to const int (cv1 T4); then temporary materialization ([conv.rval]) gets applied. Note that, [expr.type]/2 doesn't apply here because the initial type of the prvalue is cv-unqualified type:

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.

Note also, it cannot be applied after adjustment to cv1 T4 because cv1 T4 is not the initial type of the prvalue.

So the adjusted prvalue has type const int (cv1 T4) Then, temporary materialization gets applied to this prvalue; [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 [..]

The const int prvalue gets converted to an xvalue of type const int. And a temporary of type const int gets initialized. So you have an xvalue denoting a temporary of type const int. And the reference r is bound to the resulting glvalue (i.e, r is binding to a xvalue denoting a temporary of type const int).

So as far as I can tell, the temporary that has been created is const-qualified.

Can we do const_cast<int &>(r) = 101; without triggering undefined behavior?

No, this undefined behavior by definition since you're trying to modify (write into) a read-only memory location:

[expr.const.cast]/6: Depending on the type of the object, a write operation through the pointer, lvalue or pointer to data member resulting from a const_­cast that casts away a const-qualifier can produce undefined behavior.

[dcl.type.cv]/4: Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.