Подтвердить что ты не робот

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

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

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

Будет ли законным писать s.a[6] и ожидать, что он будет равен s.b[2]? Лично я чувствую, что это должен быть UB на С++, тогда как я не уверен в 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.

4b9b3361

Ответ 1

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

Нет. Потому что доступ к массиву из связанного вызова undefined поведения в C и С++.

C11 J.2 Undefined поведение

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

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

Стандарт С++ черновик, раздел 5.7. Операторы добавок в параграфе 5 говорят:

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

Ответ 2

Помимо ответа @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 также не накладывает второе дополнение, которое не должно быть там.

Ответ 3

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

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

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

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

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

Ответ 4

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

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

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

Учитывая

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

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

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

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

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

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

Ответ 5

Как упоминалось в комментарии @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]).

(Edit: вы могли бы заменить 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;
}

Ответ 6

Нет, поскольку доступ к массиву за пределами вызывает Undefined Поведение, как на C, так и на С++.

Ответ 7

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

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

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

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

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

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

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

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

Добавление:

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

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

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

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

Ответ 8

Ответ Джеда Шаффса на правильном пути, но не совсем правильный. Если компилятор вставляет отступы между 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. Я думал, что это звучит неправильно.)

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

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

Ответ 9

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

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

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

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

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

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

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

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