it-swarm.com.ru

В структуре законно ли использовать одно поле массива для доступа к другому?

В качестве примера рассмотрим следующую структуру:

struct S {
  int a[4];
  int b[4];
} s;

Было бы законно написать s.a[6] и ожидать, что он будет равен s.b[2]? Лично я чувствую, что это должно быть UB в C++, тогда как я не уверен насчет C . Однако я не смог ничего найти актуально в стандартах языков C и C++.


Обновление

Есть несколько ответов, предлагающих способы убедиться, что между полями нет заполнения , Чтобы код работал надежно. Я хотел бы подчеркнуть , Что если такой код UB, то отсутствие дополнения недостаточно. Если это UB, , То компилятор может предположить, что обращения к S.a[i] и S.b[j] не перекрываются И компилятор может свободно изменять порядок таких обращений к памяти. Например,

    int x = s.b[2];
    s.a[6] = 2;
    return x;

может быть преобразован в

    s.a[6] = 2;
    int x = s.b[2];
    return x;

который всегда возвращает 2.

51
Nikolai

Было бы законно написать s.a [6] и ожидать, что оно будет равно s.b [2]?

Нет. Потому что доступ к массиву вне границ вызвал неопределенное поведение в C и C++.

C11 J.2 Неопределенное поведение

  • Сложение или вычитание указателя на объект массива или сразу за ним, а также целочисленный тип приводит к результату, который указывает сразу за объект массива и используется в качестве операнда унарного оператора *, который оценивается (6.5.6).

  • Индекс массива выходит за пределы допустимого диапазона, даже если объект, очевидно, доступен с данным индексом (как в выражении lvalue a[1][7] с учетом объявления int a[4][5]) (6.5.6).

Стандарт C++ черновик раздел 5.7 Аддитивные операторы, параграф 5 гласит:

Когда выражение с целочисленным типом добавляется или вычитается из указателя результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива и массив достаточно велик, результат указывает на смещение элемента от исходный элемент такой, что разница индексов результирующий и исходный элементы массива равны интегральному выражению . [...] Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или одного последнего элемента массива объект, оценка не должна вызывать переполнение; в противном случае поведение не определено.

61
M.S Chaudhari

Помимо ответа на @rsp (Undefined behavior for an array subscript that is out of range) я могу добавить, что доступ к b через a недопустим, поскольку язык C не определяет, сколько может быть отступов между концом области, выделенной для a, и началом b, поэтому даже если вы можете запустить его в конкретной реализации, он не переносим.

instance of struct:
+-----------+----------------+-----------+---------------+
|  array a  |  maybe padding |  array b  | maybe padding |
+-----------+----------------+-----------+---------------+

Второе заполнение может пропустить, так как выравнивание struct object является выравниванием a, которое совпадает с выравниванием b, но язык C также не навязывает второго дополнения, чтобы не быть там.

34
alinsoar

a и b - это два разных массива, а a определяется как содержащий элементы 4. Следовательно, a[6] обращается к массиву вне границ и поэтому является неопределенным поведением. Обратите внимание, что подстрочный индекс массива a[6] определен как *(a+6), поэтому доказательство UB фактически дается разделом «Аддитивные операторы» в сочетании с указателями ». См. Следующий раздел стандарта C11 (например, this online черновая версия) описывая этот аспект:

6.5.6 Аддитивные операторы

Когда выражение с целочисленным типом добавляется или вычитается из указателя результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива и массив достаточно велик, результат указывает на смещение элемента от исходный элемент такой, что разница индексов результирующие и исходные элементы массива равны целочисленному выражению . Другими словами, если выражение P указывает на i-й элемент объект массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) указывают соответственно на i + n-й и i -ные элементы массива, если они существуют. Более того, если выражение P указывает на последний элемент объекта массива, Выражение (P) +1 указывает на один последний элемент массива, и если выражение Q указывает один за последним элементом массива объект, выражение (Q) -1 указывает на последний элемент массива объект. Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или одного последнего элемента массива объект, оценка не должна вызывать переполнение; в противном случае поведение не определено. Если результат указывает на один последний элемент объекта массива, он не должен использоваться как операнд унарного * оператор, который оценивается.

Тот же аргумент применим к C++ (хотя здесь и не указаны).

Кроме того, хотя это явно неопределенное поведение из-за факта превышения границ массива a, обратите внимание, что компилятор может ввести заполнение между членами a и b, так что - даже если бы разрешалась такая арифметика указателей - a+6 не обязательно приводил бы к тому же самому адрес как b+2.

11
Stephan Lechner

Это законно? Нет. Как уже упоминалось, он вызывает Неопределенное поведение .

Это будет работать? Это зависит от вашего компилятора. Это то, что касается неопределенного поведения: это undefined

