Почему это неявное преобразование (между разными типами указателей) допустимо?
Я оказался в следующей ситуации:
#include <stdio.h>
typedef struct T1 { int id; } T1;
typedef struct T2 { int id; } T2;
void f(T1 *ptr) { printf("f called\n"); }
int main(void)
{
T2 obj;
T2 *ptr = &obj;
f(ptr); // shouldn't this be a compilation error ?
return 0;
}
конечно, это недопустимо С++, но в C программа печатает "f called". Как это справедливо?
ИЗМЕНИТЬ
(На всякий случай это неясно). Программа по-прежнему компилирует и запускается, если T2
"структурно" отличается, например
typedef struct T2 { double cc[23]; } T2;
Ответы
Ответ 1
Это неверно, если вы хотите принудительно выполнить стандартный совместимый код, важно скомпилировать с правильными флагами, например, как gcc
, так и clang
следующие флаги:
-std=c99 -pedantic-errors
создаст ошибку для диагностики, требуемую стандартом C99, и аналогичным образом вы можете использовать -std=c11
для C11. Это приведет к появлению следующей ошибки от gcc
(увидеть ее в прямом эфире):
ошибка: передача аргумента 1 из 'f' из несовместимого типа указателя
У компиляторов есть расширения и разрешены такие функции, как implicit int из-за устаревшего кода, и важно знать разницу. Подробнее см. gcc document: Языковые стандарты, поддерживаемые GCC.
Быстрый способ увидеть, что это действительно недействительно, - перейти к Обоснование для языков международного стандартного программирования-C, в которых говорится в разделе 6.3.2.3
Указатели, которые имеют дело с преобразованиями, которые:
Неправильно преобразовать указатель на объект любого типа в указатель на объект другого типа без явного приведения.
Для получения более длинного пути мы перейдем к черновик проекта C99 6.5.2.2
Вызов функций, который говорит (акцент мой вперед):
Если выражение, обозначающее вызываемую функцию, имеет тип, который включает прототип, аргументы неявно преобразуются, так как если по назначению,
и если мы перейдем к разделу 6.5.16
Операторы присваивания, который гласит:
Одно из следующих действий должно выполняться
и для указателей имеем:
- оба операнда являются указателями на квалифицированные или неквалифицированные версии совместимых типов, а тип, на который указывает левый, имеет все квалификаторы типа, на которые указывает справа;
- один операнд является указателем на объект или неполным типом, а другой - указателем на квалифицированную или неквалифицированную версию void и тип, на который указывают левые, имеет все квалификаторы типа указана справа;
- левый операнд - это указатель, а справа - константа нулевого указателя;
мы видим, что ни один из этих случаев не выполняется, поэтому преобразование недействительно.
Ответ 2
При компиляции я получаю следующее предупреждение:
temp.c: В функции main:
temp.c: 20: 5: warning: передать аргумент 1 из 'f из несовместимого типа указателя [включен по умолчанию]
temp.c: 13: 6: note: expected 'struct T1 *, но аргумент имеет тип' struct T2 *
Он "действителен", потому что они оба указатели и, следовательно, могут быть преобразованы, это просто не очень хорошая идея.
Ответ 3
В соответствии со стандартом C99, раздел 6.5.2.2 [вызовы функций], параграф 7, это valid разрешен в C, но недействителен.
Например, в вашем коде, если T1
и T2
представляют собой разные структуры, имеющие разные элементы, а адрес T2
передается в f()
и принимается как T1*
, то это абсолютно неправильно и результат является фатальным. На всякий случай, когда он скомпилирован [он должен был выпустить предупреждения о passing argument <number> of <a function> from incompatible pointer type
], не означает, что это правильно.
В вашем коде, поскольку вы не получаете доступа к структурным переменным внутри f()
, из-за совместимой оптимизации предупреждения могли быть исчезны.
Он читает
Если выражение, обозначающее вызываемую функцию, имеет тип, который включает прототип, аргументы неявно преобразуются, так как если по назначению, к типам соответствующих параметров, принимая тип каждого параметра является неквалифицированной версией его объявленный тип. Обозначение многоточия в прототипе функции declarator вызывает преобразование типа аргумента для остановки после последнего объявленный параметр. Активные объявления по умолчанию выполняются на конечные аргументы.
Ответ 4
Как отмечали другие, в C этот код требует диагностики (и, видимо, вы его получили, в виде предупреждения gcc).
Вы можете "исправить" код приложением:
f( (T1 *)ptr );
В вашей примерной программе это нормально. Однако в более сложной программе возникнет проблема. Поскольку T1
и T2
не являются совместимыми типами, то f
записывает через указатель, а затем чтение через ptr
(или наоборот) будет строгое сглаживание.
Вы можете обойти проблему, хотя, пользуясь тем, что союзы могут использоваться для сглаживания в C, и что существует специальное условие для членов объединения, являющихся структурами с общей начальной последовательностью:
union
{
T1 t1;
T2 t2;
} obj;
f( &obj.t1 ); // might write to ptr->id
printf("%d\n", obj.t2.id); // OK, writes int that f wrote
Так как пример объединения должен работать, любой здравомыслящий компилятор просто использовал бы общий макет для T1
и T2
и не выполнял бы никаких оптимизаций с строгим сглаживанием здесь, поэтому можно было бы разумно ожидать, что код с литой будет "просто работайте", как вы видите в своем примере.