Переносимость использования offsetd из stddef.h вместо того, чтобы использовать свой собственный - PullRequest
11 голосов
/ 15 июля 2011

Это подробный вопрос с тремя частями.Суть в том, что я хочу убедить некоторых людей в том, что безопасно использовать <stddef.h> определение offsetof безоговорочно, а не (при некоторых обстоятельствах) менять свое собственное.Рассматриваемая программа написана полностью на старом простом C, поэтому, пожалуйста, полностью игнорируйте C ++ при ответе.

Часть 1: При использовании таким же образом, как стандарт offsetof, выполняетрасширение этого макроса вызывает неопределенное поведение для C89, почему или почему нет, и отличается ли он в C99?

#define offset_of(tp, member) (((char*) &((tp*)0)->member) - (char*)0)

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

Часть 2. Насколько вам известно, была ли выпущена рабочая версия C?Реализация, которая при использовании расширения вышеупомянутого макроса будет (при некоторых обстоятельствах) вести себя иначе, чем если бы вместо этого использовался его макрос offsetof?

Часть 3: Насколько вам известноКакова последняя выпущенная реализация C, которая либо не предоставила stddef.h, либо не дала рабочее определение offsetof в этом заголовке?Утверждает ли эта реализация соответствие какой-либо версии стандарта C?

Для частей 2 и 3 ответьте только в том случае, если вы можете назвать конкретную реализацию и указать дату ее выпуска.Ответы, в которых изложены общие характеристики реализаций, которые могут соответствовать требованиям, мне не нужны.

Ответы [ 4 ]

10 голосов
/ 15 июля 2011

Нет способа написать переносимый макрос offsetof. Вы должны использовать тот, который предоставлен stddef.h.

Относительно ваших конкретных вопросов:

  1. Макрос вызывает неопределенное поведение. Вы не можете вычитать указатели, кроме случаев, когда они указывают на один и тот же массив.
  2. Большая разница в практическом поведении заключается в том, что макрос не является целочисленным константным выражением , поэтому его нельзя безопасно использовать для статических инициализаторов, ширины битовых полей и т. Д. Также строгий тип проверки границ Реализации C могут полностью сломать его.
  3. Никогда не было ни одного стандарта С, в котором бы не было stddef.h и offsetof. Пре-ANSI-компиляторы могут этого не иметь, но они имеют гораздо более фундаментальные проблемы, которые делают их непригодными для современного кода (например, отсутствие void * и const).

Более того, даже если в каком-то теоретическом компиляторе не было stddef.h, вы могли бы просто обеспечить замену вставки, точно так же, как люди вводят stdint.h для использования с MSVC ...

6 голосов
/ 15 июля 2011

Чтобы ответить на вопрос №2: да, gcc-4 * (в настоящее время я смотрю v4.3.4, выпущенную 4 августа 2009 года, но она должна сохраняться для всех выпусков gcc-4 на сегодняшний день). В их stddef.h используется следующее определение:

#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)

, где __builtin_offsetof - это встроенный компилятор, такой как sizeof (то есть , а не , реализованный в виде макроса или функции времени выполнения). Компиляция кода:

#include <stddef.h>

struct testcase {
    char array[256];
};

int main (void) {
    char buffer[offsetof(struct testcase, array[0])];
    return 0;
}

приведет к ошибке при использовании предоставленного вами расширения макроса («размер массива« буфер »не является целочисленным выражением константы»), но будет работать при использовании макроса, предоставленного в stddef.h. Сборки с использованием gcc-3 использовали макрос, похожий на ваш. Я полагаю, что у разработчиков gcc было много таких же проблем, связанных с неопределенным поведением и т. Д., Которые были здесь выражены, и они создали встроенную компилятор как более безопасную альтернативу попыткам сгенерировать эквивалентную операцию в коде C.

Дополнительная информация:

Относительно ваших других вопросов: я думаю, что ответ Р. и его последующие комментарии хорошо описывают соответствующие разделы стандарта в отношении вопроса № 1. Что касается вашего третьего вопроса, я не слышал о современном C компиляторе, который не имеет stddef.h. Я, конечно, не считаю, что какой-либо компилятор не имеет такого основного стандартного заголовка, как «production». Точно так же, если их реализация offsetof не сработала, то компилятору еще есть над чем поработать, прежде чем его можно будет считать «производственным», как если бы другие вещи в stddef.h (например, NULL) не работали. Компилятор C, выпущенный до стандартизации C, может не иметь этих вещей, но стандарту ANSI C уже более 20 лет, поэтому крайне маловероятно, что вы столкнетесь с одним из них.

