15
votes

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]):

  1. The result of the expression dynamic_cast<T>(v) is the result of converting the expression v to type T. T shall be a pointer or reference to a complete class type, or "pointer to cv void." The dynamic_cast operator shall not cast away constness.

  2. 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 type T. If T 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 by T. If T 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 by T.

  3. If the type of v is the same as T, or it is the same as T except that the class object type in T 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 T.

  5. If T is "pointer to cv1 B" and v has type 'pointer to cv2 D" such that B is a base class of D, the result is a pointer to the unique B subobject of the D object pointed to by v. Similarly, if T is "reference to cv1 B" and v has type cv2 D such that B is a base class of D, the result is the unique B subobject of the D object referred to by v. The result is an lvalue if T is an lvalue reference, or an xvalue if T 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 if B is an inaccessible or ambiguous base class of D.

  6. Otherwise, v shall be a pointer to or an lvalue of a polymorphic type.

  7. If T is "pointer to cv void," then the result is a pointer to the most derived object pointed to by v. Otherwise, a run-time check is applied to see if the object pointed or referred to by v can be converted to the type pointed or referred to by T.) The most derived object pointed or referred to by v can contain other B objects as base classes, but these are ignored.

  8. If C is the class type to which T 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 a public base class subobject of a C object, and if only one object of type C is derived from the subobject pointed (referred) to by v the result points (refers) to that C object.

    • Otherwise, if v points (refers) to a public base class subobject of the most derived object, and the type of the most derived object has a base class, of type C, that is unambiguous and public, the result points (refers) to the C subobject of the most derived object.

    • Otherwise, the run-time check fails.

  9. 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?

3
6. is pretty much crystal clear, isn't it ? Especially since everything above does not apply.Alexandre C.
@AlexandreC.: How can you tell, from the code shown, that (4) does not apply? Note that it says "null pointer value", which may not be known until runtime, not "null pointer literal" or "null pointer constant".Ben Voigt
(I'm not the downvoter). Indeed, I read "null pointer 'literal'" or something along these lines. Are you trying to imply that the compiler should translate the dynamic cast into assert(!v) or something similar in the non polymorphic case ?Alexandre C.
It seems to me that it depends on how you interpret the "Otherwise" in 6. If it means "other than 5", then the code should be rejected. But if it means "other than any of 2-5", then the code can only be rejected if the compiler can prove that it's called on something non-null. I think the writers may have intended the former, based on the C++98 wording and the way most compilers handle this, but the wording seems ambiguous at best, wrong at worst.abarnert
gcc 4.7.1 rejects it, even with a null pointer literal.Alexandre C.

3 Answers

3
votes

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

3
votes

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.

1
votes

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!