Что происходит в макросе offsetoff? - PullRequest
2 голосов
/ 03 октября 2009

Среда выполнения Visual C ++ 2008 C предлагает оператор 'offsetof', который фактически является макросом, определенным следующим образом:

#define offsetof(s,m)   (size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))

Это позволяет рассчитать смещение переменной-члена m в классе s.

Что я не понимаю в этой декларации:

  1. Почему мы вообще приводим m к чему-либо, а затем разыменовываем его? Разве это не сработало бы так же хорошо:

    & (((S *) 0) -> м)

  2. В чем причина выбора ссылки на символ (char&) в качестве цели приведения?

  3. Зачем использовать летучие? Есть ли опасность того, что компилятор оптимизирует загрузку m? Если да, то каким образом это могло произойти?

Ответы [ 5 ]

2 голосов
/ 03 октября 2009

Смещение в байтах. Таким образом, чтобы получить число, выраженное в байтах, вы должны преобразовать адреса в char, потому что это тот же размер, что и байт (на этой платформе).

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

Обновление:

Если мы посмотрим на определение макроса:

(size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))

С удаленным приведением к символу это будет:

(size_t)&((((s *)0)->m))

Другими словами, получить адрес члена m в объекте с нулевым адресом, который на первый взгляд выглядит хорошо. Таким образом, должен быть какой-то способ, которым это могло бы вызвать проблему.

Одна вещь, которая приходит на ум, заключается в том, что оператор & может быть перегружен для любого типа m. Если это так, этот макрос будет выполнять произвольный код для «искусственного» объекта, который находится где-то очень близко к нулевому адресу. Это, вероятно, приведет к нарушению доступа.

Этот вид злоупотребления может выходить за пределы применения offsetof, которое предполагается использовать только с типами POD. Возможно, идея заключается в том, что лучше возвращать нежелательное значение, чем аварийное завершение.

(Обновление 2: как Стив указал в комментариях, с operator -> подобной проблемы не будет)

1 голос
/ 03 октября 2009

offsetof - это то, с чем нужно быть очень осторожным в C ++. Это реликт от C. В наши дни мы должны использовать указатели членов. Тем не менее, я считаю, что указатели на элементы данных перегружены и повреждены - я на самом деле предпочитаю offsetof.

Несмотря на это, offsetof полон неприятных сюрпризов.

Во-первых, для ваших конкретных вопросов, я подозреваю, что реальная проблема заключается в том, что они адаптировались относительно традиционного макроса C (который, как я думал, был задан в стандарте C ++). Они, вероятно, используют reinterpret_cast для "это C ++!" причины (так почему (size_t) приведение?), и char & вместо char *, чтобы попытаться немного упростить выражение.

Приведение к типу char выглядит избыточно в этой форме, но, вероятно, это не так. (size_t) не эквивалентен reinterpret_cast, и если вы попытаетесь привести указатели к другим типам в целые числа, вы столкнетесь с проблемами. Я не думаю, что компилятор даже позволяет это, но, честно говоря, я страдаю от сбоя памяти ATM.

Тот факт, что char является однобайтовым типом, имеет некоторую актуальность в традиционной форме, но это может быть только из-за того, что приведение снова верное. Честно говоря, я помню, что бросил в void *, затем char *.

Кстати, если у вас возникли проблемы с использованием специфических для C ++ вещей, они действительно должны использовать std :: ptrdiff_t для окончательного приведения.

В любом случае, возвращаясь к неприятным сюрпризам ...

VC ++ и GCC, вероятно, не будут использовать этот макрос. IIRC, они имеют встроенный компилятор, в зависимости от опций.

Причина в том, чтобы делать именно то, для чего предназначено offsetof, а не то, что делает макрос, что надежно в C, но не в C ++. Чтобы понять это, подумайте, что произойдет, если ваша структура использует множественное или виртуальное наследование. В макросе, когда вы разыменовываете нулевой указатель, вы пытаетесь получить доступ к указателю виртуальной таблицы, которого нет в нулевом адресе, что означает, что ваше приложение, вероятно, дает сбой.

