You must be a bit confused about dynamic dispatch.
It is often said that "dynamic dispatch occurs only when you make a call through a pointer or through a reference". Formally speaking, this statement is totally bogus and misleading.
Dynamic dispatch in C++ happens always when you call a virtual function, with one and only one exception: dynamic dispatch is disabled when you use a fully qualified-name of the target function. For example:
some_pointer->SomeClass::some_function(); // fully-qualified name
In the above code the call will be dispatched statically, even if some_function
is a virtual function. From the language point of view, there's no other ways to avoid the dynamic dispatch, i.e. in all other cases all calls to virtual functions are dispatched dynamically. What you use: a pointer, a reference, an immediate object - does not matter, the dispatch is still dynamic. Where you are calling the function from: from constructor/destructor or from somewhere else - does not matter, the dispatch is still dynamic. And I repeat: this is how things are from the point of view of the C++ language itself. This is how an "abstract C++ machine" works.
What happens in practice though, is that in many cases the dynamic dispatch can be replaced with static dispatch, because the compiler knows the dynamic type of the object in advance, at the compile-time and, consequently, knows the target of the dispatch. In such cases it makes more sense to call the target function directly, instead of going through the costlier dynamic dispatch mechanism. Yet, this is nothing else than just an optimization. Some people, unfortunately, mistake that optimization for language-mandated behavior, coming up with such meaningless statements as "dynamic dispatch occurs only when you make a call through a pointer or through a reference".
In your specific case the dispatch is dynamic. Since in your case the compiler does not know the dynamic types of the objects involved, it cannot optimize it into a static dispatch, which is why your code "works as intended".
P.S. Anticipating the possible questions about something I said above: Dynamic dispatch for calls made from constructors/destructors is limited by the current dynamic type of the object, which is why in straightforward cases it is possible (and easy) for the compiler to optimize them into a static dispatch. This is the reason for another popular urban legend that states that virtual calls from constructors/destructors are resolved statically. In reality, in general case they are resolved dynamically, as they should (again, observing the current dynamic type of the object).