Является ли операнд `sizeof` оценен с помощью VLA?
Аргумент в разделе комментариев этого ответа побудил меня задать этот вопрос.
В следующем коде bar
указывает на массив переменной длины, поэтому sizeof
определяется во время выполнения вместо времени компиляции.
int foo = 100;
double (*bar)[foo];
Аргумент состоял в том, оценивает ли его операнд sizeof
, когда операнд является массивом переменной длины, делая sizeof(*bar)
undefined поведение, когда bar
не инициализируется.
Используется ли undefined поведение sizeof(*bar)
, потому что я разыменовываю неинициализированный указатель? Является ли операнд sizeof
фактически оцененным, когда тип является массивом переменной длины или он просто определяет его тип (как обычно работает sizeof
)?
Изменить: Кажется, что все цитируют этот отрывок из проекта C11. Кто-нибудь знает, если это формулировка в официальном стандарте?
Ответы
Ответ 1
Да, это вызывает поведение undefined.
В N1570 6.5.3.4/2 мы имеем:
Оператор sizeof дает размер (в байтах) своего операнда, который может быть выражение или имя в скобках типа. Размер определяется по типу операнда. Результат - целое число. Если тип операнда - это тип массива переменной длины, операнд оценивается; в противном случае операнд не оценивается, а результат является целочисленной константой.
Теперь возникает вопрос: является ли тип *bar
массивом переменной длины?
Так как bar
объявляется как указатель на VLA, разыменование его должно давать VLA. (Но я не вижу конкретного текста, определяющего, действительно ли он).
Примечание. Дальнейшее обсуждение можно было бы здесь, возможно, можно утверждать, что *bar
имеет тип double[100]
, который не является VLA.
Предположим, что мы согласны с тем, что тип *bar
на самом деле является типом VLA, а затем в sizeof *bar
оценивается выражение *bar
.
bar
является неопределенным в этой точке. Теперь посмотрим на 6.3.2.1/1:
если lvalue не определяет объект при его оценке, поведение undefined
Так как bar
не указывает на объект (в силу неопределенности), оценка *bar
вызывает поведение undefined.
Ответ 2
Два других ответа уже процитированы N1570 6.5.3.4p2:
Оператор sizeof
возвращает размер (в байтах) своего операнда, который может быть выражением или именем типа в скобках. Размер определяется из типа операнда. Результатом является целое число. Если тип операнда - тип массива переменной длины, операнд оценивается; в противном случае операнд не оценивается и результат целочисленная константа.
Согласно этому абзацу стандарта да, операнд из sizeof
оценивается.
Я собираюсь утверждать, что это дефект в стандарте; что-то оценивается во время выполнения, но операнд - нет.
Давайте рассмотрим более простой пример:
int len = 100;
double vla[len];
printf("sizeof vla = %zu\n", sizeof vla);
В соответствии со стандартом, sizeof vla
оценивает выражение vla
. Но что это значит?
В большинстве случаев вычисление выражения массива дает адрес исходного элемента, но оператор sizeof
является явным исключением из этого. Можно предположить, что оценка vla
означает доступ к значениям его элементов, что ведет к неопределенному поведению, поскольку эти элементы не были инициализированы. Но нет другого контекста, в котором оценка выражения массива обращается к значениям его элементов, и в этом случае абсолютно нет необходимости делать это. (Исправление: если строковый литерал используется для инициализации объекта массива, значения элементов оцениваются.)
Когда выполняется объявление vla
, компилятор создает некоторые анонимные метаданные для хранения длины массива (это необходимо, поскольку назначение нового значения для len
после того, как vla
определено и выделено, не меняет длина vla
). Все, что нужно сделать, чтобы определить sizeof vla
, это умножить это сохраненное значение на sizeof (double)
(или просто извлечь сохраненное значение, если оно хранит размер в байтах).
sizeof
также может применяться к имени типа в скобках:
int len = 100;
printf("sizeof (double[len]) = %zu\n", sizeof (double[len]));
Согласно стандарту выражение sizeof
оценивает тип. Что это обозначает? Очевидно, что он должен оценить текущее значение len
. Другой пример:
size_t func(void);
printf("sizeof (double[func()]) = %zu\n", sizeof (double[func()]));
Здесь имя типа включает вызов функции. Оценка выражения sizeof
должна вызвать функцию.
Но во всех этих случаях нет реальной необходимости оценивать элементы объекта массива (если он есть), и нет смысла это делать.
sizeof
, примененный к чему-либо кроме VLA, может быть оценен во время компиляции. Разница, когда sizeof
применяется к VLA (объекту или типу), состоит в том, что что-то должно быть оценено во время выполнения. Но вещь, которая должна быть оценена, не является операндом из sizeof
; это просто все, что необходимо для определения размера операнда, который никогда не является самим операндом.
Стандарт говорит, что операнд из sizeof
оценивается, если этот операнд имеет тип массива переменной длины. Это дефект в стандарте.
Возвращаясь к примеру в вопросе:
int foo = 100;
double (*bar)[foo] = NULL;
printf("sizeof *bar = %zu\n", sizeof *bar);
Я добавил инициализацию в NULL
, чтобы было еще яснее, что разыменование bar
имеет неопределенное поведение.
*bar
имеет тип double[foo]
, который является типом VLA. В принципе, оценивается *bar
, который будет иметь неопределенное поведение, поскольку bar
неинициализирован. Но опять же, нет необходимости в разыменовании bar
. Компилятор сгенерирует некоторый код при обработке типа double[foo]
, включая сохранение значения foo
(или foo * sizeof (double)
) в анонимной переменной. Все, что нужно сделать для оценки sizeof *bar
, - это получить значение этой анонимной переменной. И если бы стандарт был обновлен для последовательного определения семантики sizeof
, было бы ясно, что оценка sizeof *bar
хорошо определена и дает 100 * sizeof (double)
без необходимости разыменования bar
.
Ответ 3
Действительно, стандарт означает, что поведение undefined:
повторное цитирование N1570 6.5.3.4/2:
Оператор sizeof дает размер (в байтах) своего операнда, который может быть выражением или заключенным в скобки именем типа. Размер определяется по типу операнда. Результат - целое число. Если тип операнда - тип массива переменной длины, то операнд оценивается; в противном случае операнд не оценивается, а результат является целочисленной константой.
Я думаю, что формулировка из Стандарта путается: операнд оценивается не означает, что будет оценен *bar
. Оценка *bar
никоим образом не помогает вычислять его размер. sizeof(*bar)
необходимо вычислить во время выполнения, но для генерируемого кода для этого нет необходимости в разыменовании bar
, он, скорее всего, получит информацию о размере из скрытой переменной, содержащую результат вычисления размера во время bar
.