I think the class names in your example are a bit confusing. Let's call them Interface
, Base
and Impl
. Note that Interface
and Base
are unrelated.
The C++ Standard defines the C-style cast, called "explicit type conversion (cast notation)" in [expr.cast]. You can (and maybe should) read that whole paragraph to know exactly how the C-style cast is defined. For the example in the OP, the following is sufficient:
A C-style can performs a conversion of one of [expr.cast]/4:
const_cast
static_cast
static_cast
followed by const_cast
reinterpret_cast
reinterpret_cast
followed by const_cast
The order of this list is important, because:
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.
Let's examine your example
Impl impl;
Interface* pIntfc = &impl;
Base* pBase = (Base*)pIntfc;
A const_cast
cannot be used, the next element in the list is a static_cast
. But the classes Interface
and Base
are unrelated, therefore there is no static_cast
that can convert from Interface*
to Base*
. Therefore, a reinterpret_cast
is used.
Additional note: the actual answer to your question is: as there is no dynamic_cast
in the list above, a C-style cast never behaves like a dynamic_cast
.
How the actual address changes is not part of the definition of the C++ language, but we can make an example of how it could be implemented:
Each object of a class with at least one virtual function (inherited or own) contains (read: could contain, in this example) a pointer to a vtable. If it inherits virtual functions from multiple classes, it contains multiple pointers to vtables. Because of empty base class optimization (no data members), an instance of Impl
could look like this:
+=Impl=======================================+
| |
| +-Base---------+ +-Interface---------+ |
| | vtable_Base* | | vtable_Interface* | |
| +--------------+ +-------------------+ |
| |
+============================================+
Now, the example:
Impl impl;
Impl* pImpl = &impl;
Interface* pIntfc = pImpl;
Base* pBase = pImpl;
+=Impl=======================================+
| |
| +-Base---------+ +-Interface---------+ |
| | vtable_Base* | | vtable_Interface* | |
| +--------------+ +-------------------+ |
| ^ ^ |
+==|==================|======================+
^ | |
| +-- pBase +-- pIntfc
|
+-- pimpl
If you instead do a reinterpret_cast
, the result is implementation-defined, but it could result in something like this:
Impl impl;
Impl* pImpl = &impl;
Interface* pIntfc = pImpl;
Base* pBase = reinterpret_cast<Base*>(pIntfc);
+=Impl=======================================+
| |
| +-Base---------+ +-Interface---------+ |
| | vtable_Base* | | vtable_Interface* | |
| +--------------+ +-------------------+ |
| ^ |
+=====================|======================+
^ |
| +-- pIntfc
| |
+-- pimpl +-- pBase
I.e. the address is unchanged, pBase
points to the Interface
subobject of the Impl
object.
Note that dereferencing the pointer pBase
takes us to UB-land already, the Standard doesn't specify what should happen. In this exemplary implementation, if you call pBase->GetType()
, the vtable_Interface*
is used, which contains the SomeMethod
entry, and that function is called. This function doesn't return anything, so in this example, nasal demons are summoned and take over the world. Or some value is taken from the stack as a return value.