С++ реинтерпретировать_каст, виртуальный и шаблоны в порядке?

В С++ предполагается следующая иерархия классов:

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() is virtual

person Sebastian Krysmanski    schedule 26.01.2012    source источник
comment
В этом конкретном фрагменте вашего кода я не вижу ошибок.   -  person Renan Greinert    schedule 26.01.2012
comment
Извините, пришлось обновить.   -  person Sebastian Krysmanski    schedule 26.01.2012
comment
Обратите внимание, что ваши BaseClassFactory и ваши ChildClassFactory классы никак не связаны между собой. Кроме того, нет смысла иметь шаблон Factory<T>, потому что каждый экземпляр шаблона представляет собой отдельный, не связанный тип, поэтому виртуальный интерфейс не дает вам вообще никакой выгоды.   -  person Kerrek SB    schedule 26.01.2012
comment
@Kerrek SB: Почему мне это вообще ничего не приносит? Код работает так, как задумано на Visual C++, так что он не может быть совершенно неправильным. Кроме того, поскольку вызов create() разрешается во время выполнения с помощью vtable, это определенно дает мне что-то. Я могу передать экземпляр фабрики с типом Factory<BaseClass>, и его пользователю не нужно знать, какой класс (кроме BaseClass) фактически создан.   -  person Sebastian Krysmanski    schedule 26.01.2012
comment
@SebastianKrysmanski C++-код, который работает, не означает, что он не совсем неправильный. А говорить, что он работает с Visual C++, еще менее важно.   -  person R. Martinho Fernandes    schedule 26.01.2012
comment
@SebastianKrysmanski: Когда я делаю что-то вроде T* t = new T; delete t; t->foo();, и в настоящее время это работает на моем текущем компиляторе, значит ли это, что здесь тоже не может быть совершенно неправильно?   -  person PlasmaHH    schedule 26.01.2012
comment
@PlasmaHH: плохой пример, потому что он явно не работает. Вопрос здесь в том, использует ли мой код неопределенное поведение или нет. Если это UB, то он не портативный. Но если это определенное поведение, мне должно быть позволено сказать, что это работает на моем компиляторе.   -  person Sebastian Krysmanski    schedule 26.01.2012
comment
@SebastianKrysmanski: НЕТ, вы не понимаете, что на самом деле означает UB. Когда он определен, он будет работать везде, а не только в вашем компиляторе. Когда это UB, он может работать или не работать где угодно. И ясно, что ваш код вызывает неопределенное поведение, как указано в моем ответе ниже, что здесь так же хуже, как UB. Вы пытаетесь заставить это работать в моем компиляторе и аргументируете это тем, что это не может быть UB, но затем вы должны применять это везде и не можете просто выбирать определенные вещи, которые вы хотите работать, и указать на других и сказать, что это явно не может работать, потому что это УБ.   -  person PlasmaHH    schedule 26.01.2012
comment
@PlasmaHH: Конечно, я знаю, что означает UB. Когда я написал комментарий выше, я просто еще не был уверен, что эксплуатировал UB. О, у нас разные толкования того, что ясно означает.   -  person Sebastian Krysmanski    schedule 26.01.2012
comment
@SebastianKrysmanski: я имел в виду, что вы можете просто полностью удалить шаблон Factory<T>, и вы получите точно такое же поведение.   -  person Kerrek SB    schedule 26.01.2012
comment
@KerrekSB: Но тогда я не мог вызвать create(), не зная дочернего класса (например, ChildClassFactory). Но это на самом деле то, что я хочу. Возможно, мне не стоило использовать пример, который так похож на хорошо известный шаблон проектирования.   -  person Sebastian Krysmanski    schedule 26.01.2012
comment
@SebastianKrysmanski: Если у вас на самом деле нет нескольких отдельных фабрик для ChildClass, у вас есть ровно один базовый класс для каждого производного класса, что несколько избыточно. Вы можете просто специализировать фабричный класс, если это все, что вам нужно, без использования дополнительного производного класса.   -  person Kerrek SB    schedule 26.01.2012


Ответы (3)


Нет это не так. Вы не можете использовать результат reinterpret_cast, кроме как для обратного отбрасывания, за исключением нескольких особых случаев:

ISO14882:2011(е) 5.2.10-7:

Указатель объекта может быть явно преобразован в указатель объекта другого типа.70 Когда prvalue v типа «указатель на T1» преобразуется в тип «указатель на cv T2», результатом является static_cast(static_cast(v)) если и T1, и T2 являются типами стандартной компоновки (3.9) и требования выравнивания T2 не более строгие, чем требования T1, или если любой из типов недействителен. Преобразование значения prvalue типа «указатель на 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 виртуальных функций, поэтому эти функции будут вызываться. Все это долго после вызова неопределенного поведения. Это как если бы вы прыгали на машине в большой каньон и думали: «Эй, все в порядке» только потому, что еще не коснулись земли.

person PlasmaHH    schedule 26.01.2012
comment
@AndersK: я добавил некоторые пояснения, почему его ситуация тоже потерпит неудачу. - person PlasmaHH; 26.01.2012
comment
Но у Factory нет базового класса, поэтому ваш пример не применим. - person Sebastian Krysmanski; 26.01.2012
comment
хорошо, я согласен с тем, что код @Sebastian Krysmanskis действительно представляет собой неопределенное поведение, однако мне еще предстоит увидеть компилятор, в котором этот код не запустится. Не то, чтобы я мог утверждать, что протестировал их все. Итак, на мой взгляд, ответ на вопрос переносим ли он на другие компиляторы? is: вероятно, но это зависит от поведения, которое не определено в стандарте. - person PeterT; 26.01.2012
comment
@PeterT: Это настолько сильно зависит от таких вещей, как макет виртуального стола, что вы находитесь на очень тонком льду. Функция, которую вызывает baseFactory->create();, называется ChildClassFactory::create. Этот вызов можно быстро уничтожить, добавив виртуальные функции, переместив члены, слегка изменив некоторые типы. Это так же переносимо, как использование встроенного ассемблера. - person PlasmaHH; 26.01.2012
comment
@PlasmaHH: я думаю, что мы где-то здесь. Таким образом, фактический вопрос заключается в том, всегда ли виртуальные таблицы шаблона создаются точно с одинаковым макетом (например, указатель на create всегда является первым в виртуальной таблице) для всех экземпляров шаблона. - person Sebastian Krysmanski; 26.01.2012
comment
@SebastianKrysmanski: Вы уже слишком далеко. vtables — это деталь реализации, и компилятор может их использовать, а может и не использовать; не говоря о том, как компилятор может или не может их компоновать. Возможно, он даже мог бы использовать хеш-таблицу, составленную из сигнатуры функций, кто знает все внутренние детали компилятора? Это также зависит от других вещей, таких как то, что ваш компилятор имеет один и тот же битовый шаблон для указателя производного и базового классов. То, что компиляторы, которые вы использовали до сих пор, делают это таким образом, не означает, что все делают это, или что ваши компиляторы будут продолжать делать это в будущем. - person PlasmaHH; 26.01.2012
comment
@PlasmaHH: Итак, подводя итог: информация (vtable, ...), используемая для отправки виртуального вызова create(), может быть несовместима по битам с экземплярами шаблона, созданными из того же шаблона, верно? - person Sebastian Krysmanski; 26.01.2012
comment
@SebastianKrysmanski: Да, и он может быть несовместим с каким-либо другим типом, поскольку это разные типы. - person PlasmaHH; 26.01.2012

Нет, reinterpret_cast следует использовать только для низкоуровневого кода, поскольку он не будет выполнять правильную манипуляцию адресами. Вместо этого используйте static_cast или dynamic_cast,

Зачем вам две фабрики, это не вписывается в шаблон фабрики GoF.

reinterpret_cast - это не способ сделать это, поскольку он медленный (проверки во время выполнения) и не является хорошим OO-дизайном (вы хотите использовать полиморфизм, встроенный в язык).

Вместо этого создайте конструкторы в фабричном классе, который создает типы, которые вам нужны, а затем вызовите конструктор отдельных типов.

Фабричный шаблон позволяет вам не знать об изменениях в вашей реализации, что хорошо, поскольку вы минимизируете свои зависимости и упрощаете поддержку кода в будущем.

