Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
xmh0511 opened this issue Mar 21, 2022 · 31 comments · May be fixed by #5357
Open

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

xmh0511 opened this issue Mar 21, 2022 · 31 comments · May be fixed by #5357
Assignees
Labels
cwg Issue must be reviewed by CWG. not-editorial Issue is not deemed editorial; the editorial issue is kept open for tracking.

Comments

@xmh0511
Copy link
Contributor

xmh0511 commented Mar 21, 2022

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

@jensmaurer
Copy link
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.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 21, 2022

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.

@jensmaurer
Copy link
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].

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 21, 2022

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?

@jensmaurer
Copy link
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.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 21, 2022

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.

@jensmaurer
Copy link
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.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 21, 2022

@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

@jensmaurer
Copy link
Member

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

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

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.

@jensmaurer
Copy link
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?

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

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?

@jensmaurer
Copy link
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.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

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.

@jensmaurer
Copy link
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.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

How about this suggestion?

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

Although the first const_cast option is picked since it first appears in the list, however, the example still can be interpreted in more than one way as a static_­cast followed by a const_­cast. const_cast<B1*>(static_cast<B1 const*>(Dptr)); and const_cast<D*>(static_cast<D const*>(Dptr));.

@jensmaurer
Copy link
Member

So, the net concern is that the relationship of the two sentences after the bullets in [expr.cast] p4 is unclear.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

Yes. I think the change is reasonable. If there is more than one of the same kind of interpretations, the program is ill-formed because of the ambiguity(which one is selected). However, if there is an unambiguity option that first appears in the list and thus is used, it is regardless of whether the number of the interpretation that appears in the below is, it does not matter.

@jensmaurer jensmaurer self-assigned this Mar 22, 2022
@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

@jensmaurer I figure out a simple counterexample

struct Trick{
  Trick() = default;
  template<class T>
  Trick(T){}

  template<class T>
    operator T(){
    return T{0};
  }
};
int main(){
   int a = 0;
   char* ptr = (char*)&a
}

Anyway, const_cast is not a correct interpretation, static_cast<Trick>(&a) thus first appears in the list while the expected interpretation reinterpret_cast<char*>(&a) appears after the former.

@jensmaurer
Copy link
Member

jensmaurer commented Mar 22, 2022

I think we want all the involved casts to convert to a type similar to the type given as the type-id of the cast-expression. And we want the last conversion in each bullet to convert to the type-id.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

I think we want all the involved casts to convert to a type similar to the type given as the type-id of the cast-expression. And we want the last conversion in each bullet to convert to the type-id.

Yes. That's the reason why I want to ask for some restrictions/requirements to the possible used types in the cast notation conversion. And it is exactly why I think the result may be implementation-defined If we do not specify some requirements

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 22, 2022

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.

@jensmaurer
Copy link
Member

jensmaurer commented Mar 22, 2022

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

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 23, 2022

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.

@jensmaurer
Copy link
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.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Mar 23, 2022

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.

@jensmaurer
Copy link
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.

@jensmaurer
Copy link
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.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Aug 25, 2022

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.

@jensmaurer
Copy link
Member

Thanks.

@jensmaurer
Copy link
Member

CWG2828

@jensmaurer jensmaurer changed the title [expr.const.cast] Use "shall" to impose the requirement [expr.const.cast] Use "shall" to impose the requirement CWG2828 Nov 22, 2023
@jensmaurer jensmaurer added cwg Issue must be reviewed by CWG. not-editorial Issue is not deemed editorial; the editorial issue is kept open for tracking. labels Nov 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cwg Issue must be reviewed by CWG. not-editorial Issue is not deemed editorial; the editorial issue is kept open for tracking.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants