Навіть, допустимо, ви чудово знаєте свою платформу, та добре знаєте, як у вас реалізується layout, vtables (і їх розміщення), paddings, etc. То, ви можете не вистрілити собі ніде в ногу і це навіть працюватиме стабільно, але є ще один підводний камінь, що якщо після розіменування B** ми можемо отримати B*, який вказує вже не на потрібний нам derived, а на якогось іншого спадкоємця?
#include <iostream>
struct B {    
    virtual ~B() {}
    virtual void print() = 0;
};
 
struct D : B {
    int i;
    
    void print() override { std::cout << "[D::print] j = " << i << '\n'; }
    virtual void do_nothing() { std::cout << "[D::do_nothing]\n"; }
};
 
struct XXX : B {    
    void print() override { }
    virtual void hack() { std::cout << "[XXX::hack]\n"; }
};
 
int main() {
 
    D* dparr[2]{new D, new D};
    
    dparr[1]->do_nothing();
    
    B** bpp = (B**)dparr; // same as reinterpret_cast<B**>(dparr)
    XXX hacker;
    bpp[1] = &hacker;
    
    dparr[1]->do_nothing(); // calls XXX::hack 
}
https://rextester.com/UTZEY51315
mimik написав:Я тепер нічого не розумію.
 Коротше кажучи, на більшості сучасних платформ та компіляторів код з https://rextester.com/IKSXJW90373 - працюватиме, бо загалом розробники компіляторів теж не дурні, а на більшості систем layout буде очікуваним (тобто у вас UB лише формально, на ділі всеодно все працюватиме), але якщо ви пишете, наприклад, бібліотеку, якою можуть користуватися хтозна де, то боронь Боже вас такий код писати)