person Damian    schedule 26.01.2012
comment
Нет, он определенно не переносим (хорошо, может быть, в его примере с пустым классом, но даже тогда я не думаю, что он переносим по стандарту, который является единственной существующей переносимостью)! Но и не медленно, так как reinterpret_cast не не выполняет никаких проверок во время выполнения. В процессоре не должно быть никаких операций, компилятор просто переинтерпретирует указатель. - person Christian Rau; 26.01.2012
comment
reinterpret_cast не медленный (проверки во время выполнения), так как это просто хак во время компиляции, говорящий компилятору по-разному интерпретировать битовый шаблон. Проверка во время выполнения не требуется. Кроме того, поскольку Factory<ChildClass> и Factory<BaseClass> являются двумя совершенно не связанными типами, интерпретация битового шаблона одного экземпляра как другого экземпляра не является переносимой. На этот раз это может сработать только потому, что они идентичны, но это не так. - person PlasmaHH; 26.01.2012
comment
@PlasmaHH: они не совсем разные. Они используют один и тот же класс, только аргументы разных типов. Здесь важно то, что метод create() является виртуальным. Поскольку Factory не имеет собственных полей (кроме vtable), интерпретировать не нужно битовых шаблонов. - person Sebastian Krysmanski; 26.01.2012
comment
@SebastianKrysmanski: у вас неправильное представление о шаблонах и классах. Шаблон — это шаблон, а не класс или тип. Только после создания экземпляра он станет типом. Но это никак не связано с типами с другими параметрами шаблона. Foo<int> — это совершенно другой тип, чем Foo<char>. Это становится яснее, если учесть, что можно специализироваться на «Foo‹char›» и сделать его действительно чем-то совершенно другим. - person PlasmaHH; 26.01.2012
comment
@PlasmaHH: Вот почему я написал, что знаю, что бывают ситуации, когда вы не можете использовать reinterpret_cast. У меня есть только ChildClass и BaseClass (и, возможно, другие классы, производные от BaseClass). - person Sebastian Krysmanski; 26.01.2012
comment
@SebastianKrysmanski: отношения наследования параметров шаблона здесь совершенно неуместны. Шаблоны — это просто шаблоны (в стандартном английском значении этого термина), поскольку они предоставляют механизм для создания кода. Вы можете взять свой код и заменить производное от Factory<CC> рукописным классом Factory_CC (и то же самое для BC), оба класса с тем же телом, что и шаблон. С точки зрения C++ это ничего не изменит, но может прояснить, что Factory_CC и Factory_BB совершенно не связаны. - person PlasmaHH; 26.01.2012
comment
@PlasmaHH: Да, но дело в том, что CC и BC имеют один и тот же базовый класс (по крайней мере, это то, что должен показать мой пример). - person Sebastian Krysmanski; 26.01.2012
comment
@SebastianKrysmanski: Итак, вы хотите сказать, что спустя долгое время после вызова UB у вас есть счастливый случай, когда вы получаете указатель, который случайно имеет пригодный для использования битовый шаблон, когда он интерпретируется как неправильный тип в текущей версии вашего компилятора с текущими настройками компиляции? Что ж, если это ваша интерпретация portable, пусть будет так, но для большинства людей в мире, полагающихся на изменчивое поведение UB != Portable - person PlasmaHH; 26.01.2012
comment
@PlasmaHH: Не уверен, что ты имеешь в виду. Какое отношение все это имеет к ходу времени? - person Sebastian Krysmanski; 26.01.2012
comment
@SebastianKrysmanski Суть в том, что в C ++ определенный фрагмент кода может отлично работать на одном компиляторе и давать сбой (или даже заставлять вселенную взорваться) на другом компиляторе (или просто на следующей версии вашего компилятора). Это тот случай, когда ваш код вызывает неопределенное поведение, и в этом случае ваш код неверен или, по крайней мере, не переносим (это то, что вы снова ищу). - person Christian Rau; 26.01.2012
comment
@SebastianKrysmanski И как только вы поймете, что два экземпляра шаблона с разными параметрами шаблона являются совершенно не связанными типами (независимо от того, как могут быть связаны параметры шаблона), вы увидите, что шаблон Factory просто бесполезен, так как не существует нешаблонной базы Factory class, который будет базовым классом для обоих экземпляров шаблона, поэтому виртуальная диспетчеризация не будет работать так, как вы ожидаете. - person Christian Rau; 26.01.2012
comment
@SebastianKrysmanski: Это связано со временем, когда симптомы вызова UB могут проявляться или не проявляться во время вызова UB; С тем же успехом они могут проявиться позже или не проявиться вовсе. - person PlasmaHH; 26.01.2012

Я отметил исходный ответ выше (чтобы отдать ему должное), но я подумал, что подытожу то, что я узнал здесь.

Итак, основная проблема заключается в том, что не определено, как должна быть реализована диспетчеризация виртуальных вызовов.

Это означает, что структуры данных, которые используются внутри для диспетчеризации виртуальных вызовов (например, виртуальные таблицы), могут быть или могут не быть совместимы по битам среди экземпляров шаблона, созданных на основе одного и того же шаблона.

person Sebastian Krysmanski    schedule 26.01.2012