2

Есть ли какая-то возможность проиндексировать элементы структуры в языке Си? Например, мне нужно сделать цикл, где на определенной итерации заполняется определенный элемент структуры. Насколько я понимаю, элементы структуры в памяти идут подряд, учитывая выравнивание, но как я не пытался это сделать указателями, например, ничего не получалось. Спасибо!

  • 4
    Если тип (а значит и размер) интересующих вса полей одинаковый и они расположены подряд, то можно сделать union с массивом. А вообще, пример (можно с попытками кода) того, что вам надо приведите – avp Apr 04 '21 at 22:34
  • 2
    Если вы хотите доступ по индексу, то используйте массив. Зачем вам структура? – Stanislav Volodarskiy Apr 04 '21 at 22:57
  • 1
    @StanislavVolodarskiy, ему нужно пройти по элементам структуры. – eanmos Apr 05 '21 at 05:58
  • 1
    Хороший вопрос, интересный. Если типы всех элементов одинаковы, то как уже сказал @avp можно использовать объединение. Если нет, то вряд ли можно придумать что-то годное. Возможно, получится что-то наколдовать с макросами, но выглядеть это будет ужасно. Овчинка не стоит выделки. – eanmos Apr 05 '21 at 06:59
  • @eanmos, к сожалению, элементы у меня имеют различные типы – Gleb Kamisaraw Apr 05 '21 at 07:35
  • 3
    А чего вы конкретно хотите достичь? Зачем вам перебирать элементы структуры? Может быть, получится придумать что-нибудь другое. – eanmos Apr 05 '21 at 07:48

3 Answers3

2

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

// gcc -Wall -Wextra -Wpedantic -std=c11 offset2.c -o offset2
# include <stddef.h>
# include <stdio.h>
# include <stdint.h>
# include <stdlib.h>
typedef
struct  s_S {
  int i [ 6 ] ;
  char c [ 6 ] ;
} S ;

define indexSize 12

int indexS [ indexSize ] = { offsetof ( S , i [ 0 ] ) , offsetof ( S , i [ 1 ] ) , offsetof ( S , i [ 2 ] ) , offsetof ( S , i [ 3 ] ) , offsetof ( S , i [ 4 ] ) , offsetof ( S , i [ 5 ] ) , offsetof ( S , c [ 0 ] ) , offsetof ( S , c [ 1 ] ) , offsetof ( S , c [ 2 ] ) , offsetof ( S , c [ 3 ] ) , offsetof ( S , c [ 4 ] ) , offsetof ( S , c [ 5 ] ) } ;

enum { typeSInt , typeSChar } ;

int typeS [ indexSize ] = { typeSInt , typeSInt , typeSInt , typeSInt , typeSInt , typeSInt , typeSChar , typeSChar , typeSChar , typeSChar , typeSChar , typeSChar } ;

int main ( ) { volatile S s ; int i ; for ( i = 0 ; i < indexSize ; ++ i ) switch ( typeS [ i ] ) { case typeSInt : * ( volatile int * ) ( ( ( uint8_t * ) & s ) + indexS [ i ] ) = 0 ; break ; case typeSChar : * ( volatile char * ) ( ( ( uint8_t * ) & s ) + indexS [ i ] ) = '0' ; break ; default : fprintf ( stderr , "main : typeS [ %d ] = %d\n" , i , typeS [ i ] ) ; exit ( 1 ) ; } fprintf ( stdout , "s = { { %d , %d , %d , %d , %d , %d } ,\n" " { '%c' , '%c' , '%c' , '%c' , '%c' , '%c' } }\n" , s . i [ 0 ] , s . i [ 1 ] , s . i [ 2 ] , s . i [ 3 ] , s . i [ 4 ] , s . i [ 5 ] , s . c [ 0 ] , s . c [ 1 ] , s . c [ 2 ] , s . c [ 3 ] , s . c [ 4 ] , s . c [ 5 ] ) ; }

Спецификатор типа volatile S s ; нужен обязательно, так-как вы модифицируете структуру не прямо, и компилятор не видит, как вы изменяете саму структуру по-элементно.


Стандарт :

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned69) for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

Перевод :

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

Используя offsetof получаем правильное (реальное) выравнивание поля структуры.

