Короткий ответ
- Само по себе приведение указателя static_cast<Derived*>(basePtr) обычно не вызывает немедленного UB — оно просто вычисляет (включая возможную корректировку смещения) значение указателя.
- Но если указатель не указывает на объект требуемого типа, то любое последующее использование (разыменование, доступ к членам, вызов нестатических методов через этот указатель и т.д.) — UB.
- Для ссылок хуже: static_cast<Derived&>(baseRef), когда baseRef не ссылается на объект Derived, — уже при выполнении приведения даёт UB (нельзя получить ссылку на неподходящий объект).
Немного деталей
- static_cast в иерархии наследования может делать "вниз" (base* → derived*) и при этом на этапе выполнения оно просто выдаст указатель, полученный с учётом возможного смещения (для множественного/виртуального наследования). Синтаксически это корректно, но корректность семантики требует, чтобы по исходному base* реально лежел объект Derived (или subobject Derived). В противном случае использование результата — UB.
- Для dynamic_cast к указателю (полиморфные типы) поведение безопаснее: dynamic_cast<Derived*>(basePtr) вернёт nullptr, если объект не того типа (требуется RTTI и полиморфный базовый класс). Для reference dynamic_cast бросит std::bad_cast при неудаче.
- reinterpret_cast и приведения между несвязанными типами не дают никакой «безопасности»: результат — сырая битовая интерпретация, и последующий доступ почти всегда UB (нарушение strict aliasing, выравнивания и т.д.).
Практические решения, если вы не можете включать полный тип потомка
1. Добавить виртуальные методы в базу
- Самое простое: вынести необходимое поведение в виртуальные функции StyleableElement:
- virtual void applyStyle(const Style&) = 0;
- virtual Style readStyle() const = 0;
- Тогда не нужно знать конкретный подкласс при вызове.
2. Паттерн Visitor / accept
- Если набор потомков конечен и известен в одном месте, реализовать accept(Visitor&) и в Visitor иметь перегрузки для каждого класса.
3. Предоставить type-safe downcast на стороне подкласса
- Например, в базовом классе виртуальный метод, возвращающий void* или std::any/variant/типовой идентификатор, который подклассы переопределяют так, чтобы вернуть указатель на внутреннюю реализацию. Это требует аккуратного соглашения, но позволяет избегать прямого включения типов.
4. dynamic_cast (если можно подключить объявления типов)
- Если классы полиморфные и вы можете видеть объявление Derived, dynamic_cast безопасна: вернёт nullptr (для указателей) в случае не того типа.
5. Type erasure / std::any / std::variant
- Если разные подклассы должны предоставлять разные реализации, можно хранить/возвращать объект-обёртку, скрывающую конкретный тип за интерфейсом.
Примеры (для иллюстрации)
- Небезопасный static_cast:
Base* b = /* на самом деле points to Base-only объект */;
Derived* d = static_cast<Derived*>(b); // допустимо синтаксически
d->member; // UB, если b не на Derived
- Безопасный dynamic_cast:
Derived* d = dynamic_cast<Derived*>(b);
if (d) d->member; // безопасно
Рекомендация
Если вы не можете/не хотите подключать определения конкретных подклассов места вызова, лучшая практика — добавить в StyleableElement виртуальные методы, которые инкапсулируют требуемое поведение (чтение/запись стиля). Это наиболее простое, явное и безопасное решение. Если архитектура не позволяет — используйте visitor/type-erasure/динамическую идентификацию с явным контрактом между базой и наследниками.