Что происходит в макросе offsetof?

Среда выполнения 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)->m) ?

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

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


person Frederick The Fool    schedule 03.10.2009    source источник


Ответы (5)


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

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

Обновление:

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

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

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

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

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

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

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

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

person Daniel Earwicker    schedule 03.10.2009
comment
Я не понимаю. Когда вы делаете s-›m в любом месте вашего кода, создаваемая сборка добавляет соответствующее смещение к адресу s, и это смещение всегда в байтах. Таким образом, оператор '&(((s*)0)-›m)' всегда даст вам ответ в байтах. Зачем тогда актерский состав? (Поправьте меня, если я ошибаюсь в своем понимании) - person Frederick The Fool; 03.10.2009
comment
Перегруженный оператор-› не проблема. Чтобы получить перегруженное поведение, вы должны применить его к экземпляру, а не к указателю. - person Steve314; 03.10.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. И вам все еще нужно быть осторожным, если вы работаете с иерархиями классов, потому что, как только вы используете множественное или виртуальное наследование, вы не можете предполагать, что макет производного класса соответствует макету базового класса - вы должны убедиться, что смещение допустимо для точного типа времени выполнения, а не только для конкретной базы.

Если вы работаете над библиотекой структур данных, возможно, используя одиночное наследование для узлов, но приложения не могут видеть или использовать ваши узлы напрямую, 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 (или указателей на элементы) усложняет работу, и его следует использовать в качестве оптимизации только в том случае, если вы знаете, что вам это нужно. Например, если ваша структура данных находится в файле на диске, она вам определенно не нужна - несколько накладных расходов на виртуальные вызовы будут незначительными по сравнению с накладными расходами на доступ к диску, поэтому любые усилия по оптимизации должны быть направлены на минимизацию обращений к диску.

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

person Steve314    schedule 03.10.2009

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

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

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

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

person LiraNuna    schedule 03.10.2009
comment
Он не приводит к 0 (невозможно, потому что 0 не является типом). Он приводит значение 0 к типу s, чтобы получить условный объект этого типа по адресу памяти 0. С этого момента ваше описание имеет смысл. - person Daniel Earwicker; 03.10.2009
comment
Мой плохой, английский не мой основной язык, поэтому я часто ошибаюсь :). - person LiraNuna; 03.10.2009

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

2) Тип char особенный по двум причинам.

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

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

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

person Komat    schedule 03.10.2009

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

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

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

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

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

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

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

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

поэтому в результате используется ‹const volatile char&› для приведения работы из любой комбинации

person mug896    schedule 25.09.2012