What reinterpret_cast doesn't do
In today's post, I will explain one of C++'s biggest pitfalls: reinterpret_cast. Another title for this post could be: This is not the cast you're looking for!
My motivation
My motivation for this blog post comes from multiple training classes I thought over the past several months and a couple of talks I gave. Since C++23, you have a new facility in the Standard Library: std::start_lifetime_as. When teaching class with a focus on embedded environments or presenting talks with such a focus, I started to add std::start_lifetime_as to the material. With an interesting outcome.
The feedback I get is roughly:
- why do I need
std::start_lifetime_as, I already havereinterpret_cast? - why can I use
reinterpret_cast?
If you never heard of start_lifetime_as please consider reading my post, The correct way to do type punning in C++ - The second act.
The most common (misusage) I see
I use the example from my blog post, The correct way to do type punning in C++ - The second act.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
The idea here is that you want to convert a bunch of raw bytes into a known structure. Here of name ConfigValues. Now, together with start_lifetime_as I get in more and more conversations where people tell me that the name reinterpret implies that such code should work as expected. The expectation being that such code is free of undefined behavior and in fact returns a pointer to a ConfigValues object.
While I can't disagree with such an expectation based on the wording of the standard and the C++ object model, such an expectation results in undefined behavior. In a type-safe language an object cannot be converted into an unrelated different one.
The main wording you are looking for is [expr.reinterpret.cast] p7, which says:
An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of object pointer type is converted to the object pointer type “pointer to cv T”, the result is static_cast
(static_cast (v)). [Note 5: Converting a pointer of type “pointer to T1” that points to an object of type T1 to the type “pointer to T2” (where T2 is an object type and the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. — end note]
Firstly, that entire paragraph talks about pointers to objects. Not objects themselves. It says that you can convert a ConfigValues to a void* or any other type that is an object type. What's an object type? Well, everything but a function type, a reference type, and void.
Later in the note, the standard also explicitly blesses that you can do a round-trip. For example:
1 2 3 4 | |
This allows constructs using so-called type-erasure, like std::any. You can obtain an alias of a different pointer type.
If you read that paragraph carefully again, you will see that there is no mention of converting the object itself. Only the pointer can be converted.
In terms of the C++ object model, to have a valid object, it must be created (and later destroyed). But all that ever was created is a ConfigValues object. You can see the reinterpret_cast as a tool that allows you to store a pointer of a different type. Once you want to use it, you must convert the pointer back to its original type.
Let's suppose reinterpret_cast would work as some people expect it:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
By the rules of C++, an object needs to be created and destroyed. If A would create an object, it would need to destroy the grannySmith object. Which would be surprising. Then you would have no way implementing a type-erasure utility like std::any because storing a type-erased void* would destroy the original object in the view of the abstract machinery of C++. Which would allow the compiler to bring on various other optimizations, which would then break your program.
With reinterpret_cast you have a way to alias an object A as another type B, but without the right to ever access a B object via this pointer. On the other hand start_lifetime_as implicitly creates an object a B object at the destination of the pointer A while at the same time ending the lifetime of A.
You often want object lifetime
What you mostly want in these situations is to make an object of a different type become alive. And this is precisely what std::start_lifetime_as is for.
When you apply std::start_lifetime_as to a pointer, the abstract machine understands that you create a new object of that type. Opposed to a call to new or a stack object, no constructor runs. All happens just inside the C++ object model.
There is one more thing std::start_lifetime_as does, when it starts the lifetime of a new object, the lifetime of the source is automatically ended, again without invoking an actual destructor. This is key here.
If I apply std::start_lifetime_as to my previous example, the correct implementation looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Notice B, which restarts the lifetime of the pointer as a Apple object again.
Key takeways
Remember, with reinterpret_cast you only get a pointer conversion. You're not allowed to use that new pointer to access an object from the type.
If you want to start the lifetime of an object to access data as the new type, you need std::start_lifetime_as.
Here is table that summarizes the different conversion tools and their usages:
| Conversion utility | When to use | Standard |
|---|---|---|
reinterpret_cast | Converting the pointer type | C++98 |
std::bit_cast | Obtaining the bit representation of an object | C++20 |
std::start_lifetime_as | Starting the lifetime of an object in a chunk of memory | C++23 |
Andreas