По этой причине некоторые компиляторы имеют встроенную функцию, которая просто использует указанную структуру структур вместо попытки определить тип времени выполнения. Но стандарт C ++ не предписывает и даже не предлагает этого - он существует только по соображениям совместимости с C. И вам все равно нужно быть осторожным, если вы работаете с иерархиями классов, потому что, как только вы используете множественное или виртуальное наследование, вы не можете предполагать, что макет производного класса соответствует макету базового класса - вы должны убедиться, что смещение действительно для точного типа времени выполнения, а не только для конкретной базы.

Если вы работаете с библиотекой структуры данных, возможно, используете единичное наследование для узлов, но приложения не могут видеть или использовать ваши узлы напрямую, offsetof работает хорошо. Но, строго говоря, даже тогда, есть ошибка. Если ваша структура данных находится в шаблоне, узлы могут иметь поля с типами из параметров шаблона (содержащийся тип данных). Если это не POD, технически ваши структуры тоже не POD. И все стандартные требования для offsetof - это то, что он работает для POD. На практике это будет работать - ваш тип не получил виртуальную таблицу или что-то еще только потому, что у него нет члена POD - но у вас нет никаких гарантий.

Если вы знаете точный тип времени выполнения при разыменовании с использованием смещения поля, вы должны быть в порядке даже при множественном и виртуальном наследовании, но ТОЛЬКО если компилятор обеспечивает внутреннюю реализацию offsetof для получения этого смещения в первую очередь , Мой совет - не делай этого.

Зачем использовать наследование в библиотеке структур данных? Ну как на счет ...

class node_base                       { ... };
class leaf_node   : public node_base  { ... };
class branch_node : public node_base  { ... };

Поля в node_base автоматически совместно используются (с одинаковой компоновкой) как в листе, так и в ветви, что позволяет избежать распространенной ошибки в C при случайно разных макетах узла.

Кстати - с помощью такого рода вещей можно избежать смещения. Даже если вы используете offsetof для некоторых заданий, у node_base могут быть виртуальные методы и, следовательно, виртуальная таблица, если для разыменования переменных-членов не требуется. Следовательно, node_base может иметь чисто виртуальные методы получения, установки и другие методы. Обычно это именно то, что вы должны сделать. Использование offsetof (или указателей на элементы) является сложным, и его следует использовать только в качестве оптимизации, если вы знаете, что оно вам нужно. Например, если ваша структура данных находится в файле на диске, она вам определенно не нужна - несколько накладных расходов на виртуальные вызовы будут незначительными по сравнению с накладными расходами на доступ к диску, поэтому любые усилия по оптимизации должны сводиться к минимизации доступа к диску.

Хммм - там немного по касательной. Упс.

0 голосов
/ 25 сентября 2012

2. В чем причина выбора ссылки на символ (char &) в качестве цели приведения?

если тип s имеет оператор & перегружен, то мы не можем получить адрес, используя & s

поэтому мы переинтерпретируем тип s в тип примитива char, потому что тип примитива char не имеет оператора и перегружен

теперь мы можем получить адрес от этого

если в C то reinterpret_cast не требуется

3. Зачем использовать летучие? Есть ли опасность того, что компилятор оптимизирует загрузку m? Если да, то каким образом это могло произойти?

здесь volatile не имеет отношения к оптимизации компилятора.

если у типов s есть const, volatile или оба квалификатора, то reinterpret_cast не может быть приведен к char &, потому что reinterpret_cast не может удалить cv-квалификаторы

, поэтому результат использует для работы с любой комбинацией

0 голосов
/ 03 октября 2009

1) Я тоже не знаю, почему так делается.

2) Тип символа особенный в двух отношениях.

Ни один другой тип не имеет более слабых ограничений выравнивания, чем тип char. Это важно для переинтерпретации приведения между указателями и между выражением и ссылкой.

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

3) Я думаю, что модификатор volatile используется, чтобы гарантировать, что никакая оптимизация компилятора не приведет к попытке чтения памяти.

0 голосов
/ 03 октября 2009

char гарантируется наименьшим количеством битов, которые архитектурная банка может «кусать» (он же байт).

Все указатели на самом деле являются числами, поэтому приведите адрес 0 к этому типу, потому что это начало.

Взять адрес члена, начиная с 0 (в результате 0 + location_of_m).

Приведите это обратно к size_t.

...