Каковы точные условия, при которых оценивается type_name в sizeof(type_name)? GCC оценивает f() в sizeof(int [(f(), 100)])

Контекст

В стандарте говорится (C17, 6.5.3.4 ¶2):

Оператор sizeof возвращает размер (в байтах) своего операнда, который может быть выражением или именем типа в скобках. Размер определяется типом операнда. Результатом является целое число. Если тип операнда является типом массива переменной длины, операнд оценивается; в противном случае операнд не оценивается, и результатом является целочисленная константа.

Сбивает с толку то, что в формулировке не проводится различие между двумя синтаксическими контекстами sizeof:

  • sizeof унарное выражение
  • sizeof(тип-имя)

Я считаю, что имя типа технически не «имеет» тип, а скорее «обозначает» (именует, указывает) его. Кроме того, в случае имени типа в качестве аргумента я бы интуитивно сформулировал это как оценку всех выражений присваивания внутри него – если не для точности, то для ясности.

В любом случае я понимаю, что эта формулировка означает, что в случае sizeof(type) все выражения внутри type оцениваются точно в том случае, если (т. е. тогда и только тогда, когда) type обозначает тип VLA (массив переменной длины).

Проблема

Учитывая вышесказанное, последний оператор printf следующего кода дает неожиданные результаты:

#include <stdio.h>

void f(int i) {
    printf("side effect %d; ", i);
}

int main(void) {
    int n = 9;
    int a[n];
    printf("%zu\n", sizeof a);                   // 36
    printf("%zu\n", sizeof a[n++]);              // 4
    printf("%d\n", n);                           // 9
    printf("%zu\n", sizeof(int [n++]));          // 36
    printf("%d\n", n);                           // 10
    printf("%zu\n", sizeof(int [(f(0), 100)]));
      // side effect 0; 400 (type of operand: int, a non-VLA type)

    return 0;
}

(Аргумент int i из f, опущенный в заголовке вопроса, предназначен для целей отладки и для экспериментов, если кто-то хочет различать разные вызовы этой функции.)

  • Все просто и понятно: int [(f(0), 100)] обозначает тип int [100], который не является типом VLA. Итак: почему f(0) оценивается?
  • В более общем плане: каковы точные условия, при которых оценивается type_name (или выражения внутри него) в sizeof(type_name)?

Кстати, sizeof(int [f(0), 100]) (без круглых скобок вокруг выражения-запятой, обозначающего размер массива) приводит к ошибке, которую я обсуждаю в следующем вопросе: Почему выражение-запятая, используемое в качестве размера массива, должно быть заключено в круглые скобки, если это часть декларатора массива ?

Другие ссылки

Соответствующие места в стандарте (проект C17), где обсуждается синтаксис деклараторов массивов, включают: 6.7.7 ¶1, 6.7.6.2 ¶3.

Этот ответ Кита Томпсона на вопрос о VLA актуален. Но обратите внимание, что мой вопрос не касается VLA как таковых (хотя они используются в приведенном выше коде для сравнения).

Во-первых, int [100] — это тип массива. Его тип буквально int [100]. Это основа языка C, и так было всегда. Если вы объявите int a[100], то тип a будет int [100]. Это тип массива. Во-вторых, в данном контексте (f(0), 100) — это выражение. Запятая — это оператор запятой. При оценке он сначала оценивает f(0). Затем он оценивает 100, что, конечно же, всего лишь 100.

Tom Karzes 06.04.2024 11:49

Предполагается, что @TomKarzes sizeof оценивается во время компиляции, и только аргументы VLA (или предположительно типы, обозначающие их) вызывают исключение.

Lover of Structure 06.04.2024 11:50

Нет, аргумент sizeof — это массив переменной длины, поскольку (f(0), 100) не является постоянным выражением. Да, результат всегда будет 100, но первый аргумент может иметь побочные эффекты, и в любом случае компилятор не обязан рассматривать его как постоянное выражение. Константные выражения не могут содержать вызовы функций. Поскольку это не постоянное выражение, оно предназначено для VLA. Вы можете проверить это, попытавшись использовать его в объявлении static, например. static int a[(f(0), 100)];. Вы получите ошибку.

Tom Karzes 06.04.2024 11:51

Рассмотрим следующий контрастный пример: printf("%zu\n", sizeof *(f(1), &a)); // side effect 1; 36printf("%zu\n", sizeof (f(2), n)); // 4

Lover of Structure 06.04.2024 11:55
(f(0), 100) не является постоянным выражением. Период. Из этого все следует. Что ты до сих пор не понимаешь?
Tom Karzes 06.04.2024 11:56

@TomKarzes Я вижу, что стандарт (проект C17) определяет VLA способом, о котором я не знал, в 6.7.6.2 ¶4. Я задал этот вопрос, потому что считаю это определение неинтуитивным. В любом случае, хотели бы вы опубликовать ответ по этому поводу?

Lover of Structure 06.04.2024 12:01

Re: «Я считаю, что имя типа технически не «имеет» тип, а скорее «обозначает» (именует, определяет) его»: в стандартном тексте, который вы цитируете, не говорится «иметь». Там написано «есть»: «Если тип операнда является типом массива переменной длины, операнд оценивается…» Если операнд имеет грамматическую форму (*имя-типа*), то это тип, поэтому он оценил…

Eric Postpischil 06.04.2024 13:20