На многих компиляторах C и C++ структура будет размещена так, что b будет сразу следовать за a в памяти и не будет проверять границы. Таким образом, доступ к [6] будет фактически таким же, как b [2], и не вызовет каких-либо исключений. 

Дано

struct S {
  int a[4];
  int b[4];
} s

и при условии отсутствия дополнительного заполнения , структура на самом деле является просто способом просмотра блока памяти, содержащего 8 целых чисел. Вы можете привести его к (int*), а ((int*)s)[6] будет указывать на ту же память, что и s.b[2]

Стоит ли полагаться на такое поведение? Точно нет. Не определено означает, что компилятор не должен поддерживать это. Компилятор может дополнить структуру, что может сделать предположение, что & (s.b [2]) == & (s.a [6]) неверно. Компилятор также может добавить проверку границ при доступе к массиву (хотя включение оптимизации компилятора, вероятно, отключит такую ​​проверку).

Я испытал последствия этого в прошлом. Довольно часто иметь такую ​​структуру

struct Bob {
    char name[16];
    char whatever[64];
} bob;
strcpy(bob.name, "some name longer than 16 characters");

Теперь боб. Что бы ни было "чем 16 символов". (именно поэтому вы всегда должны использовать strncpy, кстати)

6
dwilliss

Как упомянуто в комментарии @MartinJames, если вам нужно гарантировать, что a и b находятся в смежной памяти (или, по крайней мере, могут обрабатываться как таковые, (редактировать), если ваша архитектура/компилятор не использует необычный размер/смещение блока памяти и принудительно выравнивание, которое потребует добавления отступов), вам нужно использовать union.

union overlap {
    char all[8]; /* all the bytes in sequence */
    struct { /* (anonymous struct so its members can be accessed directly) */
        char a[4]; /* padding may be added after this if the alignment is not a sub-factor of 4 */
        char b[4];
    };
};

Вы не можете напрямую получить доступ к b из a (например, a[6], как вы и просили), но вы можете получить доступ к элементам как a, так и b с помощью all (например, all[6] ссылается на ту же ячейку памяти, что и b[2]).

(Правка: Вы можете заменить 8 и 4 в приведенном выше коде на 2*sizeof(int) и sizeof(int) соответственно, чтобы более вероятно соответствовать выравниванию архитектуры, особенно если код должен быть более переносимым, но тогда вы должны быть осторожны, чтобы избежать любые предположения о том, сколько байтов содержится в a, b или all. Однако это будет работать при том, что, вероятно, является наиболее распространенным (1-, 2- и 4-байтовым) выравниванием памяти.)

Вот простой пример:

#include <stdio.h>

union overlap {
    char all[2*sizeof(int)]; /* all the bytes in sequence */
    struct { /* anonymous struct so its members can be accessed directly */
        char a[sizeof(int)]; /* low Word */
        char b[sizeof(int)]; /* high Word */
    };
};

int main()
{
    union overlap testing;
    testing.a[0] = 'a';
    testing.a[1] = 'b';
    testing.a[2] = 'c';
    testing.a[3] = '\0'; /* null terminator */
    testing.b[0] = 'e';
    testing.b[1] = 'f';
    testing.b[2] = 'g';
    testing.b[3] = '\0'; /* null terminator */
    printf("a=%s\n",testing.a); /* output: a=abc */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abc */

    testing.a[3] = 'd'; /* makes printf keep reading past the end of a */
    printf("a=%s\n",testing.a); /* output: a=abcdefg */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abcdefg */

    return 0;
}
5
Jed Schaaf

Нет , так как доступ к массиву вне границ вызывает неопределенное поведение, как в C, так и в C++.

3
gsamaras

Краткий ответ: Нет. Вы находитесь в стране неопределенного поведения.

Длинный ответ: Нет. Но это не значит, что вы не можете получить доступ к данным другими более простыми способами ... если вы используете GCC, вы можете сделать что-то вроде следующего (разработка ответа dwillis):

struct __attribute__((packed,aligned(4))) Bad_Access {
    int arr1[3];
    int arr2[3];
};

и тогда вы могли получить доступ через ( источник Godbolt + asm ):

int x = ((int*)ba_pointer)[4];

Но это приведение нарушает строгие псевдонимы, поэтому безопасно только с g++ -fno-strict-aliasing. Вы можете привести структурный указатель на указатель на первый член, но затем вы снова в лодке UB, потому что вы получаете доступ за пределами первого члена.

Или просто не делайте этого. Спасите будущего программиста (вероятно, себя) от душевной боли этого беспорядка.

Кроме того, пока мы на этом, почему бы не использовать std :: vector? Это не защищает от дурака, но на заднем плане у него есть охранники, чтобы предотвратить такое плохое поведение.

