C++ Logo

std-discussion

Advanced search

Re: Is it valid use reinterpret_cast to form pointer to object?

From: Mykola Garkusha <garkusha.mykola_at_[hidden]>
Date: Thu, 10 Aug 2023 00:05:33 +0100
The way I read the standard, std::launder is more powerful than that.

reinterpret_cast cannot change "pointer value" apart from
pointer-interconvertible types, but combined with std::launder, that is
possible in some circumstances.

For example, while array pointer and its first element are not
pointer-interconvertible, std::launder can make the same effect as long as
the result of std::launder cannot reach "bytes" not accessible to the
source pointer.

this example is OK to me, and you can use a_array_ptr and a_ptr to access A
a's data

> int a[5];
> int(*a_array_ptr)[5] =
> std::launder(reinterpret_cast<int(*)[5]>(&a[0]));
> int* a_ptr = std::launder(reinterpret_cast<int*>(&a));


 Formally, std::launder does impose some reachability requirements. *And
I'd keep storages reachable through result and source as two separate
entities and don't try merging them with reachability requirements as that
would make it look like std::launder only works with
pointer-interconvertible pointers.*

> All bytes of storage that would be reachable through ([basic.compound])
> the result are reachable through p.


Now reachability requirements are these:

> A byte of storage b is reachable through a pointer value that points to
> an object x if there is an object y, pointer-interconvertible with x, such
> that b is within the storage occupied by y, or the immediately-enclosing
> array object if y is an array element.




 For example, let's try to substitute that reachability requirements for
our example int(*a_array_ptr)[5] =
std::launder(reinterpret_cast<int(*)[5]>(&a[0]));

-------------------
For source pointer:
------------------

 A byte of storage "int c" is reachable through a "pointer to first
element of array a" that points to an object a[0] if there is an object
a[0], pointer-interconvertible with a[0], such that storage "int a[5]" is
within the immediately-enclosing array object ******a[5]******* if a[0] is
an array element.

-------------------
For result pointer:
 -------------------
 A byte of storage "int a[5]" is reachable through a pointer "a_array_ptr"
that points to an object "a" if there is an object "a",
pointer-interconvertible with "a", such that storage "int a[5]" is within
the storage occupied by "a" (storage "int a[5]" itself)


 Note in both I use storage "int a[5]" as a proof that the result pointer
does not access more "bytes" in a defined way than the source pointer.


 Even more, I reckon that with the similar reasoning the following code is
valid as well

Precondition: T1 and T2 got the same size of alignment

> T1 storage;
> T2 *ptr_ = ::new (&storage_) T2;
> T2* ptr2 = std::launder(reinterpret_cast<T2 *>(&storage_))
> ptr2->.....


On Tue, Aug 8, 2023 at 11:52 PM Julien Villemure-Fréchette <
julien.villemure_at_[hidden]> wrote:

