Skip to content

[expr.const.cast] Use "shall" to impose the requirement CWG2828 #5355

Description

@xmh0511

[expr.const.cast] p3 states

For two similar types T1 and T2, a prvalue of type T1 may be explicitly converted to the type T2 using a const_­cast if, considering the qualification-decompositions of both types, each P1i is the same as P2 i for all i. The result of a const_­cast refers to the original entity.

Presumably, the requirement in this rule should impose on any case that uses the const_cast casting. Even though the conversion would be a standard qualification conversion, it is not supported

typedef int CK[][2];
int main(){
   int arr[2][2]; 
   CK& rf0 = arr;  // ok, qualification conversion 
   CK& rf = const_cast<CK&>(arr);  // ill-formed, the requirement is not satisfied
}

Change [expr.const.cast] p3 to

For any casting from an expression of type T1 to type T2 using a const_cast, considering the qualification-decompositions of both types, each P1i shall be the same as P2 i for all i. The result of a const_­cast refers to the original entity.

The rules defined in the subsequence paragraphs that use const_cast casting all should satisfy this precondition.


Another issue appears in [expr.const.cast] p7: U1 suddenly appears without any introduction. Maybe, change it to

A conversion from a type T1 to a type T2 casts away constness if T1 and T2 are different, there is a qualification-decomposition ([conv.qual]) of T1

cv10 P10 cv11 P11 ... cv1n-1 P1n-1 cv1n U1

yielding n such that T2 has a qualification-decomposition of the form

cv20 P20 cv21 P21 ... cv2n-1 P2n-1 cv2n U2

and there is no qualification conversion that converts T1 to

cv20 P10 cv21 P11 ... cv2n-1 P1n-1 cv2n U1

would be more clear.

