Here I will refer to C++20 (draft) wording, because one relevant editorial issue was fixed between C++17 and C++20 and also it is possible to refer to specific sentences in HTML version of the C++20 draft, but otherwise there is nothing new in comparison to C++17.
At first, definitions of pointer values [basic.compound]/3:
Every value of pointer type is one of the following:
— a pointer to an object or function (the pointer is said to point to the object or function), or
— a pointer past the end of an object ([expr.add]), or
— the null pointer value for that type, or
— an invalid pointer value.
Now, lets see what happens in the (char *)&a
expression.
Let me not prove that a
is an lvalue denoting the object of type A
, and I will say ?the object a
? to refer to this object.
The meaning of the &a
subexpression is covered in [expr.unary.op]/(3.2):
if the operand is an lvalue of type T
, the resulting expression is a prvalue of type “pointer to T
” whose result is a pointer to the designated object
So, &a
is a prvalue of type A*
with the value ?pointer to (the object) a
?.
Now, the cast in (char *)&a
is equivalent to reinterpret_cast<char*>(&a)
, which is defined as static_cast<char*>(static_cast<void*>(&a))
([expr.reinterpret.cast]/7).
Cast to void*
doesn't change the pointer value ([conv.ptr]/2):
A prvalue of type “pointer to cv T
”, where T
is an object type, can be converted to a prvalue of type “pointer to cv void
”. The pointer value ([basic.compound]) is unchanged by this conversion.
i.e. it is still ?pointer to (the object) a
?.
[expr.static.cast]/13 covers the outer static_cast<char*>(...)
:
A prvalue of type “pointer to cv1 void
” can be converted to a prvalue of type “pointer to cv2 T
”, where T
is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1.
If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T
, then the resulting pointer value is unspecified.
Otherwise, if the original pointer value points to an object a, and there is an object b of type T
(ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b.
Otherwise, the pointer value is unchanged by the conversion.
There is no object of type char
which is pointer-interconvertible with the object a
([basic.compound]/4):
Two objects a and b are pointer-interconvertible if:
— they are the same object, or
— one is a union object and the other is a non-static data member of that object ([class.union]), or
— one is a standard-layout class object and the other is the first non-static data member of that object, or, if the object has no non-static data members, any base class subobject of that object ([class.mem]), or
— there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.
which means that the static_cast<char*>(...)
doesn't change the pointer value and it is the same as in its operand, namely: ?pointer to a
?.
So, (char *)&a
is a prvalue of type char*
whose value is ?pointer to a
?. This value is stored into char* ptr
variable. Then, when you try to do pointer arithmetic with such a value, namely ptr + offset
, you step into [expr.add]/6:
For addition or subtraction, if the expressions P
or Q
have type “pointer to cv T
”, where T
and the array element type are not similar, the behavior is undefined.
For the purposes of pointer arithmetic, the object a
is considered to be an element of an array A[1]
([basic.compound]/3), so the array element type is A
, the type of the pointer expression P
is ?pointer to char
?, char
and A
are not similar types (see [conv.qual]/2), so the behavior is undefined.