> I lacked some precision a little bit. I'll use formal terminology.
>
> std::launder has 2 preconditions. The second one relates to reachability,
> which has a strong relationship with respect to pointer
> interconvertiblility.
> "All bytes of storage that would be reachable through ([basic.compound])
> the result are reachable through p."
>
> Now check the definition of reachability
> http://eel.is/c++draft/basic.compound#5
>
> "A byte of storage b is reachable through a pointer value that points to
> an object x if there is an object y, pointer-interconvertible with x, such
> that b is within the storage occupied by y, or the immediately-enclosing
> array object if y is an array element."
>
> In your example, your use of launder would be well defined I think (though
> not the intented use of launder I think).
>
>
>
> On August 8, 2023 4:16:02 p.m. EDT, Mykola Garkusha <
> garkusha.mykola_at_[hidden]> wrote:
>
>> http://eel.is/c++draft/basic#:~:text=If%20two%20objects,end%20note%5D
>> That is what reinterpret_cast says about pointer interconvertible
>>
>> http://eel.is/c++draft/ptr.launder#2 std::launder clearly allows to get
>> a "pointer to T" given *the address* where T's storage starts
>>
>>
>>
>> On Tue, Aug 8, 2023 at 9:05 PM Julien Villemure-Fréchette via
>> Std-Discussion <std-discussion_at_[hidden]> wrote:
>>
>>> This seems strange. std::launder explicitly says it cannot convert a
>>> pointer to 'o1' to a pointer to 'o2' unless 'o1' and 'o2' are pointer
>>> interconvertible, otherwise it is UB.
>>>
>>> from my understanding, I thought that std::launder was only an escape
>>> hatch for dealing with stale references to non transparently replaceable
>>> objects.
>>>
>>>
>>> On August 6, 2023 1:20:00 p.m. EDT, Mykola Garkusha via Std-Discussion <
>>> std-discussion_at_[hidden]> wrote:
>>>
>>>> Thanks, this is super clear actually. Everything settled after deep
>>>> diving into pointer conversions for non-pointer-interconvertible objects.
>>>>
>>>> So then with std::launder, this shall work as it should return pointer
>>>> value "pointer to T"?
>>>>
>>>> T2* ptr2 = std::laudner(reinterpret_cast<T2 *>(&storage_))
>>>> ptr2->value...
>>>>
>>>>
>>>> On Sun, Aug 6, 2023 at 5:39 PM Matthew House via Std-Discussion <
>>>> std-discussion_at_[hidden]> wrote:
>>>>
>>>>> On Sun, Aug 6, 2023 at 10:03 AM Mykola Garkusha
>>>>> <garkusha.mykola_at_[hidden]> wrote:
>>>>> > Just want to clarify this clearly to be 100% sure.
>>>>> >
>>>>> > > Not in general; [basic.lval] para. 11 is specifically about reading
>>>>> > > and writing scalar values through a glvalue, and nothing else.
>>>>> Since
>>>>> > > ptr2 is a T* pointer pointing to an std::byte object, you can't do
>>>>> > > most things with ptr2 that you could do with an arbitrary T*
>>>>> pointer,
>>>>> >
>>>>> > ptr2 should point to T2 now
>>>>> >
>>>>> > https://eel.is/c++draft/expr#reinterpret.cast-7
>>>>> > https://eel.is/c++draft/expr#reinterpret.cast-3
>>>>>
>>>>> That's not how it works in this case, since the two objects 'storage_'
>>>>> and '*ptr_' are not pointer-interconvertible. By
>>>>> [expr.reinterpret.cast] para. 7, 'T* ptr2 =
>>>>> reinterpret_cast<T*>(&storage_)' is equivalent to 'T* ptr2 =
>>>>> static_cast<T*>(static_cast<void*>(&storage_))'. Here, '&storage_'
>>>>> points to the entire 'storage_' array (not to its first element
>>>>> 'storage_[0]', as I mistakenly said earlier), so by [expr.static.cast]
>>>>> para. 4 and [conv.ptr] para. 2, 'static_cast<void*>(&storage_)' also
>>>>> points to the 'storage_' array.
>>>>>
>>>>> For 'T* ptr2 = static_cast<T*>(...)', we have to look at
>>>>> [expr.static.cast] para. 14: *if* there exists an object of type
>>>>> (similar to) T pointer-interconvertible with 'storage_', then 'ptr2'
>>>>> will point to that T object; *otherwise*, 'ptr2' will be a T* pointer
>>>>> pointing to the 'storage_' array. But if we look at the definition of
>>>>> pointer-interconvertible objects in [basic.compound] para. 4, none of
>>>>> those bullets apply for 'storage_' and '*ptr_': they are different
>>>>> objects, neither is a union object, and neither is the first
>>>>> non-static member of the other. Therefore, 'ptr2' is a T* pointer
>>>>> pointing to the std::byte[sizeof(T)] array 'storage_', not a T*
>>>>> pointer pointing to the T object '*ptr_'.
>>>>>
>>>>> > Not sure how strict aliasing (
>>>>> https://eel.is/c++draft/expr#basic.lval-11) can be violated in the
>>>>> example below, as the access (for scalar variables) would be done through
>>>>> dynamic type, as T2 lifetime has started and storage_ provides storage for
>>>>> it.
>>>>> > T2* ptr2 = reinterpret_cast<T2 *>(&storage_)
>>>>> > ptr2->value/*scalar*/;
>>>>> >
>>>>> > > ([expr.ref] para. 8). Most (but not all) usual
>>>>> > > operations require that the dynamic type of the pointee actually
>>>>> > > matches the type of the pointer.
>>>>> >
>>>>> > I don't see how the above violates the sample below.
>>>>> > reinterpret_cast<T2 *>(&storage_)->....
>>>>>
>>>>> Look at [expr.ref] para. 8. The class member access 'ptr2->value' is
>>>>> illegal, regardless of what is done with that glvalue afterwards. The
>>>>> "strict aliasing" rule in [basic.lval] para. 11 only applies *after*
>>>>> that class member access is done, and even then it helps only if
>>>>> 'value' is a char, unsigned char, or std::byte.
>>>>>
>>>>> > The type of expression above is T* and that is what the *actual*
>>>>> result of reintepret_cast is as T2's object address should be equal to the
>>>>> address of storage_ and storage_ provides storage for T2's object which is
>>>>> in turn tested within storage_. Although I agree that std::launder would
>>>>> make it more explicit and I can be wrong on this.
>>>>> https://eel.is/c++draft/basic#intro.object-9
>>>>>
>>>>> The std::byte[sizeof(T)] array does provide storage for the T object,
>>>>> but this is irrelevant here, since the array object and the T object
>>>>> are not pointer-interconvertible. The only importance of providing
>>>>> storage is that the T object is nested within the array by
>>>>> ([intro.object] para. 4), so constructing the T object doesn't end the
>>>>> lifetime of the array ([basic.life] para. 1).
>>>>> --
>>>>> Std-Discussion mailing list
>>>>> Std-Discussion_at_[hidden]
>>>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion
>>>>>
>>>> --
>>> Std-Discussion mailing list
>>> Std-Discussion_at_[hidden]
>>> https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion
>>>
>>

Received on 2023-08-09 23:05:47