AlexGlebe
  • 17,227
  • 4
    От этого так и веет UB. Во-первых, нарушается strict aliasing rule. Во-вторых, type punning октетов в int может запросто закончится trap representation. В общем, такое использовать точно не стоит. – eanmos Apr 05 '21 at 06:38
  • ? по-русски пожалуйста. У меня всегда всё работало. @eanmos – AlexGlebe Apr 05 '21 at 06:40
  • 2
    По русски? Пожалуйста:https://ru.stackoverflow.com/a/503266/177993 – gbg Apr 05 '21 at 06:43
  • Я всегда это напоминаю. Только volatile. @eanmos @gbg – AlexGlebe Apr 05 '21 at 06:44
  • 2
    Во-первых, разве в станарте что-то сказано про то, что volatile как-то может повлиять на strict-aliasing? Я не помню такого. Во-вторых, я не вижу практического смысла в языке, созданном для агрессивных оптимизаций, активно препятствовать применению этих оптимизаций. – gbg Apr 05 '21 at 06:48
  • 2
    Кроме того, не совсем уверен, но мне кажется использование volatile приводит к еще одному UB при попытке записи в volatile-объект через non-volatile lvalue. – eanmos Apr 05 '21 at 06:49
  • Всё очень правильно написано. Приведение указателя на поле, с правильным типом не приводит к неопределёнке. Неопределёнка это у оптимизатора, она решается volatile. @gbg @eanmos – AlexGlebe Apr 05 '21 at 06:54
  • Aliasing (псевдонимы/наложение/алиасинг) - решается volatile. Strict aliasing (строгий алиасинг) - не нарушен, типы правильные. @gbg @eanmos – AlexGlebe Apr 05 '21 at 07:03
  • @AlexGlebe на основании какого места в стандарте вы это утверждаете? – gbg Apr 05 '21 at 07:05
  • 3
    Достаточной причиной сжечь этот код будет то, что при касте uint8_t в int может не совпасть выравнивание, что на некоторых системах приведет к краху, на других (ARM, например) добавляет penalty к доступу. Вот по этому и ввели правило строгих псевдонимов (strict aliasing rule). И volatile никак эту проблему не решает. – eanmos Apr 05 '21 at 07:07
  • Стандарт разрешает менять типы указателей, и если всё правильно выравнено, то этот указатель правильно указывает. см. ответ @gbg – AlexGlebe Apr 05 '21 at 07:21
  • Увидел update ответа. Я был не прав, offsetof даст нам правильное выравнивание, что решает проблему misaligned access. Но это не отменяет нарушение strict aliasing rule (UB) и, как следствие, возможность получения trap representation, которое потом будет использоваться (UB). Кроме того, мне не нравится этот volatile, возможно здесь еще одно UB (см. мой комментарий выше). – eanmos Apr 05 '21 at 07:27
  • Правильное выравнивание необходимое, но недостаточное условие. См. ISO/IEC 9899:2011 §6.5 ¶7. – eanmos Apr 05 '21 at 07:28
  • 2
    @eanmos, Кроме того, мне не нравится этот volatile, возможно здесь еще одно UB. Да, это UB. n1570, 6.7.3/6: ... If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.. – wololo Apr 05 '21 at 09:47
  • 2
    плохой совет... хотя я бы сказал, что нарушения SA здесь всё же нет, да и UB даже без volatile (точнее только без volatile, wololo прав) тоже вроде нет... а вот попытка исправить что-то с его помощью — дурацкая идея, да и такая адресная арифметика — весьма грязная штука... – Fat-Zer Apr 05 '21 at 09:49
  • Если грамотно пользоваться, то проблем не будет. Вы привели предупреждение, что если программист сделал сам приведение типа с volatile в без него, то это уже на его совести будет (это его лажа). @wololo – AlexGlebe Apr 05 '21 at 10:08
  • 1
    Если убрать volatile, то вроде бы никаких UB больше нет. 1) Выравнивание соблюдается, т.к. смещение рассчитывается с помощью offsetof. 2) strict aliasing rule тоже не нарушается, т.к. указатель ( ( uint8_t * ) & s ) + indexS [ i ] указывает на объект типа int, следовательно его можно разыменовать. 3) Да, объект типа int может содержать trap representation, но это не важно, ведь мы присваиваем новое значение объекту, а не читает trap representation. – wololo Apr 05 '21 at 10:14
  • 3
    @AlexGlebe, Вы привели предупреждение какое ещё предупреждение? Я привёл цитату из стандарта языка. Попытка доступа к volatile-объекту через lvalue with non-volatile-qualified type приводит к неопределённому поведению. Все гарантии стандарта снимаются, конечный результат выполнения программы никак не специфицируется. Если грамотно пользоваться И как это сделать? Как вы поняли, что грамотно сняли volatile-квалификатор? – wololo Apr 05 '21 at 10:22
  • @wololo, ваш комментарий сделал все мои комментарии выше нерелевантными :) Вернее, они и были нерелевантными, но понял я это только после вашего комментария. Я почему-то зациклился на этих строках с кастом и совершенно проигнорировал весь окружающий код. UB хоть и нет, но выглядит это ужасно, так что код все еще нужно предать огню. – eanmos Apr 05 '21 at 10:27
