This question is inspired by comments here.
Consider the following code snippet:
struct X {}; // no virtual members
struct Y : X {}; // may or may not have virtual members, doesn't matter
Y* func(X* x) { return dynamic_cast<Y*>(x); }
Several people suggested that their compiler would reject the body of func
.
However, it appears to me that whether this is defined by the Standard depends on the run-time value of x
. From section 5.2.7 ([expr.dynamic.cast]
):
The result of the expression
dynamic_cast<T>(v)
is the result of converting the expressionv
to typeT
.T
shall be a pointer or reference to a complete class type, or "pointer to cvvoid
." Thedynamic_cast
operator shall not cast away constness.
If
T
is a pointer type,v
shall be a prvalue of a pointer to complete class type, and the result is a prvalue of typeT
. IfT
is an lvalue reference type,v
shall be an lvalue of a complete class type, and the result is an lvalue of the type referred to byT
. IfT
is an rvalue reference type,v
shall be an expression having a complete class type, and the result is an xvalue of the type referred to byT
.
If the type of
v
is the same asT
, or it is the same asT
except that the class object type inT
is more cv-qualified than the class object type inv
, the result isv
(converted if necessary).
If the value of
v
is a null pointer value in the pointer case, the result is the null pointer value of typeT
.
If T is "pointer to cv1
B
" andv
has type 'pointer to cv2D
" such thatB
is a base class ofD
, the result is a pointer to the uniqueB
subobject of theD
object pointed to byv
. Similarly, if T is "reference to cv1B
" andv
has type cv2D
such thatB
is a base class ofD
, the result is the uniqueB
subobject of theD
object referred to byv
. The result is an lvalue ifT
is an lvalue reference, or an xvalue ifT
is an rvalue reference. In both the pointer and reference cases, the program is ill-formed if cv2 has greater cv-qualification than cv1 or ifB
is an inaccessible or ambiguous base class ofD
.
Otherwise,
v
shall be a pointer to or an lvalue of a polymorphic type.
If
T
is "pointer to cvvoid
," then the result is a pointer to the most derived object pointed to byv
. Otherwise, a run-time check is applied to see if the object pointed or referred to byv
can be converted to the type pointed or referred to byT
.) The most derived object pointed or referred to byv
can contain otherB
objects as base classes, but these are ignored.
If
C
is the class type to whichT
points or refers, the run-time check logically executes as follows:
If, in the most derived object pointed (referred) to by
v
,v
points (refers) to apublic
base class subobject of aC
object, and if only one object of typeC
is derived from the subobject pointed (referred) to byv
the result points (refers) to thatC
object.
Otherwise, if
v
points (refers) to apublic
base class subobject of the most derived object, and the type of the most derived object has a base class, of typeC
, that is unambiguous andpublic
, the result points (refers) to theC
subobject of the most derived object.
Otherwise, the run-time check fails.
The value of a failed cast to pointer type is the null pointer value of the required result type. A failed cast to reference type throws
std::bad_cast
.
The way I read this, the requirement of a polymorphic type only applies if none of the above conditions are met, and one of those conditions depends on the runtime value.
Of course, in a few cases the compiler can positively determine that the input cannot properly be NULL (for example, when it is the this
pointer), but I still think the compiler cannot reject the code unless it can determine that the statement will be reached (normally a run-time question).
A warning diagnostic is of course valuable here, but is it Standard-compliant for the compiler to reject this code with an error?
A very good point.
Note that in C++03 the wording of 5.2.7/3 and 5.2.7/4 is as follows
3 If the type of v is the same as the required result type (which, for convenience, will be called R in this description), or it is the same as R except that the class object type in R is more cv-qualified than the class object type in v, the result is v (converted if necessary).
4 If the value of v is a null pointer value in the pointer case, the result is the null pointer value of type R.
The reference to type R
introduced in 5.2.7/3 seems to imply that 5.2.7/4 is intended to be a sub-clause of 5.2.7/3. In other words, it appears that 5.2.7/4 is intended to apply only under the conditions described in 5.2.7/3, i.e. when types are the same.
However, the wording in C++11 is different and no longer involves R
, which no longer suggests any special relationship between 5.2.7/3 and 5.2.7/4. I wonder whether it was changed intentionally...
To me, it seems pretty clear cut. I think the confusion comes when you make the wrong interpretation that the enumeration of requirements is an "else if .. else if .." type of thing.
Points (1) and (2) simply define what the static input and output types are allowed to be, in terms of cv-qualification and lvalue-rvalue-prvalue-- etc. So that's trivial and applies to all cases.
Point (3) is pretty clear, if both the input and output type are the same (added cv-qualifiers aside), then the conversion is trivial (none, or just added cv-qualifiers).
Point (4) clearly requires that if the input pointer is null, then the output pointer is null too. This point needs to be made as a requirement, not as a matter of rejecting or accepting the cast (via static analysis), but as a matter of stressing the fact that if the conversion from input pointer to output pointer would normally entail an offset to the actual pointer value (as it can, under multiple-inheritance class hierarchies), then that offset must not be applied if the input pointer is null, in order to preserve the "nullness" of the pointer. This just means that when the dynamic-cast is performed, the pointer is checked for nullity, and if it is null, the resulting pointer must also have a null-value.
Point (5) simply states that if it is an upcast (from derived to base), then the cast is resolved statically (equivalent to static_cast<T>(v)
). This is mostly to handle the case (as the footnote indicates) where the upcast is well-formed, but that there could be the potential for an ill-formed cast if one were to go to the most-derived object pointed to by v (e.g., if v actual points to derived object with multiple base classes in which the class T appears more than once). In other words, this means, if it's an upcast, do it statically, without a run-time mechanism (thus, avoiding a potential failure, where it shouldn't happen). Under this case, the compiler should reject the cast on the same basis as if it was a static_cast<T>(v)
.
In Point (6), clearly, the "otherwise" refers directly to Point (5) (and surely to the trivial case of Point (3)). Meaning (together with Point (7)), that if the cast is not an upcast (and not an identity-cast (Point (3))), then it is a down-cast, and it should be resolved at run-time, with the explicit requirement that the type (of v) be a polymorphic type (has a virtual function).
Your code should be rejected by a standard-compliant compiler. To me, there's no doubts about it. Because, the cast is a down-cast, and the type of v is not polymorphic. It doesn't meet the requirements set out by the standard. The null-pointer clause (point (4)) really has nothing to do with whether it is accepted code or not, it just has to do with preserving a null pointer-value across the cast (otherwise, some implementations could make the (stupid) choice to still apply the pointer-offset of the cast even if the value is null).
Of course, they could have made a different choice, and allowed the cast to behave as a static-cast from base to derived (i.e., without a run-time check), when the base type is not polymorphic, but I think that breaks the semantics of the dynamic-cast, which is clearly to say "I want a run-time check on this cast", otherwise you wouldn't use a dynamic-cast!
I believe the intention of that wording is that some casts can be done at compile-time, e.g. upcasts or dynamic_cast<Y*>((X*)0)
, but that others need a run-time check (in which case a polymorphic type is needed.)
If your code snippet was well-formed it would need a run-time check to see if it's a null pointer value, which contradicts the idea that a run-time check should only happen for the polymorphic case.
See DR 665 which clarified that certain casts are ill-formed at compile-time, rather than postponed to run-time.
This question is inspired by comments here.
Consider the following code snippet:
struct X {}; // no virtual members
struct Y : X {}; // may or may not have virtual members, doesn't matter
Y* func(X* x) { return dynamic_cast<Y*>(x); }
Several people suggested that their compiler would reject the body of func
.
However, it appears to me that whether this is defined by the Standard depends on the run-time value of x
. From section 5.2.7 ([expr.dynamic.cast]
):
The result of the expression
dynamic_cast<T>(v)
is the result of converting the expressionv
to typeT
.T
shall be a pointer or reference to a complete class type, or "pointer to cvvoid
." Thedynamic_cast
operator shall not cast away constness.
If
T
is a pointer type,v
shall be a prvalue of a pointer to complete class type, and the result is a prvalue of typeT
. IfT
is an lvalue reference type,v
shall be an lvalue of a complete class type, and the result is an lvalue of the type referred to byT
. IfT
is an rvalue reference type,v
shall be an expression having a complete class type, and the result is an xvalue of the type referred to byT
.
If the type of
v
is the same asT
, or it is the same asT
except that the class object type inT
is more cv-qualified than the class object type inv
, the result isv
(converted if necessary).
If the value of
v
is a null pointer value in the pointer case, the result is the null pointer value of typeT
.
If T is "pointer to cv1
B
" andv
has type 'pointer to cv2D
" such thatB
is a base class ofD
, the result is a pointer to the uniqueB
subobject of theD
object pointed to byv
. Similarly, if T is "reference to cv1B
" andv
has type cv2D
such thatB
is a base class ofD
, the result is the uniqueB
subobject of theD
object referred to byv
. The result is an lvalue ifT
is an lvalue reference, or an xvalue ifT
is an rvalue reference. In both the pointer and reference cases, the program is ill-formed if cv2 has greater cv-qualification than cv1 or ifB
is an inaccessible or ambiguous base class ofD
.
Otherwise,
v
shall be a pointer to or an lvalue of a polymorphic type.
If
T
is "pointer to cvvoid
," then the result is a pointer to the most derived object pointed to byv
. Otherwise, a run-time check is applied to see if the object pointed or referred to byv
can be converted to the type pointed or referred to byT
.) The most derived object pointed or referred to byv
can contain otherB
objects as base classes, but these are ignored.
If
C
is the class type to whichT
points or refers, the run-time check logically executes as follows:
If, in the most derived object pointed (referred) to by
v
,v
points (refers) to apublic
base class subobject of aC
object, and if only one object of typeC
is derived from the subobject pointed (referred) to byv
the result points (refers) to thatC
object.
Otherwise, if
v
points (refers) to apublic
base class subobject of the most derived object, and the type of the most derived object has a base class, of typeC
, that is unambiguous andpublic
, the result points (refers) to theC
subobject of the most derived object.
Otherwise, the run-time check fails.
The value of a failed cast to pointer type is the null pointer value of the required result type. A failed cast to reference type throws
std::bad_cast
.
The way I read this, the requirement of a polymorphic type only applies if none of the above conditions are met, and one of those conditions depends on the runtime value.
Of course, in a few cases the compiler can positively determine that the input cannot properly be NULL (for example, when it is the this
pointer), but I still think the compiler cannot reject the code unless it can determine that the statement will be reached (normally a run-time question).
A warning diagnostic is of course valuable here, but is it Standard-compliant for the compiler to reject this code with an error?
A very good point.
Note that in C++03 the wording of 5.2.7/3 and 5.2.7/4 is as follows
3 If the type of v is the same as the required result type (which, for convenience, will be called R in this description), or it is the same as R except that the class object type in R is more cv-qualified than the class object type in v, the result is v (converted if necessary).
4 If the value of v is a null pointer value in the pointer case, the result is the null pointer value of type R.
The reference to type R
introduced in 5.2.7/3 seems to imply that 5.2.7/4 is intended to be a sub-clause of 5.2.7/3. In other words, it appears that 5.2.7/4 is intended to apply only under the conditions described in 5.2.7/3, i.e. when types are the same.
However, the wording in C++11 is different and no longer involves R
, which no longer suggests any special relationship between 5.2.7/3 and 5.2.7/4. I wonder whether it was changed intentionally...
To me, it seems pretty clear cut. I think the confusion comes when you make the wrong interpretation that the enumeration of requirements is an "else if .. else if .." type of thing.
Points (1) and (2) simply define what the static input and output types are allowed to be, in terms of cv-qualification and lvalue-rvalue-prvalue-- etc. So that's trivial and applies to all cases.
Point (3) is pretty clear, if both the input and output type are the same (added cv-qualifiers aside), then the conversion is trivial (none, or just added cv-qualifiers).
Point (4) clearly requires that if the input pointer is null, then the output pointer is null too. This point needs to be made as a requirement, not as a matter of rejecting or accepting the cast (via static analysis), but as a matter of stressing the fact that if the conversion from input pointer to output pointer would normally entail an offset to the actual pointer value (as it can, under multiple-inheritance class hierarchies), then that offset must not be applied if the input pointer is null, in order to preserve the "nullness" of the pointer. This just means that when the dynamic-cast is performed, the pointer is checked for nullity, and if it is null, the resulting pointer must also have a null-value.
Point (5) simply states that if it is an upcast (from derived to base), then the cast is resolved statically (equivalent to static_cast<T>(v)
). This is mostly to handle the case (as the footnote indicates) where the upcast is well-formed, but that there could be the potential for an ill-formed cast if one were to go to the most-derived object pointed to by v (e.g., if v actual points to derived object with multiple base classes in which the class T appears more than once). In other words, this means, if it's an upcast, do it statically, without a run-time mechanism (thus, avoiding a potential failure, where it shouldn't happen). Under this case, the compiler should reject the cast on the same basis as if it was a static_cast<T>(v)
.
In Point (6), clearly, the "otherwise" refers directly to Point (5) (and surely to the trivial case of Point (3)). Meaning (together with Point (7)), that if the cast is not an upcast (and not an identity-cast (Point (3))), then it is a down-cast, and it should be resolved at run-time, with the explicit requirement that the type (of v) be a polymorphic type (has a virtual function).
Your code should be rejected by a standard-compliant compiler. To me, there's no doubts about it. Because, the cast is a down-cast, and the type of v is not polymorphic. It doesn't meet the requirements set out by the standard. The null-pointer clause (point (4)) really has nothing to do with whether it is accepted code or not, it just has to do with preserving a null pointer-value across the cast (otherwise, some implementations could make the (stupid) choice to still apply the pointer-offset of the cast even if the value is null).
Of course, they could have made a different choice, and allowed the cast to behave as a static-cast from base to derived (i.e., without a run-time check), when the base type is not polymorphic, but I think that breaks the semantics of the dynamic-cast, which is clearly to say "I want a run-time check on this cast", otherwise you wouldn't use a dynamic-cast!
I believe the intention of that wording is that some casts can be done at compile-time, e.g. upcasts or dynamic_cast<Y*>((X*)0)
, but that others need a run-time check (in which case a polymorphic type is needed.)
If your code snippet was well-formed it would need a run-time check to see if it's a null pointer value, which contradicts the idea that a run-time check should only happen for the polymorphic case.
See DR 665 which clarified that certain casts are ill-formed at compile-time, rather than postponed to run-time.
0 commentaires:
Enregistrer un commentaire