Переносимость использования offsetof из stddef.h вместо использования собственного

Это вопрос с мелкими деталями, состоящий из трех частей. Контекст состоит в том, что я хочу убедить некоторых людей в том, что безоговорочно использовать определение offsetof, данное <stddef.h>, а не (при некоторых обстоятельствах) использовать собственное. Данная программа полностью написана на простом старом 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 отвечайте только в том случае, если вы можете назвать конкретную реализацию и указать дату ее выпуска. Ответы, в которых указываются общие характеристики подходящих реализаций, мне не нужны.


person zwol    schedule 14.07.2011    source источник
comment
Когда поставщик компилятора предоставил вам файл заголовка, это не является неопределенным поведением. Они знают, как определенное неопределенное поведение выглядит на их продукте.   -  person Hans Passant    schedule 15.07.2011
comment
Нет такой вещи, как любая другая причина, кроме ... когда мы говорим о неопределенном поведении. Вы либо заботитесь о спецификации, либо нет. Даже если каждый компилятор C с незапамятных времен отлично работает с какой-то неопределенной конструкцией, завтрашние компиляторы с более сильной оптимизацией могут не работать. (Поскольку компилятор может предположить, что вы не вызываете неопределенное поведение и делаете выводы оттуда. Более умные компиляторы = больше выводов)   -  person Nemo    schedule 15.07.2011
comment
Я знаю, как это работает, и я все еще настаиваю на любой другой причине, кроме ... части, потому что это правило почти повсеместно заменяется реализациями, определяющими, что все указатели, независимо от того, на каком языке они указывают на, и включая нулевой указатель, сопоставимы в глобальном плоском адресном пространстве.   -  person zwol    schedule 15.07.2011
comment
Вы на 100% уверены, что ни одна система в будущем больше не будет использовать архитектуру сегментированной памяти? И вы на 100% уверены, что ни один составитель компилятора не найдет способ использовать указатели предположений, которые можно сравнить только внутри объекта с чем-либо вообще? Ваш хрустальный шар должен быть превосходным ... Как бы то ни было, как указывает Р., ваш макрос также вызывает неопределенное поведение, применяя -> к нулевому указателю. (Кроме того, вы обычно кажетесь более озабоченными компиляторами в прошлом, чем в будущем, когда последних будет бесконечно больше.)   -  person Nemo    schedule 15.07.2011
comment
@Nemo: Да, для людей, с которыми я разговариваю, обеспечение возможности сборки программы во всех средах, в которых она собиралась в прошлом, намного важнее, чем предотвращение проблемы, которая может произойти в некоторой гипотетической среде в будущем.   -  person zwol    schedule 15.07.2011
comment
@Zack, я понимаю, что вам нужно убеждать людей с извращенными приоритетами, но, пожалуйста, поймите, что у них наоборот. Любая гипотетическая прошлая система с такими серьезными проблемами соответствия имеет равное количество битов в области безопасности и, следовательно, непригодна для использования в любом реальном развертывании. Будет ли этим людям волноваться, если вы откажетесь от поддержки запуска сервера базы данных на Win95? Вероятно, не потому, что они поймут, что это нежизнеспособная цель из-за фатальных неисправленных недостатков безопасности.   -  person R.. GitHub STOP HELPING ICE    schedule 15.07.2011
comment
@Nemo: Я совершенно уверен, что никто больше никогда не будет использовать сегментированную память, но реальная проблема заключается в том, что люди, использующие системы с высоким уровнем безопасности и желающие пожертвовать 90% или даже 99% производительностью ради интенсивной безопасности, могут захотеть использовать реализации C с радикально разные модели указателей, в которых невозможно, выполняя арифметические действия с указателем на один объект, случайно получить указатель на другой объект. Такие реализации не допускают хакерского определения макроса для offsetof.   -  person R.. GitHub STOP HELPING ICE    schedule 15.07.2011
comment
@R: Да, я сам действительно думал о безопасных реализациях; не обязательно для безопасности, а просто для проверки ошибок. @Zack: Назовите это гипотетическим, но, по моему опыту, 90% работы над любым фрагментом кода происходит после его первой поставки. Стандарты помогают смягчить это. Ваш опыт может отличаться.   -  person Nemo    schedule 15.07.2011


Ответы (4)


Чтобы ответить на № 2: да, gcc-4 * (сейчас я смотрю на версию 4.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;
}

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

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

Что касается других ваших вопросов: я думаю, что ответ R и его последующие комментарии хорошо справляются с изложением соответствующих разделов стандарта в том, что касается вопроса № 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 вообще не упоминается в основополагающей книге Кернигана и Ричи "Язык программирования C" (даже в самой последнее издание). Первое издание книги было неофициальным стандартом до того, как был стандартизован Си, и я часто слышу, как люди ошибочно называют эту книгу ЭТОМ стандартом для языка. Его гораздо легче читать, чем официальный стандарт, поэтому я не знаю, виню ли я их за то, что они сделали его своей первой точкой отсчета. Однако независимо от того, во что они верят, стандарт ясно, что 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-й маркер). Использование версии offsetof для stddef.h всегда будет приводить к поведению, определенному в стандарте (при условии, что компилятор соответствует стандартам). Я бы не советовал определять собственную версию, поскольку это может вызвать проблемы с переносимостью, но если других не удается убедить, то приведенный выше фрагмент #ifndef offsetof может быть приемлемым компромиссом.

