Изменение объекта const – нет предупреждения? Кроме того, в этом случае это UB?

Почему в следующем коде нет предупреждений?

int deserialize_students(const Student *dest, const int destCapacityMax) { FILE *ptr_file; int i=0; ptr_file =fopen("output.txt","r"); if (!ptr_file) return -1; if(destCapacityMax==0) return -2; while (!feof (ptr_file)) { fscanf (ptr_file, "%d", &dest[i].id); // UB? fscanf (ptr_file, "%s", dest[i].name); fscanf (ptr_file, "%d", &dest[i].gender); i++; if(i==destCapacityMax) return 0; } fclose(ptr_file); return 0; } 

Вот как я это назвал:

 Student students[5]; deserialize_students(students,5); 

Также у меня есть следующий вопрос: что я сделал неопределенное поведение? Замечания:

  • Я думаю, что проходящие students в порядке, потому что функция ожидает const Student* и я могу пройти не const . Правильно?
  • Но когда я fscanf этот объект в fscanf , я fscanf UB? Или это зависит от того, были ли students объявлены const или нет в первую очередь (вне этой функции)?

В вашем коде нет неопределенного поведения.

То, что передал вызывающий, является изменчивым объектом. Так что это нормально, чтобы изменить его либо напрямую, либо явным образом:

 int func(const int *p) { int *q = (int*)p; *q = 5; } 

(вероятно, нелогично, но легально), пока объект, переданный func() является изменчивым.

Но если переданный объект был const то это было бы неопределенным поведением. Так что в вашем случае это не определено.

Спецификатор const является только контрактом, что функция не должна изменять dest . Он не влияет на фактическую изменчивость объекта. Таким образом, модификация const-qual вызывает UB или нет, зависит от того, прошел ли переданный объект к любому такому определителю.

Что касается предупреждения, GCC (5.1.1) предупреждает:

 int func(const int *p) { fscanf(stdin, "%d", p); } 

с:

 warning: writing into constant object (argument 3) [-Wformat=] 

Вероятно, VS не признает, что fscanf() изменяет объект. Но стандарт C только говорит, что он не определен, если вы изменяете объект, сконструированный с помощью константы:

(C11 draft, 6.7.3, 6)

Если предпринимается попытка изменить объект, определенный с помощью типа, соответствующего const, с использованием значения lvalue с неконстантированным classом, поведение не определено.

Диагностика, требуемая стандартом C, не требуется, если код вызывает неопределенное поведение. В общем, вы сами по себе, если ваш код вызывает UB, а компилятор не сможет помочь вам по всем причинам.

ИМХО это формальный UB.

Функция deserialize_students объявляет параметр const Student *dest . Оттуда, dest[i].id является dest[i].id int, а &dest[i].id является &dest[i].id const int * .

Вы не получаете предупреждения, потому что fscanf – это вариационная функция, а компилятор не может контролировать константу (даже если gcc использует ее как специальный случай), но если вы использовали временную переменную temp, вы получите ошибку:

 int id; fscanf (ptr_file, "%d", &id); dest[i].id = id; // here you get an error 

Таким образом, вы передаете const-указатель на функцию, которая модифицирует pointee ( fscanf ) и IMHO, достаточно, чтобы квалифицировать ее как формальный UB. Можно представить себе реализацию компилятора, который передал бы указатель на копию значения fscanf , так как вы обещали, что это будет const. Или это передало бы указатель на копию массива students поскольку deserialize_students объявляет свой параметр как const.

Есть ли реальный риск? IMHO нет, потому что, когда вы передаете модифицируемое dest функции, нормальная реализация компилятора будет просто передавать исходный адрес и будет тем же самым передавать адрес dest[i].id в fscanf . Таким образом, все это закончится правильной модификацией исходного массива. Но, как уже сказал Питер, как и все случаи неопределенного поведения, один из возможных результатов работает так, как вы ожидаете , поэтому работа со всеми проверенными компиляторами не является страхованием за то, что вы не являетесь неопределенным поведением.

NB: поскольку исходный массив не является константой, объект не был определен как const, поэтому я не уверен, что здесь применяется 6.7.3, § 6. Но 6.7.3 § 9 все еще говорит: для двух совместимых типов должны быть одинаково квалифицированные версии совместимого типа, поэтому int * (требуется fscanf), а const int * (фактически передано) – нет.

Стандарт неясен по этому вопросу. Из C11 7.21.6.2/12:

Спецификаторы преобразования и их значения:

d Соответствует произвольно подписанному десятичному целому числу, формат которого совпадает с ожидаемым для последовательности объектов функции strtol со значением 10 для базового аргумента. Соответствующим аргументом должен быть указатель на целое число со знаком .

Теперь вы можете утверждать, что, поскольку он не говорит «указатель на не-const signed integer», это на самом деле означает, что int * и const int * оба в порядке. Или вы можете утверждать, что они означают указатель на целое число со знаком, не связанным со знаком.

Обратите внимание, что определение передачи vararg (7.16) явно говорит о том, что для получения аргумента типа const int * undefined использовать va_arg(int *) . Однако функция fscanf НЕ указана, чтобы вести себя так, как если бы использовался va_arg. Так что это не имеет прямого отношения.


Мнение следует: вся спецификация printf и scanf – это неряшливый беспорядок, который ниже обычного качества спецификации, найденного в остальной части стандарта. Существует много других таких примеров, например, в соответствии с точной формулировкой стандарта, printf("%lx", 1L); это неопределенное поведение, тогда как printf("%lx", -1L); не является неопределенным поведением.

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

Это неопределенное поведение.

Как отметил Майкл Уолз в комментариях, причина, по которой компилятор согласился с этим, заключается в том, что fscanf() является переменным, поэтому некоторые проверки компилятора, включая константу, ослаблены.

Как и все случаи неопределенного поведения, один возможный результат работает так, как вы ожидаете, – с выбранным вами компилятором и библиотекой, но не обязательно с другим. Вот почему тестирование не является надежным средством идентификации присутствия неопределенного поведения – передача любого теста также разрешена.