2

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

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

Тут главное, при программировании цикла по полям структуры, не запутаться с типом источника, которым будем заполнять каждое поле.

Получается что-то в таком духе.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>

struct fld_descr { size_t off, len; };

void set_fld_val (void target, struct fld_descr d[], int i, void val) { memcpy((char *)target + d[i].off, val, d[i].len); }

int main() { struct tstv { int a, b, c; double x, y; char ch; char s[10]; } x = {1, 2, 3, 9.8, 3.62, 'z', "start"};

printf("x: %d %d %d, %f %f, '%c' <%s>\n", x.a, x.b, x.c, x.x, x.y, x.ch, x.s);

#define DSCR(struct, field_name, t) {offsetof(struct, field_name), sizeof(t)} struct fld_descr d[] = { DSCR(struct tstv, a, x.a), DSCR(struct tstv, b, x.a), DSCR(struct tstv, c, x.a), DSCR(struct tstv, x, double), DSCR(struct tstv, y, x.y), DSCR(struct tstv, ch, char), DSCR(struct tstv, s, char[10]) };

for (int i = 0; i < 10; i++) { int j = i % (sizeof(d) / sizeof(d[0]));

char str[10];
sprintf(str, &quot;step%d&quot;, i);
double dv = 3.14 / (i + 1);
void *p[7] = {&amp;i, &amp;i, &amp;i, &amp;dv, &amp;dv, &amp;str[4], &amp;str[0]};

set_fld_val(&amp;x, d, j, p[j]);
printf(&quot;set %d x: %d %d %d, %f %f, '%c' &lt;%s&gt;\n&quot;, j,
       x.a, x.b, x.c, x.x, x.y, x.ch, x.s);

}

return puts("End") == EOF;
}

Транслируем и запускаем

avp@avp-desktop:~/avp/hashcode$ gcc ttt.c -O2 -Wall && ./a.out
x: 1 2 3, 9.800000 3.620000, 'z' <start>
set 0 x: 0 2 3, 9.800000 3.620000, 'z' <start>
set 1 x: 0 1 3, 9.800000 3.620000, 'z' <start>
set 2 x: 0 1 2, 9.800000 3.620000, 'z' <start>
set 3 x: 0 1 2, 0.785000 3.620000, 'z' <start>
set 4 x: 0 1 2, 0.785000 0.628000, 'z' <start>
set 5 x: 0 1 2, 0.785000 0.628000, '5' <start>
set 6 x: 0 1 2, 0.785000 0.628000, '5' <step6>
set 0 x: 7 1 2, 0.785000 0.628000, '5' <step6>
set 1 x: 7 8 2, 0.785000 0.628000, '5' <step6>
set 2 x: 7 8 9, 0.785000 0.628000, '5' <step6>
End
avp@avp-desktop:~/avp/hashcode$ 
avp
  • 46,098
  • 6
  • 48
  • 116
1

В старых версиях с++ смещение внутри структуры можно легко высчитать вычетом адресов.

Допустим

struct {
  int a;
  int b;
  int c;
  } s;

int offs_a = (int)(((char)&s.a) - ((char)&s)); // Смещение a int offs_b = (int)(((char)&s.b) - ((char)&s)); // Смещение b int offs_c = (int)(((char)&s.c) - ((char)&s)); // Смещение c