… Насколько я помню, стандарт C не говорит явно, что означает оценка типа или имени типа, но C 2018 6.8 4 действительно говорит: «… Существует также неявное полное выражение, в котором выражения непостоянного размера для оцениваются переменно модифицированный тип…»

Eric Postpischil 06.04.2024 13:22

@EricPostpischil Да, но расширение этого приводит к тому, что «если тип имени типа [< операнд] является типом массива переменной длины», что является нечетным.

Lover of Structure 06.04.2024 13:25

Вычисление операнда sizeof типа VLA не имеет особого смысла. Достаточно оценить только size-выражения, присутствующие в объявлениях VLA-типов. Существует дискуссия о фактической оценке значения sizeof. См. www9.open-std.org/JTC1/SC22/WG14/www/docs/n3187.htm

tstanisl 06.04.2024 18:04
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
10
115
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В соответствии со стандартом C17 (6.5.3.4 Операторы sizeof и _Alignof)

2 Оператор sizeof выдает размер (в байтах) своего операнда, который может быть выражением или именем типа в скобках. Размер определяется типом операнда. Результатом является целое число. Если тип операнда является типом массива переменной длины, операнд оценивается; в противном случае операнд не оценивается и результатом является целочисленная константа.

и (6.7.6.2 Объявители массива)

4 Если размер отсутствует, тип массива является неполным. Если размер равен *, а не является выражением, тип массива — это Тип массива переменной длины неопределенного размера, который можно использовать только в объявлениях или именах типов с областью действия прототипа функции;146) такие тем не менее, массивы являются полными типами. Если размер является целым числом постоянное выражение и тип элемента имеет известный постоянный размер, тип массива не является типом массива переменной длины; в противном случае Тип массива — это тип массива переменной длины. (Массивы переменной длины являются условной функцией, которую реализации не обязательно поддерживать; видеть 6.10.8.3.)

И наконец (6.6 Константные выражения):

2 Константное выражение может быть вычислено во время трансляции, а не чем время выполнения, и, соответственно, может использоваться в любом месте, где константа может быть.

и

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

Как в этом выражении

sizeof(int [(f(0), 100)])

подвыражение (f(0), 100) с оператором запятой не является постоянным подвыражением в объявлении массива, тогда объявляется массив переменной длины, размер которого оценивается во время выполнения.

Таким образом, во всех этих звонках printf

printf("%zu\n", sizeof a);                   // 36
printf("%zu\n", sizeof(int [n++]));          // 36
printf("%zu\n", sizeof(int [(f(0), 100)]));

используются массивы переменной длины. Их размеры могут быть определены во время выполнения.

С другой стороны, если вы напишете, например

printf("%zu\n", sizeof( (f(0), 100)));

тогда функция f() не будет вызываться, поскольку в этом случае оператор запятая является подвыражением постоянного выражения с оператором sizeof.

Короче говоря, если в объявлении массива размер массива не указан как постоянное целочисленное выражение (и оператор запятая не является постоянным целочисленным выражением согласно приведенной выше цитате), то массив является массивом переменной длины.

Каким может быть осмысленный пример «за исключением случаев, когда они содержатся в подвыражении, которое не оценивается»?

Lover of Structure 06.04.2024 14:27

@LoverofStructure Это последний пример вызова printf в моем ответе.

Vlad from Moscow 06.04.2024 14:29

В «кроме случаев, когда они содержатся в подвыражении, которое не оценивается», относятся ли «они» к «постоянным выражениям» или к списку запрещенных вещей? То есть в вашем последнем операторе printf оператор-запятая в (f(0), 100)) (или во всем выражении) находится «в подвыражении, которое не оценивается»? Если да, то какой именно? Он не оценивается, поскольку его тип не является типом VLA, в отличие от int [(f(0), 100)], который обозначает тип VLA? В последнем случае (f(0), 100) оценивается, потому что он является частью этого типа VLA?

Lover of Structure 06.04.2024 16:11

@LoverofStructure Чтобы определить размер массива переменной длины, необходимо оценить его спецификацию типа. Для определения размера выражения (f(0), 100) достаточно определить тип выражения без его вычисления.

Vlad from Moscow 07.04.2024 13:45

Я считаю, что имя типа технически не «имеет» тип, а скорее «обозначает» (именует, указывает) его.

Это не имеет значения. Текст в C 2018 6.5.3.4 2 не основан на том, что операнд «имеет» тип. В нем говорится, что размер определяется типом операнда. Если операнд равен ( type-name ), то тип операнда — type-name.

(f(0), 100) не является целочисленным константным выражением, поскольку 6.6 3 говорит:

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

Итак, int [(f(0), 100)] — это тип массива переменной длины, поскольку в 6.7.6.2 4 указано:

… Если размер представляет собой целочисленное константное выражение, а тип элемента имеет известный постоянный размер, тип массива не является типом массива переменной длины; в противном случае тип массива является типом массива переменной длины…

Следовательно, применяется 6.5.3.4 2: «Если тип операнда является типом массива переменной длины, операнд оценивается;…» Итак, когда оценивается sizeof(int [(f(0), 100)]), оценивается его операнд (int [(f(0), 100)]). Насколько я помню, в стандарте C не указано явно, что означает вычисление имени типа, но в 6.8 4 упоминается:

… Существует также неявное полное выражение, в котором оцениваются выражения непостоянного размера для изменяемого типа…

Итак, для int [(f(0), 100)] должно вычисляться полное выражение, содержащее (f(0), 100).

Другие вопросы по теме