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.)
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);
}
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.
Related
The following quotes are needed in the question:
[dcl.init.ref]/5:
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).
(emphasis mine)
[expr.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.
Consider the following example:
const int&& r1 = 0;
Taking cv1 T1 as const int, and cv2 T2 as int
It's clear that bullet [dcl.init.ref]/(5.3.1) is applied here. The initializer expression is an rvalue (prvalue); and cv1 T1 (const int) is reference-compatible with cv2 T2 (int). And since that the converted initializer is prvalue, its type T4 (int) is adjusted to cv1 T4 (const int). Then, temporary materialization is applied.
But, per [expr.type]/2, before applying temporary materialization conversion, cv1 T4 (const int) becomes int again. Then, by applying temporary materialization, we've got an xvalue denoting an object of type int. Then the reference is bound to the resulting glvalue.
Here's my first question. The reference r1 is reference to const int and the resulting glvalue is an xvalue denoting an object of type int. So how r1, which is of type const int&&, is now binding to an xvalue of type int? Is this valid binding? Is any missing wording? Am I misunderstood/missed something?
Consider another last example:
const int&& r2 = static_cast<int&&>(0);
The same wording as above applies: The initializer expression is an rvalue (xvalue) and cv1 T1 (const int) is reference-compatible with cv2 T2 (int). And since that the converted initializer is an xvalue not prvalue, [conv.qual] or even [conv.rval] is not applied (i.e, the condition "If the converted initializer is a prvalue, ..") isn't satisfied.
I know that [conv.rval] isn't needed here since the initializer expression is already an xvalue, but [conv.qual] is required.
And that's my last question. The reference r2 is reference to const int and the resulting glvalue is an xvalue denoting an object of type int. So how r2, which is of type const int&&, is now binding to an xvalue of type int? Is this valid binding? Is any missing wording? Am I misunderstood/missed something?
Here's my first question. The reference r1 is reference to const int and the resulting glvalue is an xvalue denoting an object of type int. So how r1, which is of type const int&&, is now binding to an xvalue of type int? Is this valid binding?
Yes, that's what happens. I fail to see the issue here.
I know that [conv.rval] isn't needed here since the initializer expression is already an xvalue, but [conv.qual] is required.
No it isn't. Again, I fail to see the issue.
There is, in fact, no rule that says that a reference of type T&&, can only refer to an object whose type is exactly T. A const int&& can refer to an int object. The concept of reference-compatibility was invented in order to describe what types of objects a reference can refer to.
I am trying to understand how reference initialization. For example, let's look at a typical example.
double val = 4.55;
const int &ref = val;
I can think of 2 possibilities of what is happening in the above snippet.
Possibility 1
The usual explanation is given as follows:
Here a temporary(prvalue) of type int with value 4 is created and then the reference ref is bound to this temporary(prvalue) int object instead of binding to the variable val directly. This happens because the type of the variable val on the right hand side is double while on the left hand side we have a reference to int. But for binding a reference to a variable the types should match. Moreover, the lifetime of the temporary prvalue is extended.
Possibility 2
I think there is another possibility that could happen which is as follows:
Here a temporary(prvalue) of type int with value 4 is created. But since const int &ref expects a glvalue and currently we've a prvalue, the temporary materialization kicks in and so the prvalue is converted to an xvalue. Then the reference ref is bound to this materialized xvalue(since xvalue is also a glvalue) instead of binding to the variable val directly. This happens because the type of the variable val on the right hand side is double while on the left hand side we have a reference to int. But for binding a reference to a variable the types should match. Moreover, the lifetime of the materialized temporary xvalue is extended.
My questions are:
Which of the above explanation is correct according to the C++11 standard. I am open to accept that none of the explanation above is correct in which case what is the correct explanation.
Which of the above explanation is correct according to the C++17 standard. I am open to accept that none of the explanation above is correct in which case what is the correct explanation.
I am also confused to whether a prvalue in the first step of both of the possibilities above, is actually a temporary object? Or the xvalue is the actual object. I mean do we have 2 temporary objects, like the first one due to "conversion to prvalue" and second one due to the "prvalue to xvalue" conversion(temporary materiliazation). Or do we only have one temporary which is due to the "prvalue to xvalue" temporary materialization.
PS: I am not looking for a way to solve this. For example, i know that i can simply write:
const double &ref = val;. My aim is to understand what is happening according to C++11 and C++17 standards.
val in const int &ref = val; is a lvalue, not a prvalue. Even if it is converted to a prvalue, this doesn't mean creation of a temporary in either C++11 or C++17. In C++17 this isn't the case since only prvalue-to-xvalue conversion (and some special cases not relevant here) creates a temporary, while in C++11 [conv.lval]/2 says that only for class types lvalue-to-rvalue conversion creates a temporary.
The initialization of a reference is explained in [dcl.init.ref].
In C++11, all previous cases fall through and so according to [dcl.init.ref]/5.2.2 a temporary of the destination type is created and initialized by copy-initialization from the initializer expression and the reference is bound to that temporary. In this copy-initialization the lvalue val is converted to a prvalue of type double and then to a prvalue of type int, neither of these steps creating additional temporaries.
In C++17, all cases fall through until [dcl.init.ref]/5.2.2, which states that the initializer expression is first implicitly converted to a prvalue of the destination type, which does not imply creation of a temporary, and then the temporary materialization conversion (prvalue-to-xvalue conversion) is applied and the reference bound to the result, i.e. the xvalue referring to the temporary.
In the end there is always exactly one temporary, the one created according to the rule in [dcl.init.ref].
Lets look at each of the cases(C++11 vs C++17) separately.
C++11
From decl.init.ref 5.2.2:
Otherwise, a temporary of type “ cv1 T1” is created and initialized from the initializer expression using the rules for a non-reference copy-initialization ([dcl.init]). The reference is then bound to the temporary.
One more important thing to note is that from basic.lval#4:
Class prvalues can have cv-qualified types; non-class prvalues always have cv-unqualified types...
When applied to your example, this means that a temporary of type int is created and is initialized from the initializer expression val using the rules for a non-reference copy-initialization. The temporary int so created has the value category of prvalue if/when used as an expression.
Next, the reference ref is then bound to the temporary int created which has value 4. Thus,
double val = 4.55;
const int &ref = val; // ref refers to temporary with value 4
C++17
From decl.init.ref 5.2.2.2:
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.
When applied to your example, this means that the initializer expression val is implicitly converted to a prvalue of type const int. Now we currently have a prvalue const int. But before temporary materialization is applied, expr 6 kicks in which says:
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.
This means before temporary materialization could happen, the prvalue const int is adjusted to prvalue int.
Finally, temporary materialization is applied and and ref is bound to the resulting xvalue.
Here's my take for C++17:
double val = 4.55;
const int &ref = val;
We're binding a const int reference to the lvalue expression of type double denoted by val. According to the declaration of references;
Otherwise, the initializer expression is implicitly converted to a prvalue of type “T1”.
we convert the expression val to type int. Note that this implicit type! conversion fits with the draft.
Next,
The temporary materialization conversion is applied, considering the type of the prvalue to be “cv1 T1”, ...
in order to apply the temporary materialization (also known as prvalue -> xvalue conversion), we must have fully matched types so the expression is aditionally converted to const int is considered to be const int without any conversions (source). The value category remains the same (prvalue). And finally,
... and the reference is bound to the result.
we have a reference binding of type T to a prvalue of type T which induces temporary materialization (source) and val gets converted to xvalue. The created temporary object is of type const int and value of 4.
EDIT: And to answer your questions, of course. So neither of the given possibilities aren't technically correct for C++17 right at the start. Strictly speaking, neither prvalues nor xvalues are temporary objects. Rather, they are value category of expressions and expression might (or might not) denote a (temporary) object. So technically, xvalues denote temporary objects while prvalues don't. You could still say that xvalues are temporary objects, that's completely fine by me as long as you know what you're talking about.
Consider this code:
int **p = 0;
class S {
public:
explicit operator int**&() {
return p;
}
};
int main() {
S a;
int *const*&b (a); // error in both g++-7 and clang-5.0 with "-std=c++17"
return 0;
}
You will agree
a qualification conversion from int** to int*const* is possible, and
int *const*&b (a) is a direct-initialization.
First, we refer to 11.6.3, paragraph 5 [dcl.init.ref] from n4700.
A reference to type “cv1 T1 (= int*const*)” is initialized by an expression of type “cv2 T2 (= S)” as follows:
If the reference is an lvalue reference and the initializer expression
...
has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (this conversion is selected by enumerating the applicable conversion functions (16.3.1.6) and choosing the best one through overload resolution (16.3)),
then the reference is bound to the initializer expression lvalue in the first case and to the lvalue result of the conversion in the second case...
Here, we expect T3 to be int*const*. As noted above, whether it's a possible conversion is determined as per 16.3.1.6, paragraph 1 [over.match.ref].
... Assuming that “reference to cv1 T” is the type of the reference being
initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:
... For direct-initialization, those explicit conversion functions that
are not hidden within S and yield type “lvalue reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2”, respectively, where T2 is the same type as T or can be converted to type T with a qualification conversion are also candidate functions.
Here, S::operator int**& yields "lvalue reference to T2 (= int**)", and it can be converted to T (= int*const*) by a qualification conversion. Here, we can say that the conversion is possible, but the program is not accepted in both g++-7 and clang-5.0. Why is that?
The reference initialization rule we're looking for is [dcl.init.ref]:
A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:
We have cv1 T1 as int* const* and cv2 T2 as S. We then go through the next sections carefully:
If the reference is an lvalue reference and the initializer expression
is an lvalue (but is not a bit-field), and “cv1 T1” is reference-compatible with “cv2 T2”, or
has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (this conversion is selected by enumerating the applicable conversion functions ([over.match.ref]) and choosing the best one through overload resolution),
then the reference is bound to the initializer expression lvalue in the first case and to the lvalue result of the conversion in the second case (or, in either case, to the appropriate base class subobject of the object).
Our reference is an lvalue reference. The initializer expression is an lvalue but the two types are not reference-compatible, so the first bullet does not apply.
The initializer expression does have non-reference-related class type, but it cannot be converted to a reference-compatible type. The reference-compatible part is important. int** is not reference-compatible with int* const*, and while the former can be converted to the latter, the result would not be an lvalue - which is also required.
So, this section doesn't apply, and we move on.
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.
Our reference meets neither of those criteria, so the initialization is ill-formed.
A simpler version of this failure would be:
int* pi;
int const*& r = pi; // error
We can't go through a qualification conversion when we have an lvalue reference to non-const type.
I'm looking at the standard 5.16 paragraph 3, trying to understand what is going on. Consider the type M defined as
struct M {
M();
M(const M&);
M(M&&);
};
If I have a ternary expression pred ? E1 : E2, where the type of E1 is const M& and the type of E2 is M&& does 5.16 paragraph 3 bullet 1 apply?
— If E2 is an lvalue: E1 can be converted to match E2 if E1 can be implicitly converted (Clause 4) to the
type “lvalue reference to T2”, subject to the constraint that in the conversion the reference must bind
directly (8.5.3) to an lvalue.
I think it doesn't, because to have an implicit conversion to const M&, which requires M to have the member function operator const M&().
However, I'm not sure, because it could be converted to const M implicitly, can the reference be implicitly added?
If it is implicitly convertible, does M&& bind directly to const M&?
I went through the procedure in 8.5.3, and I think that paragraph 5 bullet 2 is where this case falls, so it does bind directly, but I'm not certain.
— If the initializer expression [..] has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be
implicitly converted to an xvalue, class prvalue, or function lvalue of type “cv3 T3”, where
“cv1 T1” is reference-compatible with “cv3 T3”
You don't have expressions of type M&&, instead it would be adjusted to be an xvalue of type M.
So the question is: if you have an xvalue of type M, can it be implicitly converted to lvalue reference to const M? The answer is yes, since a const lvalue reference can be initialized with an rvalue. Such a reference binding is direct since it falls under the following case:
If the initializer expression
— is an xvalue (but not a bit-field), class prvalue, array prvalue or function lvalue and “cv1 T1”
is reference-compatible with “cv2 T2”, ...
rather than the last case that involves construction of a temporary, which is the indirect binding case.
Therefore this use of the conditional operator will be well-formed. The xvalue of type M will be converted to an lvalue of type const M. Then the lvalue-to-rvalue conversion will be applied and the result will be a prvalue of type const M.
I have read that the code below is valid in C++11:
int && a = 3;
a = 4;
Is it supposed to write 4 in the memory address where the numeric literal 3 is stored? Maybe some compiler optimizations would prevent this from happening, but is it supposed to do so?
When you assign a prvalue that is not of class type to an rvalue reference, a temporary object is created and the reference is bound to that. You are simply modifying the temporary object.
A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:
If the reference is an lvalue reference [...]
Otherwise, [...] or the reference shall be an rvalue reference.
If the initializer expression
is an xvalue (but not a bit-field), class prvalue, array prvalue or function lvalue [...], or
has a class type [...]
[...]
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.
Conceptually, a prvalue is just a value that may or may not have come from some object in memory. Literals don't have a corresponding object in memory, so this rule forces an object to be created.