Вызов функции-члена нулевой структуры данных, которая была преобразована из несовместимого типа - Undefined?

Существует прямая структура C, объявленная в немодифицируемом заголовке. Я хотел бы «виртуально» добавить к нему удобные функции-члены. Очевидно, что моим первым выбором было бы расширение структуры и добавление методов в производный класс. Не могу, так как сама структура объявлена ​​как "вперед" в заголовке, поэтому я получаю сообщение об ошибке "ошибка: недопустимое использование неполного типа...". Я получаю аналогичную ошибку, если пытаюсь определить новую структуру с одним элементом старой структуры. Это отстой.

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

//defined in header
struct A forward;
void do_something_with_A(A* a, int arg);

//defined in my wrapper
struct B {
  B* wrap(A* a) {return reinterpret_cast<B*>(a); }
  void do_something(int arg) {do_something_with_A(reinterpret_cast<A*>(this),arg); }
}

Если бы я добавил неявные преобразования из типа B в тип A, я думал, что это могло бы работать почти, как если бы B был наследником A с нулевыми данными. Однако это, очевидно, поднимает вопрос: это не определено в С++? Обычно я думаю, что доступ к элементу незаконно приведенной структуры будет неопределенным; это имеет смысл. Тем не менее, я думаю, что было бы хорошо переинтерпретировать_приведение из одного типа в другой, передавая этот указатель, а затем снова возвращая его, не делая ничего незаконного между ними. Я также думаю, что способ, которым компилятор реализует невиртуальные члены структуры, будет создавать функцию

B::do_something(B* b, int arg)

и вызывая это с соответствующим аргументом для B. Затем это сводится к предыдущему случаю, который по моей сомнительной логике в порядке. Поэтому я думаю, что вызов .do_something для структуры, которая на самом деле является reinterpret_cast A, будет в порядке.

Однако это ничего не говорит о том, что на самом деле говорит стандарт C++ по этому вопросу. Любая помощь с этим? Кроме того, если у кого-то есть информация о том, насколько хорошо это будет работать на практике (т. Е. «Каждый когда-либо созданный компилятор принимает это» или «это работает только с несколькими компиляторами»), это также было бы полезно, но немного меньше.


person Jeremy Salwen    schedule 24.08.2011    source источник
comment
Вы не можете использовать static_cast здесь, если вы не добавите соответствующее преобразование в класс B, то есть конструктор, который принимает A*. В любом случае, либо указатель A не несет никакой информации, что означает, что он не имеет значения. Или же он несет некоторую информацию, и в этом случае вы находитесь в reinterpret_cast и UB земле, возможно, подходит для вашего компилятора, возможно, нет.   -  person Cheers and hth. - Alf    schedule 24.08.2011
comment
Кроме того, я считаю, что в С++ structs без элементов данных по-прежнему имеют размер 1.   -  person Chris Lutz    schedule 24.08.2011
comment
Извините, моя путаница в отношении reinterpret_cast и статического приведения запутала проблему. Я обновлю вопрос.   -  person Jeremy Salwen    schedule 24.08.2011
comment
Преждевременная оптимизация — корень всех зол — Дональд Кнут.   -  person Cheers and hth. - Alf    schedule 24.08.2011
comment
Если вы пишете код, который будут использовать другие люди, вы не хотите, чтобы они преждевременно когда-либо оптимизировали его.   -  person Jeremy Salwen    schedule 24.08.2011


Ответы (2)


Я не верю, что это работает, если вы используете static_cast, потому что вы не можете static_cast между двумя совершенно не связанными типами классов. Чтобы быть конкретным, если у вас есть указатель типа A* и вы пытаетесь преобразовать его в указатель типа B*, static_cast преуспевает только в том случае, если это объявление допустимо:

B* ptr(myAPtr);

или если B виртуально не является производным от A (что на самом деле не так). См. спецификацию ISO 5.2.9 для получения подробной информации об этом. Если мы рассмотрим приведенное выше объявление, единственные возможные преобразования, которые могут быть применены здесь во всех 4, — это преобразования в 4.10, а из них единственное, что может быть применимо, — это преобразование из базовых в производные классы (4.10/3), но это здесь не применяется, потому что A и B не являются связанными типами.

Единственное приведение, которое вы можете здесь использовать, это reinterpret_cast, и похоже, что оно тоже не сработает. В частности, поведение приведения через иерархии классов (5.2.10/7)