person bta    schedule 15.07.2011
comment
Я был разработчиком GCC в то время, когда это изменение было внесено в их stddef.h; Я лично не имел к этому никакого отношения, но я был свидетелем дискуссии, и она почти полностью была связана с C ++ (где перегруженные операторы и тому подобное могут сделать идиоматическое расширение полностью неправильным). Все согласились с тем, что это идиома UB, но никто не удосужился выяснить, почему именно, и для совместимости интерфейс C распознает идиому и заменяет ее на внутреннюю! Это делает gcc4 не тем примером, который мне нужен для №2, даже несмотря на то, что он избегает идиомы в своем собственном stddef.h. - person zwol; 15.07.2011
comment
... Эти люди счастливы доверять компилятору offsetof правильно реализовать ... если он вообще есть, что они не верят, что всегда так. У меня был этот разговор раньше; существует широко распространенное, AFAICT полностью ложное мнение, что offsetof был добавлен только в stddef.h в C99, поэтому вы не можете рассчитывать на его присутствие, и его проще просто определить самостоятельно. Смысл части №3 состоит в том, чтобы точно определить, откуда может исходить это ошибочное мнение. - person zwol; 15.07.2011
comment
Ах, теперь я понимаю, что вы имеете в виду. Они сомневаются в существовании offsetof, а не в его правильности. На самом деле я разговаривал с рядом людей, которые думают так же. Мой взгляд на это заблуждение см. В обновлении моего ответа. - person bta; 16.07.2011
comment
Спасибо за доработку. Я принимаю этот ответ, поскольку мне кажется, что я не получу исторической точки зрения, которую искал, и вы предоставили способ выйти из тупика в рамках проекта. - person zwol; 16.07.2011

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

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

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

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

person R.. GitHub STOP HELPING ICE    schedule 14.07.2011
comment
Да, та же точка массива вызывает UD; мой первоначальный ответ был неправильным. Пункт 6.5.6 п.9 проекта С1Х. +1. - person Fred Foo; 15.07.2011
comment
Мне нужны конкретные имена и даты выпуска производственных реализаций C для частей 2 и 3, а также конкретные ссылки на стандартный текст C с обсуждением разницы между C89 и C99, если таковые имеются Для части 1. Этот ответ не убедит людей, которых я пытаюсь убедить. - person zwol; 15.07.2011
comment
Кстати, я подозреваю, что многие реализации могут определять все объекты как живущие внутри одного массива типа char[SIZE_MAX+1], также известного как виртуальное адресное пространство. :-) - person R.. GitHub STOP HELPING ICE; 15.07.2011
comment
Именно по этой причине, поскольку я только что отредактировал вопрос, укажите причины неопределенного поведения, ДРУГОЕ, чем правило одного и того же массива. - person zwol; 15.07.2011
comment
Часть 3: C89 4.1.5 определяет offsetof; см. noderose.net/e/C89/ansi.c.txt. не более ранний стандарт C. - person R.. GitHub STOP HELPING ICE; 15.07.2011
comment
Для части 2 и UB см. C99 6.5.2.3 параграф 4. Левый операнд -> должен указывать на объект типа структуры или объединения; однако (tp*)0 не указывает ни на какой объект. - person R.. GitHub STOP HELPING ICE; 15.07.2011
comment
А по поводу того, что выражение не является постоянным, см. C99 6.6, особенно параграф 6. C99 + TC3 можно найти в форме HTML по адресу port70.net/~nsz/c/c99/n1256.html - person R.. GitHub STOP HELPING ICE; 15.07.2011
comment
@R: Отличный ответ. Я предлагаю обновить его ссылкой на -> при неопределенном поведении NULL. - person Nemo; 15.07.2011
comment
Вы не даете полезного ответа на части 2 или 3, говоря о стандарте. Я спросил конкретно о ВНЕДРЕНИЯХ в этих частях. - person zwol; 15.07.2011
comment
Доказать отсутствие несовместимых псевдо-реализаций очень сложно, и я не знаю, как бы вы хотели документировать это несуществование. Следующая лучшая вещь, ИМО, - это доказать, что любая такая реализация кандидата будет несоответствующей и, следовательно, на самом деле не C, а скорее подражатель C. - person R.. GitHub STOP HELPING ICE; 15.07.2011
comment
Я пытаюсь обратиться к конкретному, широко распространенному, возможно, совершенно неверному мнению, что offsetof не присутствует в stddef.h заголовках, предоставляемых компиляторами, совместимыми с C89. Вы продолжаете упускать из виду суть вопроса настолько тщательно, что мне жаль, что вы вообще не ответили. - person zwol; 15.07.2011
comment
Я понимаю суть дела, но не понимаю, как я мог бы доказать это, не получив исторические stddef.h каждого поставщика и не показав их вам всех ... - person R.. GitHub STOP HELPING ICE; 15.07.2011

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

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

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

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

person Jens Gustedt    schedule 14.07.2011

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

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

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

person Stephen Canon    schedule 14.07.2011