Приложение:

Если вы действительно беспокоитесь о производительности:

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

Если вы торжественно клянетесь компилятору, что вы не пытаетесь создать псевдоним, компилятор щедро вознаградит вас: Предоставляет ли ключевое слово restrict значительные преимущества в gcc/g ++

Вывод: не будь злым; ваше будущее я, и компилятор будет вам благодарен.

1
Alex Shirley

Ответ Джеда Шаффа на верном пути, но не совсем правильный. Если компилятор вставит отступ между a и b, его решение все равно не будет выполнено. Если, однако, вы заявляете:

typedef struct {
  int a[4];
  int b[4];
} s_t;

typedef union {
  char bytes[sizeof(s_t)];
  s_t s;
} u_t;

Теперь вы можете получить доступ к (int*)(bytes + offsetof(s_t, b)), чтобы получить адрес s.b, независимо от того, как компилятор выкладывает структуру. Макрос offsetof() объявлен в <stddef.h>.

Выражение sizeof(s_t) является константным выражением, допустимым в объявлении массива на C и C++. Это не даст массив переменной длины. (Извиняюсь за неправильное прочтение стандарта C раньше. Я думал, что это звучит неправильно.)

В реальном мире, однако, два последовательных массива int в структуре будут расположены так, как вы ожидаете. (Вы можете сможете создать очень надуманный контрпример, установив границу a на 3 или 5 вместо 4, а затем заставив компилятор выровнять и a, и b на 16-байтовой границе.) Вместо замысловатых Чтобы попытаться получить программу, которая не делает никаких предположений, кроме строгой формулировки стандарта, вам нужно какое-то защитное кодирование, такое как static assert(&both_arrays[4] == &s.b[0], "");. Они не добавляют никаких затрат времени выполнения и не будут работать, если ваш компилятор делает что-то, что может сломать вашу программу, при условии, что вы не запускаете UB в самом утверждении.

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

1
Davislor

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

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

  1. Хотя многие реализации планируют структуры предсказуемым образом, стандарт позволяет реализациям добавлять произвольные отступы перед любым элементом структуры, кроме первого. Код может использовать sizeof или offsetof, чтобы гарантировать, что элементы структуры будут размещены, как ожидается, но две другие проблемы останутся.

  2. Учитывая что-то вроде:

    if (structPtr->array1[x])
     structPtr->array2[y]++;
    return structPtr->array1[x];
    

    как правило, для компилятора было бы полезно предположить, что использование structPtr->array1[x] приведет к тому же значению, что и предыдущее использование в условии «если», даже если это изменит поведение кода, основанного на наложении псевдонимов между двумя массивами.

  3. Если array1[] имеет, например, 4 элемента, компилятору дано что-то вроде:

    if (x < 4) foo(x);
    structPtr->array1[x]=1;
    

можно сделать вывод, что поскольку не было бы определенных случаев, когда x не меньше 4, он мог бы безоговорочно вызвать foo(x).

К сожалению, хотя программы могут использовать sizeof или offsetof, чтобы гарантировать отсутствие каких-либо сюрпризов со структурным макетом, они не могут проверить, обещают ли компиляторы воздержаться от оптимизаций типов # 2 или # 3. Кроме того, Стандарт немного расплывчат в отношении того, что будет означать в случае, подобном следующему:

struct foo {char array1[4],array2[4]; };

int test(struct foo *p, int i, int x, int y, int z)
{
  if (p->array2[x])
  {
    ((char*)p)[x]++;
    ((char*)(p->array1))[y]++;
    p->array1[z]++;
  }
  return p->array2[x];
}

Стандарт довольно ясно, что поведение будет определено только если z находится в диапазоне 0..3, но так как тип p-> array в этом выражении - char * (из-за затухания), не ясно, приведение в доступе использование y будет иметь какой-либо эффект. С другой стороны, поскольку преобразование указателя на первый элемент структуры в char* должно привести к тому же результату, что и преобразование указателя структуры в char*, а преобразованный указатель структуры должен использоваться для доступа ко всем имеющимся в нем байтам. x должен быть определен для (как минимум) x = 0..7 [если смещение array2 больше 4, это повлияет на значение x, необходимое для попадания в члены array2, но некоторое значение x может сделать это с определенным поведение].

ИМХО, хорошим выходом из ситуации было бы определение оператора индекса для типов массивов таким образом, чтобы не происходило затухание указателя. В этом случае выражения p->array[x] и &(p->array1[x]) могут предложить компилятору предположить, что x равен 0..3, но p->array+x и *(p->array+x) потребуют, чтобы компилятор учитывал возможность других значений. Я не знаю, делают ли это какие-либо компиляторы, но Стандарт не требует этого.

0
supercat