Это UB, чтобы привести указатель к пустому указателю и записать в него? - PullRequest
2 голосов
/ 14 января 2020

Я работал над способом создания динамических c массивов в C, и я придумал это решение в качестве общей структуры того, как я хочу, чтобы мои функции / макросы работали:

//dynarray.h
#define dynarray(TYPE)\
    struct{\
        TYPE *data;\
        size_t size;\
        size_t capacity;\
    }

int dynarray_init_internal(void **ptr, size_t *size, size_t *cap, size_t type_size, size_t count);

#define dynarray_init(ARR, SIZE) dynarray_init_internal(&ARR->data, &ARR->size, &ARR->capacity, sizeof(*ARR->data), SIZE)

//dynarray.c
int dynarray_init_internal(void **ptr, size_t *size, size_t *cap, size_t type_size, size_t count){
    *ptr = malloc(type_size*count);
    if(*ptr == NULL){
        return 1;
    }

    *size = 0;
    *cap = count;
    return 1;
}

Является ли это приемлемым подходом для создания обобщенной c комбинированной функции / макроса, которая имеет дело с динамическим распределением памяти способом типа c?

Единственное, что у меня вызывает сомнения, это то, что я не уверен, является ли это неопределенным поведением или нет. Я предполагаю, что это может быть легко расширено для других функций, которые обычно ожидаются для структуры динамического массива c. Единственная проблема, с которой я могу столкнуться, заключается в том, что, поскольку это анонимная структура, вы не можете передать ее в качестве аргумента в любом месте (по крайней мере, легко), но это можно легко исправить, создав макрос dynarray_def(TYPE, NAME), который определит динамик c массив массива с NAME и он содержит данные TYPE, в то же время он работает со всеми другими функциями / макросами, перечисленными выше.

Ответы [ 2 ]

3 голосов
/ 14 января 2020

Это неопределенное поведение, потому что вы конвертируете (например) int ** в void ** и разыменовываете его, чтобы получить void *. Автоматическое преобразование c в / из void * не , а распространяется на void **. Чтение / запись одного типа как другого (в данном случае запись int * как void *) является нарушением.

Лучший способ справиться с этим - сделать всю процедуру инициализации макросом:

#define dynarray_init(ARR, SIZE) \
do {\
    (ARR)->data = malloc(sizeof(*(ARR)->data*(SIZE));\
    if ((ARR)->data == NULL){\
        _exit(1);\
    }\
    (ARR)->size = 0;\
    (ARR)->capacity = (SIZE);\
} while (0)

РЕДАКТИРОВАНИЕ:

Если вы хотите уклониться от функционально-подобных макросов, вы можете вместо этого использовать макрос для создания функции и типа структуры, с которым она работает:

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

#define dynarray(TYPE)\
struct dynarray_##TYPE {\
    TYPE *data;\
    size_t size;\
    size_t capacity;\
};\
\
int dynarray_##TYPE##_init(struct dynarray_##TYPE **ptr, size_t count){\
    *ptr = malloc(sizeof(*ptr)*count);\
    if(*ptr == NULL){\
        return 1;\
    }\
    \
    (*ptr)->size = 0;\
    (*ptr)->capacity = count;\
    return 1;\
}

// generate types and functions    
dynarray(int)
dynarray(double)

int main()
{
    struct dynarray_int *da1;
    dynarray_int_init(&da1, 5);
    // use da1
    struct dynarray_double *da2;
    dynarray_double_init(&da2, 5);
    // use da2

    return 0;
}
0 голосов
/ 15 января 2020

Поскольку некоторые редкие реализации используют разные представления для разных типов указателей, Стандарт не требует, чтобы реализации позволяли им взаимозаменяемо манипулировать ими. Вместо этого он рассматривает поддержку таких манипуляций как «популярное расширение», для которого поддержка является проблемой «качества реализации» за пределами его юрисдикции. Практически любой компилятор для платформы с удаленным распространением будет настраиваться для поддержки конструкции, и хотя авторы стандарта хотели дать программистам «шансы на победу» [их слова] для написания переносимого кода, они явно заявили, что сделали не sh, чтобы "унизить" программы, которые не были на 100% переносимыми.

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

Обратите внимание, что даже хорошо качественные компиляторы могут быть сбиты с толку некоторыми достаточно хитрыми шаблонами использования, включающими приведение указателей. Авторы Стандарта не хотели запрещать реализациям выполнять полезную оптимизацию только потому, что некоторые хитрые и надуманные шаблоны использования могли привести к неправильному поведению, но они ожидали, что реализации смогут распознавать шаблоны, которые их пользователи фактически используют. Например, с учетом:

float f;
int *ip; float *fp;
int *ipp = (int**)(&fp);
...
void test(void)
{
  fp = &f;
  f = 1.0;
  **ip+=1;
  return f;
}

компилятор не сможет реально c определить, что запись в **ip может реально повлиять на объект типа float. Однако, если адрес fp был сохранен в ip между записью в f и последующим чтением из нее, оптимизирующие компиляторы в эпоху, когда был написан стандарт, распознали бы, что преобразование T* в U* следует рассматривать как потенциальный удар памяти для любого объекта типа T*, к которому можно получить доступ через U*. Я подозреваю, что ваши шаблоны использования соответствуют последнему шаблону гораздо сильнее, чем первый.

*ipp = someFloat;
...