Вся предпосылка к этим проблемам напрашивается: если эти люди убеждены, что они не могут доверять версии offsetof, которую предоставляет компилятор, то чему может доверять? Они верят, что NULL определен правильно? Они верят, что long int не меньше обычного int? Они верят, что memcpy работает так, как должно? Они катят свои собственные версии остальной функциональности стандартной библиотеки C? Одна из главных причин наличия языковых стандартов заключается в том, что вы можете доверять компилятору, чтобы он делал все правильно. Кажется глупым доверять компилятору для всего остального, кроме offsetof.

Обновление: (в ответ на ваши комментарии)

Я думаю, что мои коллеги ведут себя так же, как и вы :-) Некоторые из наших старых кодов все еще имеют собственные макросы, определяющие NULL, VOID и другие подобные вещи, поскольку «разные компиляторы могут реализовывать их по-разному» (вздох) ). Часть этого кода была написана еще до того, как C был стандартизирован, и многие старые разработчики все еще придерживаются этого мнения, хотя стандарт C ясно говорит об обратном.

Вот одна вещь, которую вы можете сделать, чтобы доказать их неправоту и сделать всех счастливыми одновременно:

#include <stddef.h>

#ifndef offsetof
  #define offsetof(tp, member) (((char*) &((tp*)0)->member) - (char*)0)
#endif

На самом деле они будут использовать версию, предоставленную в stddef.h. Однако пользовательская версия всегда будет там, если вы столкнетесь с гипотетическим компилятором, который ее не определяет.

Основываясь на подобных беседах, которые у меня были на протяжении многих лет, я думаю, что убеждение в том, что offsetof не является частью стандарта C, исходит из двух мест. Во-первых, это редко используемая функция. Разработчики не видят это очень часто, поэтому они забывают, что оно даже существует. Во-вторых, offsetof вообще не упоминается в оригинальной книге Кернигана и Ричи "Язык программирования Си" (даже самое последнее издание). Первое издание книги было неофициальным стандартом до того, как был стандартизирован язык Си, и я часто слышу, как люди ошибочно называют эту книгу стандартом языка. Его гораздо легче читать, чем официальный стандарт, поэтому я не знаю, виню ли я их за то, что они сделали это своей первой точкой отсчета. Однако независимо от того, во что они верят, стандарт ясно, что offsetof является частью ANSI C (см. Ответ R для ссылки).


Вот еще один способ взглянуть на вопрос № 1. Стандарт ANSI C дает следующее определение в разделе 4.1.5:

     offsetof( type,  member-designator)

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

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

С другой стороны, расширение макросов, которое вы предоставили, действительно вызывает неопределенное поведение. Поскольку вы недостаточно знаете о компиляторе, чтобы предсказать, как он будет обрабатывать код, вы не можете гарантировать, что конкретная реализация offsetof всегда будет работать. Многие люди определяют свою собственную версию таким образом и не сталкиваются с проблемами, но это не означает, что код правильный. Даже если так происходит, когда конкретный компилятор определяет offsetof, написание этого кода самостоятельно вызывает UB, а использование предоставленного макроса offsetof - нет.

Прокрутка собственного макроса для offsetof не может быть выполнена без вызова неопределенного поведения (ANSI C раздел A.6.2 «Неопределенное поведение», 27-й пункт маркировки). Использование stddef.h версии offsetof всегда приводит к поведению, определенному в стандарте (при условии, что компилятор соответствует стандартам). Я бы посоветовал не определять пользовательскую версию, поскольку она может вызвать проблемы с переносимостью, но если другие не удастся убедить, приведенный выше фрагмент #ifndef offsetof может быть приемлемым компромиссом.

2 голосов
/ 15 июля 2011

(1) Неопределенное поведение уже существует до того, как вы выполните вычитание.

  1. Прежде всего, (tp*)0 - это не то, что вы думаете. Это ноль указатель , такой зверь не обязательно представлен со всеми нулями битовый паттерн.
  2. Тогда оператор-член -> - это не просто сложение со смещением. На процессоре с сегментированной памятью это может быть более сложной операцией.
  3. Взятие адреса с помощью операции & равно UB, если выражение недопустимый объект.

(2) Что касается пункта 2., в дикой природе, безусловно, все еще присутствуют архивные изображения (встроенные элементы), которые используют сегментированную память. Для 3. точка R о целочисленных константных выражениях имеет еще один недостаток: если код плохо оптимизирован, операция & может выполняться во время выполнения и сигнализировать об ошибке.

(3) Никогда не слышал о такой вещи, но этого, вероятно, недостаточно, чтобы посоветоваться с коллегами.

1 голос
/ 15 июля 2011

Я считаю, что почти каждый оптимизирующий компилятор нарушал этот макрос в разные моменты времени.Вашим коллегам, очевидно, повезло, что они не пострадали.

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

...