16

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?

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 1
    6. is pretty much crystal clear, isn't it ? Especially since everything above does not apply. – Alexandre C. Jul 02 '12 at 21:39
  • 2
    @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 Jul 02 '12 at 21:41
  • (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. Jul 02 '12 at 21:45
  • 3
    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 Jul 02 '12 at 21:48
  • gcc 4.7.1 rejects it, even with a null pointer literal. – Alexandre C. Jul 02 '12 at 21:48
  • @abanert: I don't think the "base class of" relationship is formally reflexive as used in the C++ Standard, so it at least has to be "other than 3 or 5". Which really weakens the reading. – Ben Voigt Jul 02 '12 at 21:49
  • @AlexandreC.: Exactly. Every compiler I have access to rejects it, even in C++0x/11 mode. That's why I don't think this was an intentional change. It may be a bug in the standard. – abarnert Jul 02 '12 at 21:50
  • @BenVoigt: Good point. And that further reinforces the "ambiguous at best, wrong at worst" conclusion (and, more importantly, the OP's point). – abarnert Jul 02 '12 at 21:51
  • Gcc 4.7.1 allows dynamic casting from `B*` to `B*`, even if `B` is not polymorphic. This is inconsistent with the reading of "otherwise". You have a good point, sir. – Alexandre C. Jul 02 '12 at 21:53
  • @Alexandre C.: I'm not sure what "otherwise" you are referring to, since "from `B*` to `B*`" case is fully covered by 5.2.7/3. – AnT stands with Russia Jul 02 '12 at 22:08
  • @AndreyT: People were suggesting that "otherwise" means "other than 5". The identity cast, appearing in case 3, is the counterpoint to that. – Ben Voigt Jul 02 '12 at 23:29

3 Answers3

3

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

Community
  • 1
  • 1
AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • Given that all of the compilers I have access to give the same error in C++0x/11 mode as in C++98/03 mode, and most of them involve people who were on the committee, I suspect that it wasn't changed intentionally. – abarnert Jul 02 '12 at 21:49
  • Interpreting (4) as a sub-clause of (3) makes no sense. (3) already stated what the result is, it is `v`. Adding the bit about null pointer values is 100% redundant, if the requirements of (3) are met. Still, it's a very good observation about the appearance of `R`. – Ben Voigt Jul 02 '12 at 21:51
  • @abarnert: Yes, but now the wording used in C++11 is confusing for the reasons stated in the OP. The 5.2.7 is know to follow the "ladder if conditions" structure, but that potentially makes one believe that null pointers should be allowed unconditionally. – AnT stands with Russia Jul 02 '12 at 21:51
  • @AndreyT: Yes, I agree that it's confusing. That's why I said, in the comments to the OP, that the wording is "ambiguous at best, wrong at worst" (and why I upvoted the question and your answer). – abarnert Jul 02 '12 at 21:53
  • @Ben Voigt: Yes, you are right. Yet I still don't understand the point of making a reference to `R` in 5.2.7/4. It is not referenced anywhere else in 5.2.7. – AnT stands with Russia Jul 02 '12 at 21:53
3

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.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • I don't think a null-pointer constant can be used an an argument for `dynamic_cast`. `dynamic_cast` allows null-pointer value, but a null-pointer constant cannot be transformed into a null-pointer value without a cast. – AnT stands with Russia Jul 02 '12 at 22:11
  • Edited to fix the example, and remove the last paragraph that I'm less sure of – Jonathan Wakely Jul 02 '12 at 22:39
1

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!

Mikael Persson
  • 18,174
  • 6
  • 36
  • 52
  • This has already been suggested, but the "otherwise" HAS to include point 3. Even your explanation assumes that it does. Furthermore, there are numerous places in the Standard where the interpretation as "else if ... else if ... else" is absolutely correct. And your final sentence concerning case (5) is wrong, the compiler wouldn't reject `static_cast(x)`. – Ben Voigt Jul 02 '12 at 23:30
  • @Ben Voigt: I meant (in point(5)) that it should judge the validity of the cast based on the same criteria as static-cast, I didn't mean that your particular example should be rejected, of course not. – Mikael Persson Jul 02 '12 at 23:35
  • Ah ok, that part makes sense when explained that way. But treating "otherwise" as "other than 5" still fails, because it conflicts with 3. – Ben Voigt Jul 02 '12 at 23:39
  • @BenVoigt Yeah, that's correct, it's just poorly ordered in the standard, but I think it's clear that the intent is that point (3) is the "trivial" case that super-seeds all the rest. I'll edit. But the main point is that the nullity of the pointer-value does not factor into the acceptance / rejection of the cast by the compiler, otherwise it would have to accept all pointer-casts, because there is always the possibility that the value is null. – Mikael Persson Jul 02 '12 at 23:48
  • Doesn't the standard elsewhere say that any cast of the null pointer value has to be a null pointer? If so, point 4 would be redundant if it's just about the value. – abarnert Jul 03 '12 at 00:42
  • 2
    @abarnert: Yes and no. Basically, in each individual cast method specification (static, dynamic, reinterpret, const and implicit pointer conversions), it repeats the fact that nullness should be preserved through the cast. I think they chose to do that instead of creating a separate, special section just to say that all casts preserve nullity of pointers. – Mikael Persson Jul 03 '12 at 00:53