В стандарте говорится (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 как таковых (хотя они используются в приведенном выше коде для сравнения).
Предполагается, что @TomKarzes sizeof
оценивается во время компиляции, и только аргументы VLA (или предположительно типы, обозначающие их) вызывают исключение.
Нет, аргумент sizeof
— это массив переменной длины, поскольку (f(0), 100)
не является постоянным выражением. Да, результат всегда будет 100, но первый аргумент может иметь побочные эффекты, и в любом случае компилятор не обязан рассматривать его как постоянное выражение. Константные выражения не могут содержать вызовы функций. Поскольку это не постоянное выражение, оно предназначено для VLA. Вы можете проверить это, попытавшись использовать его в объявлении static
, например. static int a[(f(0), 100)];
. Вы получите ошибку.
Рассмотрим следующий контрастный пример: printf("%zu\n", sizeof *(f(1), &a)); // side effect 1; 36
printf("%zu\n", sizeof (f(2), n)); // 4
(f(0), 100)
не является постоянным выражением. Период. Из этого все следует. Что ты до сих пор не понимаешь?
@TomKarzes Я вижу, что стандарт (проект C17) определяет VLA способом, о котором я не знал, в 6.7.6.2 ¶4. Я задал этот вопрос, потому что считаю это определение неинтуитивным. В любом случае, хотели бы вы опубликовать ответ по этому поводу?
Re: «Я считаю, что имя типа технически не «имеет» тип, а скорее «обозначает» (именует, определяет) его»: в стандартном тексте, который вы цитируете, не говорится «иметь». Там написано «есть»: «Если тип операнда является типом массива переменной длины, операнд оценивается…» Если операнд имеет грамматическую форму (
*имя-типа*)
, то это тип, поэтому он оценил…
… Насколько я помню, стандарт C не говорит явно, что означает оценка типа или имени типа, но C 2018 6.8 4 действительно говорит: «… Существует также неявное полное выражение, в котором выражения непостоянного размера для оцениваются переменно модифицированный тип…»
@EricPostpischil Да, но расширение этого приводит к тому, что «если тип имени типа [< операнд] является типом массива переменной длины», что является нечетным.
Вычисление операнда sizeof
типа VLA не имеет особого смысла. Достаточно оценить только size-выражения, присутствующие в объявлениях VLA-типов. Существует дискуссия о фактической оценке значения sizeof
. См. www9.open-std.org/JTC1/SC22/WG14/www/docs/n3187.htm
В соответствии со стандартом 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
.
Короче говоря, если в объявлении массива размер массива не указан как постоянное целочисленное выражение (и оператор запятая не является постоянным целочисленным выражением согласно приведенной выше цитате), то массив является массивом переменной длины.
Каким может быть осмысленный пример «за исключением случаев, когда они содержатся в подвыражении, которое не оценивается»?
@LoverofStructure Это последний пример вызова printf в моем ответе.
В «кроме случаев, когда они содержатся в подвыражении, которое не оценивается», относятся ли «они» к «постоянным выражениям» или к списку запрещенных вещей? То есть в вашем последнем операторе printf
оператор-запятая в (f(0), 100))
(или во всем выражении) находится «в подвыражении, которое не оценивается»? Если да, то какой именно? Он не оценивается, поскольку его тип не является типом VLA, в отличие от int [(f(0), 100)]
, который обозначает тип VLA? В последнем случае (f(0), 100)
оценивается, потому что он является частью этого типа VLA?
@LoverofStructure Чтобы определить размер массива переменной длины, необходимо оценить его спецификацию типа. Для определения размера выражения (f(0), 100) достаточно определить тип выражения без его вычисления.
Я считаю, что имя типа технически не «имеет» тип, а скорее «обозначает» (именует, указывает) его.
Это не имеет значения. Текст в 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)
.
Во-первых,
int [100]
— это тип массива. Его тип буквальноint [100]
. Это основа языка C, и так было всегда. Если вы объявитеint a[100]
, то типa
будетint [100]
. Это тип массива. Во-вторых, в данном контексте(f(0), 100)
— это выражение. Запятая — это оператор запятой. При оценке он сначала оцениваетf(0)
. Затем он оценивает100
, что, конечно же, всего лишь100
.