Activity

  1. jensmaurer commented on Mar 21, 2022

    @jensmaurer
    Member

    For the first concern, note that [expr.cast] tries a number of casts in turn. Hitting "shall" = ill-formed too soon terminates the if ladder too soon.

  2. xmh0511 commented on Mar 21, 2022

    @xmh0511
    ContributorAuthor

    For the first concern, note that [expr.cast] tries a number of casts in turn. Hitting "shall" = ill-formed too soon terminates the if ladder too soon.

    It seems that const_cast can only convert the types if their corresponding Pi are the same. There seems to have no exception here. The types specified in P4 still should satisfy those requirements. Such as this example:

    typedef int CK[][2];
    int arr[2][2]; 
    const_cast<CK&>(arr);

    which is the case specified by [expr.const.cast] p4.1, where T1 is int [2][2] and T2 is int [][2].

    The qualification-decompositions of them are: pointer to array of 2 array of 2 int vs. pointer to array of unknown bound of array of 2 int.

    where P11 is not the same as P21.

  3. jensmaurer commented on Mar 21, 2022

    @jensmaurer
    Member

    While that's true, it doesn't address the concern: We want "const_cast" simply to not be applicable and the ladder in [expr.cast] to go to the next step (e.g. static_cast). If we make the "const_cast" ill-formed before locking in to that choice, we'll never get to "static_cast" etc. in [expr.cast].

  4. xmh0511 commented on Mar 21, 2022

    @xmh0511
    ContributorAuthor

    we'll never get to "static_cast" etc. in [expr.cast].

    How would static_cast be used in a const_cast conversion? I cannot figure out that example. Would you specify that?

  5. jensmaurer commented on Mar 21, 2022

    @jensmaurer
    Member

    Take this example:

    int p;
    (void *)&p;
    

    [expr.cast] first attempts a const_cast from int* to void *. But that fails, so it attempts a static_cast next. That succeeds, and all is well.

    If, however, we say the attempt to form the const_cast is ill-formed, we'll never reach the static_cast attempt.

  6. xmh0511 commented on Mar 21, 2022

    @xmh0511
    ContributorAuthor

    Alright. I forgot the existence of [expr.cast] p4. Since we mentioned [expr.cast] p4, I think out an example that is not well specified by [expr.cast] p4 but it may be another issue.

    int const p = 0;
    (void*)&p;

    a static_­cast followed by a const_­cast may be a matched interpretation. However, we didn't specify the temporary middle type in the conversion sequence, in other words, whether the conversion be interpreted to:
    const_cast<void*>(static_cast<void*>(&p)) or const_cast<void*>(static_cast<void const*>(&p)), the former is ill-formed since static_cast casts away constness. I remember I saw this issue somewhere, I will cite the question here if I find it.

  7. jensmaurer commented on Mar 21, 2022

    @jensmaurer
    Member

    Yes, the implementation needs to pick a suitable middle type. "shall not cast away constness" is probably the wrong phrasing, then. It should be "cannot cast away constness", it seems.

  8. xmh0511 commented on Mar 21, 2022

    @xmh0511
    ContributorAuthor

    @jensmaurer we may specify the middle type by replacing the U in the qualification-decomposition of the source type. This issue sources from https://stackoverflow.com/questions/39216316/does-the-c-specification-say-how-types-are-chosen-in-the-static-cast-const-cas

  9. jensmaurer commented on Mar 21, 2022

    @jensmaurer
    Member

    We don't need to specify the middle type; if there is any, the cast works. The implementation can figure that out.

  10. xmh0511 commented on Mar 22, 2022

    @xmh0511
    ContributorAuthor

    We don't need to specify the middle type; if there is any, the cast works. The implementation can figure that out.

    Does it mean the result is implementation-defined? Since only implementation clearly knows how the conversion chain works.

  11. jensmaurer commented on Mar 22, 2022

    @jensmaurer
    Member

    Does it mean the result is implementation-defined?

    No. The underlying assumption is that the end result is the same, regardless of which middle type was chosen. Do you have a counterexample?

  12. xmh0511 commented on Mar 22, 2022

    @xmh0511
    ContributorAuthor

    Do you have a counterexample?

    Consider this example:

    struct B0{
    	int b0;
    };
    struct B1{
    	int b1;
    };
    
    struct D:B0,B1{
    };
    
    struct Trick{
    	Trick() = default;
    	Trick(std::any){
    
    	}
    	template<class T>
    	operator T(){
    		return T{0};
    	}
    };
    int main(){
       D const* Dptr = new D;
       B1* ptr = (B1*)Dptr; // #1
    }

    For the cast notation conversion that occurs at #1, the options can be these

    Sane options

    const_cast<D*>(Dptr);  // a expected result
    
    const_cast<B1*>(static_cast<B1 const*>(Dptr)); // a expected result
    
    const_cast<D*>(static_cast<D const*>(Dptr));  // a possible option, the standard conversion can apply to the result
    
    const_cast<B1*>(reinterpret_cast<B1 const*>(Dptr));  // the result is the same as the value of Dptr

    Insane options

    static_cast<Trick>(Dptr); // insane but it is true since we didn't specify the middle type

    Maybe, there are some other weird options. And in the sane options group, there are two static_­cast followed by a const_­cast, the program can be ill-formed as long as the implementation prefers. Furthermore, how to interpret the conversion relies on what the middle type the implementations would like. what about an insane implementation that selects the Trick as a middle type? That means, static_cast<Trick>(Dptr); is the first option in the list.


    Incidentally

    If a conversion can be interpreted in more than one way as a static_­cast followed by a const_­cast

    This is unclear how more than one interpretation intends? Does it mean there is more than one interpretation in the situation where the middle type should be the same or arbitrary middle types?

  13. jensmaurer commented on Mar 22, 2022

    @jensmaurer
    Member

    If a conversion can be interpreted in more than one of the ways listed above, the interpretation that appears first in the list is used, even if a cast resulting from that interpretation is ill-formed.

    Thus, the list is an ordered list: if a single const_cast suffices, we'll take that. So, we pick the const_cast<D*>(Dptr) interpretation.

    The sentence doesn't talk about middle types, so it's for any middle type.

  14. xmh0511 commented on Mar 22, 2022

    @xmh0511
    ContributorAuthor

    The sentence doesn't talk about middle types, so it's for any middle type.

    Only when the middle type is D* will const_cast<D*>(Dptr) be firstly picked. Why wouldn't the implementation picks Trick as the middle type and thus static_cast<Trick>(Dptr); is the first picked one?

    Or, do you mean for a given set S of possible middle types, which may be { Trick, D*, ...}, const_cast<D*>(Dptr) first appears in the list, so it is picked by implementation?

    However, it still cannot interpret the program will be ill-formed since there is more than one way as a static_­cast followed by a const_­cast for that given set S.
    const_cast<B1*>(static_cast<B1 const*>(Dptr)); and const_cast<D*>(static_cast<D const*>(Dptr)); Should we change that to

    If a conversion can be interpreted in more than one way as a static_­cast followed by a const_­cast that is used, the conversion is ill-formed.

    which implies that a static_­cast followed by a const_­cast first appears in the list for the given set S.

  15. jensmaurer commented on Mar 22, 2022

    @jensmaurer
    Member

    For the first (const_cast) option, there is no middle type. Anyway, my interpretation is that you try the first option, iterating through all possibilities if necessary. If you can form a conversion ("can be converted"), you pick that conversion (which might end up being ill-formed nonetheless). For your example, we never look at the second and further options.

  16. 7 remaining items

  17. xmh0511 commented on Mar 22, 2022

    @xmh0511
    ContributorAuthor

    Instead of giving too much free room for implementation, It is better to minimum limit the types used in the conversion if possible.

    we want all the involved casts to convert to a type similar to the type given as the type-id of the cast-expression

    It seems that we just want any (middle) type used in static_cast or reinterpret_cast followed by a const_cast with the type as the following:

    If The qualification-decomposition of the type T1 of the operand in the cast-expression is

    cv10 P10 cv11 P11 ... cv1n-1 P1n-1 cv1n U1

    yielding n (where n≥1 ) and the qualification-decomposition of the type T2 of the type-id in the cast-expression is

    cv20 P20 cv21 P21 ... cv2m-1 P2m-1 cv2m U2

    yielding m (where m≥1) such that a type T3 is

    cv30 P20 cv31 P21 ... cv3m-1 P2m-1 cv3m U2

    Conversion from T1 to T3 does not cast away constness

    An implementation is permitted to chose a type T4 provide that a prvalue of T3 can be converted to T4 with a qualification conversion.

  18. jensmaurer commented on Mar 22, 2022

    @jensmaurer
    Member

    I'd really like to limit the cv and P ugly strings to a single subclause. Why is requiring "similar" not good enough?

  19. xmh0511 commented on Mar 23, 2022

    @xmh0511
    ContributorAuthor

    Why is requiring "similar" not good enough?

    Because, although the (middle) type is required to similar to type-id such that const_cast can be done, however, we have many cases that the type of the operand in a cast-expression is not similar to type-id. For example:

    int const* const* ptr = 0;
    (char* const* const*)ptr;

    int const* const* is not similar to char* const* const* at all. Or, consider the inheritance case

    struct B{};
    struct D:B{};
    D const d;
    (B*) &d;

    the type D const* is neither similar to B* nor B const*. I think [expr.const.cast] p7 has converted what we want here. In short, without cv and P ugly strings,

    the (middle) type T3 should be similar to type-id and conversion from T1 to T3 does not cast away constness.

    And we can give the creative space to implementations as long as they chose a type T4 such that T3 can be converted to T4 with a qualification conversion or T4 is just T3.

  20. jensmaurer commented on Mar 23, 2022

    @jensmaurer
    Member

    I was trying to suggest that any middle type employed in the sequence of casts be similar to type-id.
    Yes, I understand that the type of the operand is likely not similar to anything.

  21. xmh0511 commented on Mar 23, 2022

    @xmh0511
    ContributorAuthor

    I was trying to suggest that any middle type employed in the sequence of casts be similar to type-id.

    Ah, I misunderstood what you said in the above comment. Yes, you're totally right, any (middle) type(s) used in the conversion should be similar to type-id(the destination type), and to not cast away constness have been required in each subclause except [expr.const.cast]. So, we only require that any middle type and type-id are similar is good enough.

  22. jensmaurer commented on Aug 24, 2022

    @jensmaurer
    Member

    For your example #5355 (comment) , I think there is a fundamental misunderstanding: We can't rely on any implicit conversions on the result of the static_cast or const_cast. And the operand of a const_cast must be a type similar to the target type of the const_cast.

    I'm wondering if this issue can be resolved by simply saying that the last cast in each sequence in [expr.cast] p4 uses the type denoted by type-id as its target type.

  23. jensmaurer commented on Aug 24, 2022

    @jensmaurer
    Member

    Oh, and I would be interested in seeing an example for the "ambiguous static_cast / const_cast" case where an earlier case in the list is actually selected, with the understanding of the target type presented in the previous comment. If there is such a case, the linked pull request should be pursued.

  24. xmh0511 commented on Aug 25, 2022

    @xmh0511
    ContributorAuthor

    I'm wondering if this issue can be resolved by simply saying that the last cast in each sequence in [expr.cast] p4 uses the type denoted by type-id as its target type.

    Yes, I think this is necessary to add to the pull request to avoid a misunderstanding similar to #5355 (comment)

    After the restriction to say the type of the last conversion should be the type-id, then for the concern mentioned in #5355 (comment), I'm wondering whether it is necessary to emphasize say the employed type of the first conversion in the conversion sequence formed by static_cast followed by const_cast or reinterpret_­cast followed by a const_­cast should be similar to type-id or not. the second conversion formed by const_cast can only process two similar types. If static_cast followed by const_cast or reinterpret_­cast followed by a const_­cast is used, this implies that the employed type in the middle conversion must be a similar type to type-id(the type that be restricted in the last conversion), otherwise, such a conversion cannot be formed.


    Oh, and I would be interested in seeing an example for the "ambiguous static_cast / const_cast" case where an earlier case in the list is actually selected, with the understanding of the target type presented in the previous comment.

     int**** ptr = 0;
     auto t = (int const*const*const*const*)ptr;

    this can be interpreted as

    1. static_cast<int const * const * const * const * >(ptr);
    2. const_cast<int const * const * const * const * >(static_cast<int * * * const * >(ptr));
    3. const_cast<int const * const * const * const * >(static_cast<int * * const * const * >(ptr));
    4. const_cast<int const * const * const * const * >(static_cast<int * const * const * const * >(ptr));
    5. const_cast<int const * const * const * const *>(static_cast<int const * const * const * const *>(ptr));

    From 2 to 5, they all be a static_­cast followed by a const_­cast, hence they are ambiguous, however, option 1 is an earlier case in the list, regardless of how many interpretations of a static_­cast followed by a const_­cast can have, it does not matter since the options that follow 1 are not used.

  25. jensmaurer commented on Aug 27, 2022

    @jensmaurer
    Member

    Thanks.

  26. jensmaurer commented on Nov 22, 2023

    @jensmaurer
    Member
  27. changed the title [-][expr.const.cast] Use "shall" to impose the requirement[/-] [+][expr.const.cast] Use "shall" to impose the requirement CWG2828[/+] on Nov 22, 2023
  28. added
    cwgIssue must be reviewed by CWG.
    not-editorialIssue is not deemed editorial; the editorial issue is kept open for tracking.
    on Nov 22, 2023
  29. frederick-vs-ja commented on Apr 28, 2024

    @frederick-vs-ja
    Contributor

    Should have been fixed by #6906 (0e026f5).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

cwgIssue must be reviewed by CWG.not-editorialIssue is not deemed editorial; the editorial issue is kept open for tracking.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions