Как использовать typeof_unqual, чтобы избежать предупреждений компилятора об отмене квалификатора const
В следующем кодеcopyThing()
записывается так, что возвращаемое значение имеет тот же тип, что и значение аргумента:
extern void *copyThingImpl(const void *x);
#define copyThing(x) ((typeof(x)) copyThingImpl(x))
void foo(void);
void foo(void)
{
char *x = "foo";
char *y;
y = copyThing(x);
(void) y;
}
При этом используются широко доступныеtypeof
оператор. Это работает нормально и компилируется без предупреждений (например,gcc -Wall -O2 -c test.c
).
Теперь, если мы изменимx
к указателю, мы столкнемся с легкими проблемами:
extern void *copyThingImpl(const void *x);
#define copyThing(x) ((typeof(x)) copyThingImpl(x))
void foo(void);
void foo(void)
{
const char *x = "foo";
char *y;
y = copyThing(x);
(void) y;
}
Это вызывает предупреждение:
warning: assignment discards 'const' qualifier from pointer target type
это правильно, потому что(typeof(x))
является(const char *)
, поэтому присвоение не-y
уронитconst
.
Насколько я понимаю, новый оператор в C23 должен иметь возможность решить этот случай. Поэтому я стараюсь
extern void *copyThingImpl(const void *x);
#define copyThing(x) ((typeof_unqual(x)) copyThingImpl(x))
void foo(void);
void foo(void)
{
const char *x = "foo";
char *y;
y = copyThing(x);
(void) y;
}
Если я скомпилирую это с соответствующей опцией, поддерживающей C23 (например,gcc -Wall -O2 -std=c2x -c test.c
), я все еще получаю то же предупреждение от различных версий gcc и clang.
Я правильно это использую? В чем смыслtypeof_unqual
если это не сработает?
5 ответов
typeof_unqual(x)
удаляет только квалификаторы, относящиеся к самому себе, а не к объекту, на который указывает, если это указатель.
Чтобы удалитьconst
от указанного типа вы должны разыменоватьx
в макросе:
#define copyThing(x) ((typeof_unqual(*(x)) *)copyThingImpl(x))
Вышеупомянутая версия работает в последних версиях gcc и clang, но требует явного стандарта c23 и некоторых дополнительных флагов, оба из которых зависят от компилятора.
Проблема вашего кода в том, что переменная (указатель) не является постоянной. Это строковый литерал, на который указывает постоянный указатель.
const char *x = "foo";
Если вы напишете, например
const char * const x = "foo";
тогда квалификатор будет удален из указателяx
в объявлении указателяy
.
Итак, вы могли бы написать
void foo(void)
{
const char * const x = "foo";
const char *y;
y = copyThing(x);
(void) y;
}
В этом случае выражениеcopyThing(x)
не будет постоянным указателем.
То естьtypeof( x )
есть иtypeof_unqual( x )
является .
Вы можете написать, например
typedef const char * T;
В этом случае эквивалентноconst char * const
и удаление квалификатораconst
из имени typedefconst T
ты получишьconst char *
.
В стандарте C23 есть пример. Объявлен массив типа
const char* const animals[] = {
"aardvark",
"bluejay",
"catte",
};
и это заявление
typeof_unqual(animals) animals2_array;
объявляет массив типа
const char* animals2_array[3];
это массив непостоянных указателей на строковые литералы.
Кстати, обратите внимание, что в C, противоположные строковым литералам C++ (по историческим причинам), есть типы непостоянных символьных массивов, хотя любая попытка изменить строковый литерал в C, как в C++, приводит к неопределенному поведению.
По сути, это просто новый вариант старого vs.char*const ptr
ЧАСТО ЗАДАВАЕМЫЕ ВОПРОСЫ. Все слева от указывает на указанный тип, а все справа от*
принадлежит самой переменной указателя.
Поэтому в случае ,typeof_unqual(ptr)
оставит тебя сconst char* ptr
. Но если бы былconst char*const ptr
, мы получаем - указанный тип остается неизменным, но квалификаторы принадлежат переменнойptr
себя удалили.
Вы действительно можете исправить это выражение, чтобы оно работало так, как задумано, с помощью(typeof_unqual(*x)*) copyThingImpl(x))
поскольку нам разрешено добавлять звездочку послеtypeof
/typeof_unqual
. Объяснение:
-
x
является . -
*x
являетсяconst char
. -
typeof_unqual(*x)
являетсяchar
. -
typeof_unqual(*x)*
является . -
(typeof_unqual(*x)*)
Внешняя скобка делает это приведением.
Если оставить в стороне всю «языковую юриспруденцию», подобные макросы, вероятно, нельзя использовать ни для каких целей. Если фактическое намерение заключалось в обеспечении безопасности типов, то макрос должен был быть объявлен как:
#define copyThing(x) ( (char*) _Generic((*x), char: copyThingImpl)(x) )
Общее выражение отбрасывает квалификаторы (полагаю, согласно C17) - 6.5.1.1 «Тип управляющего выражения — это тип выражения, как если бы оно подверглось преобразованию lvalue». Это означает, что приведенный выше макрос примет,const char*
(иvolatile char*
и т.д.) и эквиваленты массива, но не указатели на другие типы, включаяvoid*
.
Если код скомпилирован, поскольку был указан правильный тип, результатcopyThingImpl(x)
приведён кchar*
, который в этом примере был единственным допустимым типом.
typeof_unqual
удаляет только квалификаторы верхнего уровня . Так что это превратится
const char * const
к
const char *
Но в вашем примереconst
применяется к указанному типу, а не к самой переменной, поэтому это не квалификатор верхнего уровня.
Если вы попытаетесь избежать предупреждения об отбрасывании квалификатора и попытаетесь исправить его, выполнив приведение типов, вы столкнетесь с серьезными проблемами в своем проекте, поскольку вы нарушите систему типов, которой нет, но которая помогает вам локализовать плохой тип. использует.
Квалификатор — это то, что вы добавляете , чтобы позволить компилятору попытаться изменить или обновить некоторые данные. Если вы назначите/передадите эти данные в качестве параметра нецелевому получателю, вы получите предупреждение, поскольку окончательные защищенные данные больше не защищены.Компилятор любезно напоминает вам об этом. Но если вы скажите компилятору, пожалуйста, просто забудьте мое абсурдное определение, поскольку я сумасшедший и люблю поступать таким образом , тогда зачем вообще объявлять данные???
Если у вас есть какие-то данные указателя, и вы хотите назначить им псевдоним, сделайте это, также объявив переменную псевдонима. Никогда не используйте приведение.
Я помню особый случай, когда мне пришлось передать указатель на , но в определении free нет спецификации (вероятно, потому, что разработчики предполагали, что послеfree()
завершил свою работу, выпущенные данные - и, следовательно, сам указатель - вообще непригодны для использования, и поэтому их лучше реализовать какvoid free(void *);
чемvoid free(const void *);
, но это мешает вызвать free по указателю, и вы получите вот такую проблему) (вариант использования здесь не представляет интереса, поэтому не буду объяснять, почему пришлось использоватьconst *
на выделенной памяти, которую якобы мне пришлось потом защищать, после инициализации)
Я решил проблему простым приведением к перед передачей указателя, но я предпочел бы, чтобы разработчикиfree(3)
обернуть внутрь , затемconst void *
параметр дляvoid *
на случай, если они предпочтут реализовать некоторую запись в ранее недоступной для записи выделенной памяти.
В любом случае, если вы собираетесь нарушить систему типов, просто используйте приведение для(void *)
или любой другой удобный для вас тип, а затем вообще не используйте его в своем коде.
Если ваша функция:
extern void *copyThingImpl(const void *x);
в некоторых случаях подверженreturn
исходное переданное значение указателя, то оно должно было быть определено как:
extern const void *copyThingImpl(const void *x);
и внутренне изменить всеchar *
определенияconst char *
, вместо. Это правильный и типобезопасный способ сделать то, что вы хотите.
Как правило, старайтесь быть максимально снисходительными в том, что вы принимаете (используйте в параметрах, только если вы вообще не трогаете данные, и только тогда) и максимально строгими в том, что вы возвращаете (например, используйте if по какой-то причине возвращаемые вами данные не могут быть изменены, как если бы вам нужно было вернуть указатель, который был ) Переменная может быть инициализирована с помощью неconst
data (это сделает данные, хранящиеся в копии, недоступными для изменения), но обратное невозможно. Это проверяется компилятором при каждом назначении или параметре, передаваемом функции.