Если я скомпилирую следующий код с -std=c17 -Wall -Wextra -Wpedantic
с GCC 13.2.0, я не получу предупреждений, несмотря на то, что не использую void*
в аргументах, соответствующих спецификаторам формата "%p"
.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main()
{
const char cstr[] = "ABC";
size_t size = sizeof cstr;
const uint8_t ustr[] = "ABC";
const int8_t sstr[] = "ABC";
const char* pcstr = cstr;
const uint8_t* pustr = ustr;
const int8_t* psstr = sstr;
printf("cstr ptr: %p\n", cstr);
printf("size ptr: %p\n", (void*)&size); // we need cast to prevent Wformat
printf("&cstr ptr: %p\n", (void*)&cstr); // we also need this cast
printf("pcstr: %p\n", pcstr);
printf("ustr ptr: %p\n", ustr);
printf("pustr: %p\n", pustr);
printf("sstr ptr: %p\n", sstr);
printf("psstr: %p\n", psstr);
return 0;
}
После прочтения вопросов и ответов *Что такое предупреждение и как его устранить: формат «%p» ожидает аргумент типа «void *», но аргумент 2 при распечатке имеет тип «int» [-Wformat=], следует ли мне ожидать здесь неопределенного поведения? Я попробовал несколько поисков, но вы поймете, насколько сложными они могут быть для такой специфической комбинации.
Может ли быть так, что void*
и char-ish*
имеют общие свойства, которые мне не хватает, или это просто случай отсутствующих предупреждений в GCC? Надеюсь, кто-то здесь сможет пролить свет на этот вопрос.
Это похоже на одно из предупреждений «технически UB, но на практике всегда работает». Кажется, Clang предупреждает об этом, а GCC — нет.
@Клиффорд Разве порядок байтов не имеет значения для void*
?
@Wolf чисто теоретически, если разные типы указателей могут различаться по размеру, возможно, они также могут отличаться по порядку байтов. Дело в том, что %p правильно интерпретирует битовый шаблон указателя, он должен быть идентичен шаблону void*
, который, как он предполагает, был передан. Возможно, не только порядок байтов; Сегмент 8086: адресация со смещением поднимает некоторые интересные проблемы, о которых нам, к счастью, по большей части не стоит беспокоиться.
Кроме того, стандарт C требует, чтобы каждый вид указателя на объект был конвертируем в void* и обратно. На практике это означает, что каждый указатель на объект должен использовать совместимый формат, и тот же формат будет использоваться и void*. Или как еще вы собираетесь удовлетворить это требование стандарта C каким-либо разумным способом?
Вы не получаете предупреждение в случаях без приведения, поскольку void *
должен иметь то же представление, что и указатель на тип символа, т. е. char
, signed char
и unsigned char
, или любой тип, который является определением типа одного из этих типов. .
Это продиктовано разделом 6.2.5p28 стандарта C:
Указатель на void должен иметь такое же представление и выравнивание. требования как указатель на тип символа. 48)
- Те же требования к представлению и выравниванию предназначены для подразумевают взаимозаменяемость в качестве аргументов функций, возвращают значения из функции и члены профсоюзов.
Частично это связано с историей языка C до того, как void *
существовал и char *
использовался как универсальный тип указателя.
Однако в разделе 7.21.6.1p9, касающемся функции fprintf
, говорится:
Если спецификация преобразования недействительна, поведение неопределенный. Если какой-либо аргумент не является правильным типом для соответствующую спецификацию преобразования, поведение не определено.
Таким образом, хотя на самом деле это выглядит как неопределенное поведение по букве стандарта, GCC (по характеру предупреждений, которые он выдает), похоже, позволяет использовать char *
в местах, где void *
ожидается как расширение из-за требования два типа имеют одинаковое представление.
Поэтому я бы пришел к выводу, что такие конструкции безопасны в GCC, хотя и не обязательно в других компиляторах. Например, вполне возможно, что некоторые компиляторы могут использовать соглашение о вызовах, при котором void *
передается функции иначе, чем то, как она передает char *
, но опять же, это кажется натяжкой, учитывая требования к представлению.
Фактически, раздел 7.16.1.1p2 относительно макроса va_arg
гласит следующее:
... Если фактического следующего аргумента нет или тип несовместим с типом фактического следующего аргумента (как продвигается в соответствии с продвижения аргументов по умолчанию), поведение не определено, за исключением для следующих случаев:
- один тип — это целочисленный тип со знаком, другой тип — соответствующий целочисленный тип без знака, а значение может быть представлено в виде оба типа;
- один тип — это указатель на void, а другой — указатель на символьный тип.
Итак, предполагая, что fprintf
/printf
использует va_arg
для чтения переменных аргументов, поведение будет определяться вышеизложенным, но, конечно, это делает предположение о поведении семейства функций printf
.
По моему мнению, это недостаток стандарта, который следует устранить, чтобы такие преобразования были четко определены.
Именно на такой ответ я надеялся. Но сам я его найти не смог. Большое спасибо.
«Вполне возможно, что некоторые компиляторы могут использовать соглашение о вызовах, которое передает void *
функции иначе, чем то, как она передает char *
» — вы имеете в виду плавающие аргументы?
@Wolf Да, аргументы с плавающей запятой в некоторых случаях передаются в регистрах с плавающей запятой, а целочисленные значения передаются в стек. Так что теоретически разница возможна, но маловероятна.
Здесь также актуально последнее предложение 7.6.11.1p2: «… если [аргумент типа va_arg
] несовместим с фактическим типом следующего аргумента… поведение не определено… за исключением случая… . [один из двух] — указатель на void
, а другой — указатель на тип символа». Это гарантирует, что void *
и char *
взаимозаменяемы при передаче вариативной функции, которая использует va_arg
для доступа к вариативным аргументам. Однако я не могу найти каких-либо требований к функциям стандартной библиотеки с переменным числом вариантов (вести себя так, как если бы они) использовали va_arg
для доступа к аргументам с переменным числом аргументов.
(Я считаю, что обычное правило интерпретации «конкретных битов общего» будет означать, что последнее слово остается за 7.21.6.1p9, но я также считаю, что намерением комитета здесь было разрешить использование char *
с %p
без явного приведения.)
Если аргумент передается в регистре (FP или GPR), он должен быть передан в кадр стека при выполнении &arg
. Это также верно для переменной области функции, которая оптимизирована для размещения в регистре. Обратите внимание: void bar(int *y) { *y += 37; } ; int foo(int x) { bar(&x); x += 192; return x; }
«Неопределенное поведение» означает, что стандарт не определяет никаких требований. Обычно компилятор не делает ничего конкретного для обеспечения согласованного поведения (поведения, определяемого реализацией). Таким образом, если типы указателей имеют одинаковый размер и порядок байтов, вы не ожидаете никакой разницы. Реальный вопрос заключается в том, почему он не выдает предупреждение - это определена реализация.