Является ли операнд `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.