Consider the fragment:
try {
Foo f;
throw std::move(f);
}
catch (Foo& f) { }
[expr.throw] says that:
the type of the exception object
is determined by removing any top-level cv-qualifiers from the static type of the operand and adjusting the
type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T”,
respectively.
which would be Foo&&. The exception object is then initialized according to [except.throw]:
Throwing an exception copy-initializes (8.5, 12.8) a temporary object, called the exception object. The
temporary is an lvalue and is used to initialize the variable declared in the matching handler (15.3). If the
type of the exception object would be an incomplete type or a pointer to an incomplete type other than
(possibly cv-qualified) void the program is ill-formed.
This suggests to me that the exception object is initialized as:
Foo&& __exception_object = std::move(f);
and that the handler would not match. However, both gcc and clang do catch this exception. So what's the actual type of the exception object here? If Foo, why?
The static type of an expression is never a reference type.
1.3.24 [defns.static.type] defines "static type":
type of an expression (3.9) resulting from analysis of the program without considering execution semantics
the first step in that "analysis of the program" is to remove references, see
5 [expr] p5 and Expressions can have reference type
If an expression initially has the type “reference to T” (8.3.2, 8.5.3), the type is adjusted to T prior to any further analysis. The expression designates the object or function denoted by the reference, and the expression is an lvalue or an xvalue, depending on the expression.
So std::move(f) is an xvalue expression, with static type Foo.
You don't need to involve rvalues to demonstrate this, the same was true in C++03 with:
int& f();
throw f();
This throws an int not an int&.
Without considering the specifics, an exception object is an object, and a reference is not an object, so an exception object cannot be a reference. It must be an object.
Related
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.
While reading about the details of exception handling in the standard, I noticed that it doesn't ever seem to actually specify that the operand to a throw expression will provide the value for the thrown exception object.
The [expr.throw] section describing the semantics of a throw expression says only that:
Evaluating a throw-expression with an operand throws an exception (15.1); the type of the exception object is determined by removing any top-level cv-qualifiers from the static type of the operand and adjusting the type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T,” respectively.
So if someone says throw 5, the type of the exception object must be int. This does not seem to actually say that the value of the exception object must be 5 rather than, say, 7 or any other arbitrary value of type int. Section 15.1 [except.throw] continues to be just as nebulous. It says things like:
An object is passed and the type of that object determines which handlers can catch it.
Once again, the the type of the operand matters, but not the value of the operand.
Throwing an exception copy-initializes a temporary object, called the exception object.
A temporary object is copy-initialized, but no mention is made of this temporary object having any particular value. The compiler could copy-initialize the temporary exception object from a prvalue int with value 7 if it wanted to, and it would still satisfy this clause because it did copy-initialize a temporary object. The operand to the throw expression is never mentioned.
So, if I have this program:
#include <cassert>
void f() {
try {
throw 5;
} catch (int x) {
assert(x == 5);
}
}
What specific wording in the C++ standard guarantees that this assertion will never fail? As far as I can tell, the standard only guarantees that the exception object will have type int but not that it will have any particular value, i.e. the assertion could fail if the temporary object were initialized with a value of 7. There doesn't seem to be any mention of the object/value/result of the operand to the throw expression being used to create the exception object.
I have the following C++ code
template <class E>
class ExceptionWrapper {
public:
explicit ExceptionWrapper(const E& e): e(e) {}
void throwException() {
throw e;
}
private:
E e;
};
...
try {
ExceptionWrapper<E> w(...);
w.throwException();
} catch (const E& e) {
...
}
...
Question: is this code valid? I could argue that returning a reference to the class member is almost always not valid (and I am sure everybody agrees with this statement). However, my colleague claims that this is not the case with throw.
P.S. after changing catch (const E& e) to catch (E e) a nasty bug seemingly disappeared which strengthens my position - that this code is not valid.
my claim is that catching e by reference is not valid since e is a member of w and w is not alive in the catch scope.
Your claim is incorrect. throw e; throws a copy of the member, and that copy is valid in catch's scope.
§ 15.1 / 3 (n3797 draft):
Throwing an exception copy-initializes (
8.5
,
12.8
) a temporary object, called the
exception object
. The
temporary is an lvalue and is used to initialize the variable named in the matching
handler
(
15.3
). If the
type of the exception object would be an incomplete type or a pointer to an incomplete type other than
(possibly cv-qualified)
void
the program is ill-formed. Evaluating a
throw-expression
with an operand throws
an exception; the type of the exception object is determined by removing any top-level
cv-qualifiers
from the
static type of the operand and adjusting the type from “array of
T
” or “function returning
T
” to “pointer to
T
” or “pointer to function returning
T
,” respectively.
Catching by const reference is the preferred way to catch exceptions. It allows catching derivatives of std::exception without slicing the exception object.
I think the relevant point is :
15.1. Throwing an exception:
p3. A throw-expression initializes a temporary object, called the exception object, the type of which is determined
by removing any top-level cv-qualifiers from the static type of the operand of throw and adjusting the type
from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T”, respectively.
The temporary is an lvalue and is used to initialize the variable named in the matching handler (15.3). If
the type of the exception object would be an incomplete type or a pointer to an incomplete type other
than (possibly cv-qualified) void the program is ill-formed. Except for these restrictions and the restrictions
on type matching mentioned in 15.3, the operand of throw is treated exactly as a function argument in a
call (5.2.2) or the operand of a return statement.
This is from the draft for c++11, emphasis mine.
It basically means there is a temporary object created from the argument of throw. Just like there was a function E f(){return private_e;}, and that temporary is used as the argument for the appropriate handler. So you would have two possible copies actually if you didn't catch by reference.
Probably also relevant:
p5. When the thrown object is a class object, the copy/move constructor and the destructor shall be accessible,
even if the copy/move operation is elided (12.8).
If the constructor fails/throws, w doesn't exist. So "w.throwException()" is not valid.
The snippet below compiles
#include <iostream>
int& f() { static int i = 100; std::cout << i << '\n'; return i; }
int main()
{
int& r = f();
r = 101;
f();
}
and print the values (live example)
100
101
Now, reading §8.5.3/5 in N4140, I can see that it compiles because of bullet point (5.1.1), that is, the reference is an lvalue reference, the initializer expression is an lvalue and int is reference-compatible with int (or with int& - I don't know for sure which one I should use here).
Bullet points (5.1) and (5.1.1):
— 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 ...
Now suppose I change the left value reference in the declaration int& r = f(); by a right value reference, i.e., int&& r = f();. I know the code won't compile, as an rvalue reference doesn't bind to an lvalue. But what I'm curious is, how to reach this conclusion using the Standard?
I'll explain what are my difficulties:
Clearly int&& r = f(); is covered by bullet point (5.2), because the reference is an rvalue reference.
Bullet point (5.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.
In principle, I would say that (5.2.1.1) supports this initialization as the initializer is a function lvalue and int is reference compatible with int (or with int&).
Bullet points (5.2.1) and (5.2.1.1):
— 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”, or ...
Edit
I've included the bullet points verbatim from N4140 (C++14), which are equivalent to similar bullet points in N3337 (C++11).
the initializer expression is an lvalue and int is reference-compatible with int (or with int& - I don't know for sure which one I should use here).
Reference-compatibility is a relation applied to the type referred to, not the reference type. For example, [dcl.init.ref]/5 talks about intializing "a reference to type cv1 T1 by an expression of type cv2 T2", and later compares e.g. "where T1 is not reference-related to T2".
The type of the expression f() is just int, despite the fact that the return type of f is int&. Expressions simply do not have reference type when we observe them(*); the reference is stripped and used to determine the value category (see [expr]/5). For int& f(), the expression f() is an lvalue; for int g(), the expression g() is an rvalue.
(*)To be perfectly precise, expressions can have reference type in the Standard, but only as the "initial" resulting type. The reference is dropped "prior to any further analysis", which implies that this referencess is simply not observable through the type.
Now suppose I change the left value reference in the declaration int& r = f(); by a right value reference, i.e., int&& r = f();. I know the code won't compile, as an rvalue reference doesn't bind to an lvalue. But what I'm curious is, how to reach this conclusion using the Standard?
The confusion, as it seems from the discussion in the comments, seems to be that f() is not a function lvalue. Value categories such as "lvalue" and "rvalue" are properties of expressions. The term "function lvalue" must therefore refer to an expression, namely an expression of function type with the value category "lvalue".
But the expression f() is a function call expression. Grammatically, it's a postfix-expression, the postfix being the function argument list. As per [expr.call]/10:
A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.
And [expr.call]/3
If the postfix-expression designates a destructor [...]; otherwise, the type of the function call expression is the return type of the statically chosen function [...]
That is, the (observed see above) type of the expression f() is int, and the value category is "lvalue". Note that the (observed) type is not int&.
A function lvalue is for example an id-expression like f, the result of indirecting a function pointer, or an expression yielding any kind of reference to function:
using ft = void();
void f();
ft& l();
ft&& r();
ft* p();
// function lvalue expressions:
f
l()
r()
*p()
[expr.prim.general]/8 specifies that those identifiers like f are, as id-expressions, lvalues:
An identifier is an id-expression provided it has been suitably declared. [...] The type of the expression is the type of the identifier. The result is the entity denoted by the identifier. The result is an lvalue if the entity is a function, variable, or data member and a prvalue otherwise.
Back to the example int&& r = f();. Using some post-N4296 draft.
[dcl.init.ref]
5 A reference to type “cv1 T1” is initialized by an expression of type
“cv2 T2” as follows:
(5.1) If the reference is an lvalue reference and the initializer expression
The reference is an rvalue reference. 5.1 does not apply.
(5.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. [example omitted]
This applies, the reference is an rvalue-reference.
(5.2.1) If the initializer expression
(5.2.1.1) is an xvalue (but not a bit-field), class prvalue, array prvalue or function lvalue and [...], or
(5.2.1.2) has a class type (i.e., T2 is a class type) [...]
The initializer is an lvalue of type int. 5.2.1 does not apply.
(5.2.2) Otherwise:
(5.2.2.1) If T1 or T2 is a class type [...]
(5.2.2.2) Otherwise, a temporary of type “cv1 T1” is created and copy-initialized (dcl.init) from the initializer expression. The reference is then bound to the temporary.
Finally, 5.2.2.2 applies. However:
If T1 is reference-related to T2:
(5.2.2.3) cv1 shall be the same cv-qualification as, or greater cv-qualification than, cv2; and
(5.2.2.4) if the reference is an rvalue reference, the initializer expression shall not be an lvalue.
T1 and T2 are int (the reference of the return type of f() is removed and used only to determine the value category), so they're reference-related. cv1 and cv2 are both empty. The reference is an rvalue reference, and f() is an lvalue, hence 5.2.2.4 renders the program ill-formed.
The reason why the term "function lvalue" appears in 5.2.1.1 might be related to the problem of "function rvalues" (see, for example, N3010 - Rvalue References as "Funny" Lvalues). There were no function rvalues in C++03, and it seems the committee didn't want to introduce them in C++11. Without rvalue references, I think it's impossible to get a function rvalue. For example, you may not cast to a function type, and you may not return function types from a function.
Probably for consistency, function lvalues can be bound to rvalue references to function types via a cast:
template<typename T>
void move_and_do(T& t)
{
T&& r = static_cast<T&&>(t); // as if moved
}
int i = 42;
move_and_do(i);
move_and_do(f);
But for T being a function type like void(), the value category of static_cast<T&&>(t) is lvalue (there are no rvalues of function type). Hence, rvalue references to function types can bind to function lvalues.
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.