C++ reinterpret_cast, virtual и шаблоны хорошо?
В C++ предположим следующую иерархию классов:
class BaseClass { };
class ChildClass : public BaseClass { };
Далее предположим, что фабричные классы для этих двух классов имеют общий шаблонный базовый класс:
template<typename T>
class Factory {
public:
virtual T* create() = 0;
};
class BaseClassFactory : public Factory<BaseClass> {
public:
virtual BaseClass* create() {
return new BaseClass(&m_field);
}
private:
SomeClass m_field;
};
class ChildClassFactory : public Factory<ChildClass> {
public:
virtual ChildClass* create() {
return new ChildClass(&m_field);
}
private:
SomeOtherClass m_field; // Different class than SomeClass
};
Обратите внимание, что размер / внутренняя структура ChildClassFactory
а также BaseClassFactory
отличается из-за их различных полей.
Теперь, если есть экземпляр ChildClassFactory
(или же Factory<ChildClass>
), могу ли я безопасно бросить его Factory<BaseClass>
(с помощью reinterpret_cast
)?
Factory<ChildClass>* childFactory = new ChildClassFactory();
// static_cast doesn't work - need to use reinterpret_cast
Factory<BaseClass>* baseFactory = reinterpret_cast<Factory<BaseClass>*>(childFactory);
// Does this work correctly? (i.e. is "cls" of type "ChildClass"?)
BaseClass* cls = baseFactory->create();
Я знаю, что вы не всегда можете кастовать шаблонные классы таким образом, но в этом особом случае приведение должно быть безопасным, не так ли?
Я протестировал его с Visual C++ 2010, и он работает. Мой вопрос сейчас таков: переносим ли это другие компиляторы?
Обновление: так как произошла некоторая путаница, позвольте мне уточнить еще кое-что (что должно быть) важно в моем примере:
ChildClass
это детский классBaseClass
- Пользователь
Factory<BaseClass>
не знает, какой дочерний классBaseClass
будет создан. Все, что он знает, этоBaseClass
создано. Factory<T>
не имеет собственных полей (кроме vtable).Factory::create()
являетсяvirtual
3 ответа
Нет. Вы не можете использовать результат reinterpret_cast
кроме как отбрасывать вещи обратно, за исключением нескольких особых случаев:
ISO14882: 2011 (e) 5.2.10-7:
Указатель на объект может быть явно преобразован в указатель на объект другого типа.70 Когда значение v типа "указатель на T1" преобразуется в тип "указатель на cv T2", результатом является static_cast(static_cast(v)) если и T1, и T2 являются типами стандартной компоновки (3.9) и требования к выравниванию T2 не являются более строгими, чем требования для T1, или если какой-либо из типов является недействительным. Преобразование значения типа "указатель на T1" в тип "указатель на T2" (где T1 и T2 являются типами объектов, а требования к выравниванию для T2 не более строгие, чем требования для T1) и обратно к исходному типу, дает исходный значение указателя Результат любого другого такого преобразования указателя не определен.
Чтобы сделать возможный сценарий сбоя более понятным, рассмотрим множественное наследование, где используется static_cast
или же dynamic_cast
иногда будет корректировать значение указателя, но reinterpret_cast
не буду. Рассмотрим приведение в этом примере из A*
в B*
:
struct A { int x; };
struct B { int y; };
struct C : A, B { };
Чтобы понять, как ваш код не работает по-другому, рассмотрим, как большинство компиляторов реализуют механизм вызова виртуальных функций: с виртуальными указателями. Ваш экземпляр ChildClassFactory
будет иметь виртуальный указатель, указывающий на виртуальную таблицу ChildClassFactory
, Теперь, когда ты reinterpret_cast
этот зверь просто случайно "работает", потому что компилятор ожидает некоторый виртуальный указатель, указывающий на виртуальную таблицу, которая будет иметь такую же / подобную структуру. Но он все равно будет содержать значения, указывающие на ChildCLassFactory
виртуальные функции, таким образом, эти функции будут вызваны. Все это происходит долго после вызова неопределенного поведения. Это как если бы вы прыгали с машиной в большой каньон и думали: "Привет, все едет нормально" только потому, что вы еще не упали.
Я поставил галочку в оригинальном ответе выше (чтобы отдать ему должное), но я решил подвести итог тому, что я здесь узнал.
Таким образом, основная проблема заключается в том, что не определено, как следует реализовывать диспетчеризацию виртуальных вызовов.
Это означает, что структура (и) данных, которые внутренне используются для диспетчеризации виртуальных вызовов (например, vtables), могут быть или не быть бит-совместимыми среди экземпляров шаблона, созданных из одного и того же шаблона.
Нет, reinterpret_cast должен использоваться только для низкоуровневого кода, так как он не будет выполнять правильную манипуляцию с адресом. Вместо этого используйте static_cast или dynamic_cast,
Почему вы хотите две фабрики, которые не вписываются в фабричную модель GoF.
reinterpret_cast не способ сделать это, так как он медленный (проверки во время выполнения) и не является хорошим ОО-дизайном (вы хотите использовать сборку polymophisme для языка).
Вместо этого создайте конструкторы в классе фабрики, который производит типы, за которыми вы хотите, и затем вызовите конструктор отдельных типов.
Фабричный шаблон позволяет вам не знать об изменениях в вашей реализации, и это хорошо, поскольку вы минимизируете свои зависимости и упрощаете сопровождение в будущем кода.