I remember this in a simple manner that the parent class pointer (handle) can access the memory of variables defined in its own class (32-bits of memory for int unsigned b in this case). The derived class pointer can parse through the base class variable memory and its own memory (32+32 bits of memory space for int unsigned b and d in this case).
Remember that the LRM refers to call super.new in the first line for any derived class constructor.
A super.new call shall be the first statement executed in the constructor. This is because the superclass shall be initialized before the current class and, if the user code does not provide an initialization, the compiler shall insert a call to super.new automatically.
So when we create the object of derived class, the first memory chunk is allocated to the base class variables(32-bits 'b' in this case). Then the next memory chunk is allocated to the derived class variables (32-bit 'd' in this case).
By keeping this in mind, when we use base handle=derived object, then the base class handle is allowed to parse through the first 32 bit memory locations that is denoted by int unsigned b. So, B.b works fine.
Now when we try to access B.d, the base class handle "B" is not allowed to access more than 32-bit locations, so it results in an error.
In case of casting, $cast(D2,B), remember that the object is still of derived class which has 64-bit of memory allocation. So, when we do D2.d, the derived class handle is allowed to access all the memory location and it works fine.
On the contrary, if the object is of class B, then this casting will fail since the allocated memory is less (32-bit is allocated for base class object) and derived class handle needs more memory to access. Hence the $cast fails.
A somwehat similar C++ thread is interesting to read in this scenario.
As a side note, the casting is required for generalization of code. The testbench components will have a generalized handle of base class and different derived class objects can be passed which results in different behavior. Refer to this thread for more information.
I know this is not noted a standard procedure to understand this concept, but this trick makes life simple (atleast for me :-)).