Теперь если к адресу любой структуре такого же типа добавить смещение - получится адрес нужного поля. Что бы не писать кучу кода, можно упростить программу используя макросы, template-шаблоны, overload-функции, или implicit/explicit операторы. Используя эти средства - можно сделать "всеядные" операции для полей разного типа.

По поводу елементы идут или не идут подрят. Есть два вида структуры. Упакованая структура, и неупакованная. В упакованй - все элементы идут строго подрят без лишних байт. В неупакованой - компилятор добавляет байты по границам выравнивания (для x86 по гранциам двойного слова), поля тоже идут подрят, но могут быть с вкраплениями. Управление выравниванием делается с помощью #pragma pack, а так же с помощью опций компилятора. Как правило оно включено. Если вы пишите структуру в файл - то можно не делать разбор на поля, и записывать её всю одним куском, не считая поля-указатели - их нужно обрабатывать отдельно. В отдельных случаях где порядок важен, в программу добавляют assert где проверяют размер структуры на нужное значение.

UDP: компилятор может вычеркнуть все ненужные дествия вы не рассказали. (Это лечиться с помощью модификатора volatile - из коментариев к вопросу.

Покажу обработку полей класса, методом overload функцию (перегруженой). В упрощённом виде. Компилятор сам выберет нужную work1. Таким образом нужна только "таблица" (точнее "список") выгружаемых/загружаемых полей.

class x1 {
  int a;
  char b;
  char* name;

void process(void * handle, bool read) { work1(a,handle,read); // Тут можно свернуть через #define work1(b,handle,read); // Признак можно вложить в класс handle work1(name,handle,read);//handle это источник откуда/куда читать/писать }

void read(void * handle) { process(handle,true); }

void write(void * handle) { process(handle,false); }

// Тут реализовать обработку каждого типа данных void work1(char & field, void data, bool read) { // Запись чтение char* } void work1(char & field, void* data, bool read) {// Запись чтение char } void work1(int & , void* data, bool read) { // Запись чтение int } };

Но можно организовать это чуть иначе, через массив и overload конструктор, тогда получится как-то так.

class worker { // это класс читатель-писатель
  int type;
  union { // Что бы не было UB
     int * intField;
     char * charField;
     char ** pcharField;
     } u;
 public:
 worker();
 worker(int & field) { u.intField = &field; type = 1;} ;
 worker(char & field){ u.charField = &field; type=2;};
 worker(char *& field){ u.pcharField = &field; type=3;};

void process(void* hanle, bool read ) { // Добавить обработчик } };

// это наш класс class x1 { public: int a; char b; char* name;

void process(void * handle, bool read) { worker w[] = {a,b,name}; // Просто список полей for (int i=0; i < sizeof(w)/sizeof(w[0]); i++) w[i].process(hanlde,read); // И каждое обработаем }

void read(void * handle) { process(handle,true); }

void write(void * handle) { process(handle,false); } };

Так же можно использовать template-класс (worker) и обобщить конструктор. Но это будет сложнее для понимания, и так же сложнее отделить поведение класов друг от друга.

nick_n_a
  • 8,057
  • У вас у указателей при преобразовании типа урезается половина числа. нужно делать после вычисления смещения адресов(int)(..-..) Про то что компилятор может вычеркнуть все ненужные дествия вы не рассказали. (Это лечиться с помощью модификатора volatile) – AlexGlebe Apr 05 '21 at 13:21
  • Я проверил вычисление адресов у себя через (int)(..-..) - мой компилятор выругался, и не собирает этот код. Может надо size_t или какой-то другой тип.. там long ставить, что бы везде работало.... надо подумать. – nick_n_a Apr 05 '21 at 19:53
  • разница двух указателей должны быть одного типа (на что указывают). Эта разница означает на сколько элементов этого типа отличаются эти указатели. int arr[10]; (&(arr[10])-&(arr[5])) == 5 интов . А так как нужно смещение считать в байтах, то эти указатели должны указывать на uint8_t. ((uint8_t*)(&(arr[10]))-((uint8_t*)&(arr[5])) == 20 байт https://godbolt.org/z/84KqEoe1o – AlexGlebe Apr 05 '21 at 20:19
  • Хорошо, согласен. Так можно. – nick_n_a Apr 05 '21 at 20:25