This answer by CB Bailey is more accurate from the point of view of language specification. It more clearly explains why accessing shapes[i] is undefined behavior. That answer is quoted below with the pointer variable changed to shapes, Shape as the base class and Rectangle as the derived class (instead of p, Base and Derived from that question).
If you look at the expression shapes[1], shapes is a Shape* (Shape is > a completely-defined type) and 1 is an int, so according to ISO/IEC
14882:2003 5.2.1 [expr.sub] this expression is valid and identical to
*((shapes)+(1)).
Thus, accessing shapes[i] is not ill-formed. However, it is undefined as explained below.
From 5.7 [expr.add] / 5, when an integer is added to a pointer, the
result is only well defined when the pointer points to an element of
an array object and the result of the pointer arithmetic also points
the an element of that array object or one past the end of the array.
shapes, however, does not point to an element of an array object, it
points at the base class sub-object of a Derived object. It is the
Rectangle object that is an array member, not the Shape sub-object.
Note that under 5.7 / 4, for the purposes of the addition operator,
the Shape sub-object can be treated as an array of size one, so
technically you can form the address shapes + 1, but as a "one past the
last element" pointer, it doesn't point at a Shape object and
attempting to read from or write to it will cause undefined behavior.
These paragraphs give a very good idea of how the expression shapes[i] is dealt with.