Независимо от того, насколько «плохой» код и если предположить, что выравнивание и т. Д. Не является проблемой для компилятора / платформы, является ли это неопределенным или неправильным поведением?
Если у меня есть такая структура: -
struct data
{
int a, b, c;
};
struct data thing;
Является ли это законным для доступа a
, b
а c
также (&thing.a)[0]
, (&thing.a)[1]
и (&thing.a)[2]
?
В каждом случае, на всех компиляторах и платформах, которые я пробовал, с каждой настройкой, которую я пробовал, она «работала». Меня просто беспокоит, что компилятор может не понять, что b и thing [1] - это одно и то же, и что запись в 'b' может быть помещена в регистр, а вещь [1] считывает неправильное значение из памяти (например). Но в каждом случае я делал все правильно. (Я понимаю, что это мало что доказывает)
Это не мой код; это код, с которым я должен работать, меня интересует, плохой ли это код или сломанный код, так как разные вещи сильно влияют на мои приоритеты для его изменения :)
Помечены как C и C ++. Меня больше всего интересует C ++, но также C, если он другой, просто для интереса.
Ответы:
Это незаконно 1 . Это неопределенное поведение в C ++.
Вы берете элементы в виде массива, но вот что говорит стандарт C ++ (выделено мной):
Но для участников нет такого непрерывного требования:
Хотя приведенных выше двух кавычек должно быть достаточно, чтобы намекнуть, почему индексирование в a,
struct
как вы, не является определенным поведением стандартом C ++, давайте выберем один пример: посмотрите на выражение(&thing.a)[2]
- Относительно оператора нижнего индекса:Углубляемся в жирный текст приведенной выше цитаты: относительно добавления интегрального типа к типу указателя (обратите внимание на выделение здесь).
Обратите внимание на требование к массиву для предложения if ; иначе иначе в приведенной выше цитате. Выражение
(&thing.a)[2]
явно не подходит для предложения if ; Следовательно, неопределенное поведение.На заметку: хотя я много экспериментировал с кодом и его вариациями на различных компиляторах, и они не вводят здесь никаких отступов (это работает ); с точки зрения обслуживания код чрезвычайно хрупок. вам все равно следует утверждать, что реализация распределяла элементы непрерывно, прежде чем делать это. И оставайся в составе :-). Но это все еще неопределенное поведение ....
Некоторые жизнеспособные обходные пути (с определенным поведением) были предоставлены другими ответами.
Как правильно указано в комментариях, [basic.lval / 8] , который был в моей предыдущей редакции, не применяется. Спасибо @ 2501 и @MM
1 : См. Ответ
thing.a
@Barry на этот вопрос для единственного юридического случая, когда вы можете получить доступ к члену структуры через этот партнер.источник
- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
Нет. В C это неопределенное поведение, даже если нет заполнения.
То, что вызывает неопределенное поведение, - это доступ за пределы 1 . Когда у вас есть скаляр (элементы a, b, c в структуре) и вы пытаетесь использовать его в качестве массива 2 для доступа к следующему гипотетическому элементу, вы вызываете неопределенное поведение, даже если случайно есть другой объект того же типа в этот адрес.
Однако вы можете использовать адрес объекта структуры и вычислить смещение в конкретном члене:
Это должно быть сделано для каждого члена индивидуально, но может быть помещено в функцию, которая напоминает доступ к массиву.
1 (Цитата из: ISO / IEC 9899: 201x 6.5.6 Аддитивные операторы 8)
Если результат указывает на один элемент после последнего элемента объекта массива, он не должен использоваться в качестве операнда вычисляемого унарного оператора *.
2 (Цитата из: ISO / IEC 9899: 201x 6.5.6 Аддитивные операторы 7)
Для целей этих операторов указатель на объект, который не является элементом массива, ведет себя так же, как указатель на первый элемент массива. массив длины один с типом объекта в качестве типа его элемента.
источник
char* p = ( char* )&thing.a + offsetof( thing , b );
ведет к неопределенному поведению?В C ++, если это действительно нужно - создайте оператор []:
не только гарантированно работает, но и упрощает использование, вам не нужно писать нечитаемое выражение
(&thing.a)[0]
Примечание: этот ответ дается в предположении, что у вас уже есть структура с полями, и вам нужно добавить доступ через индекс. Если скорость является проблемой и вы можете изменить структуру, это может быть более эффективным:
Это решение изменит размер структуры, поэтому вы также можете использовать методы:
источник
thing.a()
.Для c ++: если вам нужно получить доступ к члену, не зная его имени, вы можете использовать указатель на переменную-член.
источник
offsetoff
в C.В ISO C99 / C11 использование типов на основе объединения является законным, поэтому вы можете использовать его вместо указателей индексации на массивы (см. Другие ответы).
ISO C ++ не допускает использование текста на основе объединения. GNU C ++ делает это как расширение , и я думаю, что некоторые другие компиляторы, которые не поддерживают расширения GNU в целом, поддерживают объединение типов. Но это не поможет вам писать строго переносимый код.
В текущих версиях gcc и clang написание функции-члена C ++ с использованием a
switch(idx)
для выбора члена приведет к оптимизации для постоянных индексов времени компиляции, но создаст ужасный ветвящийся asm для индексов времени выполнения. В этом нет ничего плохогоswitch()
; это просто ошибка упущенной оптимизации в текущих компиляторах. Они могли эффективно скомпилировать функцию Slava switch ().Решение / обходной путь - сделать это другим способом: дать вашему классу / структуре член массива и написать функции доступа для прикрепления имен к определенным элементам.
Мы можем взглянуть на вывод asm для различных случаев использования в проводнике компилятора Godbolt . Это полные функции x86-64 System V с опущенной в конце инструкции RET, чтобы лучше показать, что вы получите, если они встроены. ARM / MIPS / что бы там ни было.
Для сравнения, ответ @Slava с использованием a
switch()
для C ++ делает asm подобным этому для индекса переменной времени выполнения. (Код в предыдущей ссылке Godbolt).Это явно ужасно по сравнению с версией каламбура типа на основе объединения C (или GNU C ++):
источник
[]
оператора непосредственно на члене объединения, стандарт определяетarray[index]
как эквивалентный*((array)+(index))
, и ни gcc, ни clang не будут надежно распознавать, что доступ к*((someUnion.array)+(index))
является доступом кsomeUnion
. Единственное объяснение, которое я вижу, это то, чтоsomeUnion.array[index]
ни*((someUnion.array)+(index))
не определены стандартом, а являются просто популярными расширениями, и gcc / clang решили не поддерживать второе, но, похоже, поддерживают первое, по крайней мере, на данный момент.В C ++ это в основном неопределенное поведение (зависит от того, какой индекс).
Из [expr.unary.op]:
Таким
&thing.a
образом, считается, что выражение относится к массиву из единицint
.От [expr.sub]:
И из [expr.add]:
(&thing.a)[0]
идеально сформирован, потому что&thing.a
считается массивом размером 1, и мы берем этот первый индекс. Это разрешенный индекс.(&thing.a)[2]
нарушает предпосылку , что0 <= i + j <= n
, так как у нас естьi == 0
,j == 2
,n == 1
. Простое построение указателя&thing.a + 2
- это неопределенное поведение.(&thing.a)[1]
это интересный случай. На самом деле это ничего не нарушает в [expr.add]. Нам разрешено брать указатель на один за концом массива - что и было бы. Здесь мы обратимся к примечанию в [basic.compound]:Следовательно, получение указателя
&thing.a + 1
- это определенное поведение, но разыменование его не определено, потому что он ни на что не указывает.источник
(&thing.a + 1)
интересный случай, который мне не удалось раскрыть. +1! ... Просто любопытно, вы из комитета ISO C ++?Это неопределенное поведение.
В C ++ существует множество правил, которые пытаются дать компилятору некоторую надежду понять, что вы делаете, чтобы он мог рассуждать об этом и оптимизировать это.
Существуют правила использования псевдонимов (доступа к данным с помощью двух разных типов указателей), границ массива и т. Д.
Когда у вас есть переменная
x
, тот факт, что она не является членом массива, означает, что компилятор может предположить, что никакой[]
доступ к основанному массиву не может ее изменить. Таким образом, ему не нужно постоянно перезагружать данные из памяти каждый раз, когда вы их используете; только если кто-то мог изменить его по имени .Таким образом,
(&thing.a)[1]
можно предположить, что компилятор не ссылается наthing.b
. Он может использовать этот факт для изменения порядка чтения и записиthing.b
, делая недействительным то, что вы хотите, чтобы он делал, не отменяя того, что вы на самом деле сказали ему делать.Классический пример этого - отказ от const.
здесь обычно компилятор говорит 7, затем 2! = 7, а затем два идентичных указателя; несмотря на то, что
ptr
указывает наx
. Компилятор принимает тот факт, чтоx
это постоянное значение, чтобы не читать его, когда вы запрашиваете значениеx
.Но когда вы берете адрес
x
, вы заставляете его существовать. Затем вы отбрасываете const и изменяете его. Таким образом, фактическое место в памяти, гдеx
оно было изменено, компилятор может не читать его при чтенииx
!Компилятор может стать достаточно умным, чтобы понять, как даже избежать
ptr
чтения за чтением*ptr
, но часто это не так. Не стесняйтесь пойти и использоватьptr = ptr+argc-1
или немного запутаться, если оптимизатор становится умнее вас.Вы можете предоставить обычай,
operator[]
который получит нужный элемент.иметь и то и другое полезно.
источник
(&thing.a)[0]
может изменить этоx
потому что знает, что вы не можете изменить его определенным образом. Аналогичная оптимизация может произойти, когда вы изменяетеb
через,(&blah.a)[1]
если компилятор может доказать, что не было определенного доступа,b
который мог бы его изменить; такое изменение могло произойти из-за, казалось бы, безобидных изменений в компиляторе, окружающем коде и т. д. Так что даже проверки того, что он работает, недостаточно.Вот способ использовать прокси-класс для доступа к элементам в массиве элементов по имени. Он очень похож на C ++ и не имеет преимуществ перед функциями доступа, возвращающими ref, за исключением синтаксических предпочтений. Это перегружает
->
оператора для доступа к элементам как членам, поэтому, чтобы быть приемлемым, нужно не только не любить синтаксис accessors (d.a() = 5;
), но и допускать использование->
с объектом, не являющимся указателем. Я полагаю, что это также может сбить с толку читателей, не знакомых с кодом, так что это может быть скорее хитрый трюк, чем то, что вы хотите внедрить в производство.Структура
Data
в этом коде также включает перегрузки для оператора нижнего индекса для доступа к индексированным элементам внутри егоar
члена массива, а также функцииbegin
иend
для итерации. Кроме того, все они перегружены неконстантными и константными версиями, которые, как я чувствовал, необходимо включить для полноты.Когда
Data
«S->
используется для доступа к элементу по имени (например:my_data->b = 5;
), АProxy
объект возвращается. Затем, поскольку этоProxy
rvalue не является указателем,->
автоматически вызывается его собственный оператор, который возвращает указатель на себя. Таким образомProxy
создается экземпляр объекта, который остается действительным во время оценки исходного выражения.Создание
Proxy
объекта заполняет его 3 ссылочных членаa
,b
и вc
соответствии с указателем, переданным в конструктор, предполагается, что он указывает на буфер, содержащий по крайней мере 3 значения, тип которых указан как параметр шаблонаT
. Таким образом, вместо использования именованных ссылок, которые являются членамиData
класса, это экономит память, заполняя ссылки в точке доступа (но, к сожалению, используя,->
а не.
оператор).Чтобы проверить, насколько хорошо оптимизатор компилятора устраняет все косвенные обращения, возникающие при использовании
Proxy
, приведенный ниже код включает 2 версииmain()
.#if 1
Версия использует->
и[]
оператор, а также#if 0
версия выполняет эквивалентный набор процедур, но только путем непосредственного доступаData::ar
.Nci()
Функция генерирует во время выполнения целочисленных значений для инициализации элементов массива, который предотвращает оптимизатор от только подключить постоянные значения непосредственно в каждыйstd::cout
<<
вызов.Для gcc 6.2 при использовании -O3 обе версии
main()
генерируют одну и ту же сборку (переключение между#if 1
и#if 0
перед первойmain()
для сравнения): https://godbolt.org/g/QqRWZbисточник
main()
с функциями синхронизации! например,int getb(Data *d) { return (*d)->b; }
компилируется в простоmov eax, DWORD PTR [rdi+4]
/ret
( godbolt.org/g/89d3Np ). (Да,Data &d
это упростило бы синтаксис, но я использовал указатель вместо ref, чтобы подчеркнуть странность->
такой перегрузки .)int tmp[] = { a, b, c}; return tmp[idx];
не оптимизировать, так что это здорово, что это делает.operator.
C ++ 17.Если чтения значений достаточно, и эффективность не вызывает беспокойства, или если вы доверяете своему компилятору хорошо оптимизировать вещи, или если структура составляет всего 3 байта, вы можете безопасно сделать это:
Для версии, предназначенной только для C ++, вы, вероятно, захотите использовать ее
static_assert
для проверкиstruct data
стандартного макета и, возможно, вместо этого выбросить исключение для недопустимого индекса.источник
Это незаконно, но есть обходной путь:
Теперь вы можете проиндексировать v:
источник