Указатель на объект может быть явно преобразован в указатель на объект другого типа.65) За исключением преобразования rvalue типа «указатель на T1» в тип «указатель на T2» (где T1 и T2 типов объектов и если требования к выравниванию T2 не более строгие, чем требования T1), и возвращение к исходному типу дает исходное значение указателя, результат такого преобразования указателя не указан.

Таким образом, сразу нет гарантии, что что-то будет работать, если два объекта имеют разные ограничения выравнивания, и вы не можете гарантировать, что это правда. Но предположим, что вы могли бы. В этом случае, однако, я считаю, что это действительно будет работать правильно! Вот рассуждения. Когда вы вызываете функцию-член объекта B, срабатывает правило &5.2.2/1) и говорит, что, поскольку функция не является виртуальной:

[...] Функция, вызываемая при вызове функции-члена, обычно выбирается в соответствии со статическим типом выражения объекта. [...]

Итак, мы по крайней мере вызываем правильную функцию. А как насчет указателя this? Ну, согласно &5.2.2/4:

[...] Если функция является нестатической функцией-членом, параметр «this» функции (9.3.2) должен быть инициализирован указателем на объект вызова, преобразованный, как если бы явным преобразованием типа (5.4 ). [...]

Преобразование типа, выполненное в последней части, — это преобразование идентификатора из B* в B*, поскольку это выбранный тип. Итак, вы вызвали правильную функцию с правильно установленным указателем this. Хороший! Наконец, когда вы выполняете reinterpret_cast возврат к исходному типу, согласно предыдущему правилу, вы возвращаете объект A*, и все идет так, как ожидалось.

Конечно, это работает, только если объекты имеют одинаковые требования к выравниванию, и это не может быть гарантировано. Следовательно, вы не должны этого делать!

Надеюсь это поможет!

person templatetypedef    schedule 24.08.2011
comment
Но там написано, что требования к выравниванию (T2)‹= требования к выравниванию (T!), чтобы это работало. Если я правильно понимаю, что означают требования к выравниванию, я бы подумал, что пустая структура не имеет требований к выравниванию и, следовательно, имеет менее или такие же строгие требования к выравниванию, чем любая структура. - person Jeremy Salwen; 24.08.2011
comment
Но момент, когда вы вызываете do_something на A*, который был переинтерпретирован как B*, совершенно не определен. - person Mark B; 24.08.2011
comment
@Марк: да, конечно. Я планирую скрыть конструкторы B, чтобы B мог быть создан только путем приведения из A (если только вы намеренно не выполняете приведение из какого-либо другого типа к B... но я не могу контролируй это) - person Jeremy Salwen; 24.08.2011
comment
@ Джереми Салвен. Согласно спецификации ($ 3,9 / 3), ограничения выравнивания для объектов зависят от реализации. Соответствующая реализация может, теоретически, выбрать случайное число для использования в качестве выравнивания для каждой структуры или может заставить пустую структуру иметь более строгие ограничения выравнивания, чем любой другой тип. Таким образом, вы не можете обязательно полагаться на то, что это работает правильно. - person templatetypedef; 24.08.2011

Я считаю, что если вы преобразуете A* в B*, а затем снова возвращаете его в A*, то стандарт говорит, что вы в порядке. Это будут reinterpret_casts, но не static_casts.

Но что именно не так с обычным решением?

class B
{
private:
  A* ptr;
public:
  B(A* p) : ptr(p) {}
  void do_something(int arg) { do_something_with_A(ptr,arg); }
};

Кажется, он столь же эффективен, как и ваше решение, и меньше возится.

person john    schedule 24.08.2011
comment
возможно, мне следует уточнить: разрешен ли вызов b.do_something(5), когда b действительно имеет тип A? И причина, по которой я не хочу следовать вашему предложению, заключается в том, что оно не так эффективно, как мое решение. Он не разрешает unique_ptrs в структуре без дополнительного уровня косвенности. - person Jeremy Salwen; 24.08.2011
comment
Хорошо понял. Но теперь я не уверен в ответе. Мой инстинкт говорит, что это не нормально, но, вероятно, сработает. - person john; 24.08.2011
comment
Но ваши требования кажутся любопытными. Вы явно управляете временем жизни объектов A, потому что хотите использовать unique_ptr, но A является анонимным типом, так как же вам удается распределять объекты A? - person john; 24.08.2011
comment
Существуют библиотечные функции как для выделения, так и для освобождения функций типа A. Я оборачиваю их с помощью пользовательского освобождения и статических функций-членов. - person Jeremy Salwen; 24.08.2011