Когда и как оцениваются VLA в выражениях sizeof?

Стандарт C имеет этот язык:

6.5.3.4 Операторы sizeof и _Alignof

Семантика

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

Мне неясно, что Стандарт подразумевает под Если тип операнда является типом массива переменной длины, операнд оценивается

  • Если тип операнда представляет собой тип массива переменной длины, кажется, что нет никакой цели для оценки аргумента, поскольку размер может быть определен из определения типа, как это предусмотрено в 6.7.6.2 Массив. деклараторы, что Размер каждого экземпляра типа массива переменной длины не меняется в течение его жизни.
  • С другой стороны, если операнд представляет собой имя в скобках типа массива переменной длины, например, в sizeof(char[foo()]), выражение размера должно оцениваться во время выполнения для вычисления размера, но язык Стандарта, похоже, не охватывает этот случай ( что такое тип имени типа?)

Следует ли изменить язык стандарта C для уточнения?

Вот тестовая программа, иллюстрирующая поведение в некоторых конкретных случаях VLA:

#include <stdio.h>

static int N = 0;
int foo(void) { return ++N; }

int main() {
    typedef char S[foo()];      // foo() is called
    printf("typedef char S[foo()];\t");                             printf("N=%d\n", N);
    printf("sizeof(S)=%d\t\t", (int)sizeof(S));                     printf("N=%d\n", N);

    typedef char U[foo()];      // foo() is called
    printf("typedef char U[foo()];\t");                             printf("N=%d\n", N);
    printf("sizeof(U)=%d\t\t", (int)sizeof(U));                     printf("N=%d\n", N);

    S s1;
    printf("S s1;\t\t\t");                                          printf("N=%d\n", N);
    printf("sizeof(s1)=%d\t\t", (int)sizeof(s1));                   printf("N=%d\n", N);

    S s2;
    printf("S s2;\t\t\t");                                          printf("N=%d\n", N);
    printf("sizeof(s2)=%d\t\t", (int)sizeof(s2));                   printf("N=%d\n", N);

    U u1;
    printf("U u1;\t\t\t");                                          printf("N=%d\n", N);
    printf("sizeof(u1)=%d\t\t", (int)sizeof(u1));                   printf("N=%d\n", N);

    U *pu1 = &u1;
    printf("U *pu1 = &u1;\t\t");                                    printf("N=%d\n", N);
    printf("sizeof(*pu1)=%d\t\t", (int)sizeof(*pu1));               printf("N=%d\n", N);

    U *pu2 = NULL;
    printf("U *pu2 = NULL;\t\t");                                   printf("N=%d\n", N);
    // sizeof(*pu2) does not evaluate *pu2, contrary to the Standard specification
    printf("sizeof(*pu2)=%d\t\t", (int)sizeof(*pu2));               printf("N=%d\n", N);

    char x2[foo()][foo()];      // foo() is called twice
    printf("char x2[foo()][foo()];\t");                             printf("N=%d\n", N);
    printf("sizeof(x2)=%d\t\t", (int)sizeof(x2));                   printf("N=%d\n", N);
    printf("sizeof(x2[0])=%d\t\t", (int)sizeof(x2[0]));             printf("N=%d\n", N);

    // sizeof(char[foo()]) evaluates foo()
    printf("sizeof(char[foo()])=%d\t", (int)sizeof(char[foo()]));   printf("N=%d\n", N);
    return 0;
}

Вывод (как clang, так и gcc):

typedef char S[foo()];  N=1
sizeof(S)=1             N=1
typedef char U[foo()];  N=2
sizeof(U)=2             N=2
S s1;                   N=2
sizeof(s1)=1            N=2
S s2;                   N=2
sizeof(s2)=1            N=2
U u1;                   N=2
sizeof(u1)=2            N=2
U *pu1 = &u1;           N=2
sizeof(*pu1)=2          N=2
U *pu2 = NULL;          N=2
sizeof(*pu2)=2          N=2
char x2[foo()][foo()];  N=4
sizeof(x2)=12           N=4
sizeof(x2[0])=4         N=4
sizeof(char[foo()])=5   N=5

person chqrlie    schedule 21.07.2020    source источник
comment
sizeof(*pu2) does not evaluate *pu2, contrary to the Standard specification Вы имеете в виду, что если будет оцениваться sizeof(*pu2), то вы ожидаете, что будет вызван foo()?   -  person KamilCuk    schedule 21.07.2020
comment
Мне нравится использовать int i = 0; char a[rand()%2 + 1]; printf("%zu\n", sizeof a[i++]); printf("%d\n", i); шаг i. int i = 0; char a[42]; printf("%zu\n", sizeof a[i++]); printf("%d\n", i); не увеличивает i, поскольку i в аргументе sizeof не оценивается. @chqrlie Это близко к тому, что вы ищете?   -  person chux - Reinstate Monica    schedule 21.07.2020
comment
sizeof(*pu2) не оценивает *pu2, что противоречит стандартной спецификации. Возможно, он оценивается, и вы получаете неопределенное поведение, которое выглядит так, как будто оно не оценивается.   -  person Language Lawyer    schedule 21.07.2020
comment
@LanguageLawyer: учитывая, что pu2 явно инициализируется как NULL, оценка *pu2 должна иметь неопределенное поведение, которое действительно может остаться незамеченным, например, если код пропущен, что вполне вероятно, поскольку размер, если тип U, можно определить, даже не глядя на pu2.   -  person chqrlie    schedule 22.07.2020
comment
@KamilCuk: я не ожидаю, что foo() будет оцениваться, но разыменование нулевого указателя должно иметь видимые побочные эффекты, хотя стандарт этого не требует. На самом деле, даже если pu2 имеет квалификацию volatile, указатель не разыменовывается clang.   -  person chqrlie    schedule 22.07.2020
comment
@chux-ReinstateMonica: в вашем примере оценка i++ не требуется для определения типа sizeof a[i++], и на самом деле тип a[i++] не является типом массива переменной длины, поэтому оценка не должна выполняться. . Также обратите внимание на 6.7.6.2 Деклараторы массивов p5 Если выражение размера является частью операнда оператора sizeof и изменение значения выражения размера не повлияет на результат оператора, оно не указано, оценивается ли выражение размера. a должно быть определено как char a[10][rand() % 2 + 1]; для sizeof a[i++] для увеличения i.   -  person chqrlie    schedule 22.07.2020
comment
@chqrlie Согласен, что этот пример был плохим.   -  person chux - Reinstate Monica    schedule 22.07.2020
comment
@chqrlie: Но на каком основании вы заключаете, что оценка обязательно включает преобразование lvalue? *pu2 явно оценивается в выражении *pu2 = 42;, но поскольку это левый операнд оператора присваивания, он не подвергается преобразованию lvalue (раздел 6.3.2.1/p2). Это же предложение также исключает операнд оператора sizeof из преобразования lvalue, поэтому нет оснований полагать, что sizeof *pu2 будет разыменовывать NULL больше, чем *pu2 = 42.   -  person rici    schedule 22.07.2020
comment
С char x2[foo()][foo()]; неясно, есть ли у вас char x2[3][4] или char x2[4][3]. Порядок оценки, вероятно, зависит от компилятора. Вы должны напечатать sizeof(x2[0]), чтобы узнать, что применимо.   -  person Jonathan Leffler    schedule 22.07.2020
comment
@JonathanLeffler: действительно, вероятно, по крайней мере зависит от компилятора: clang в OS/X печатает sizeof(x2[0])=4, но gcc в linux печатает sizeof(x2[0])=3. Я не нашел ничего в Стандарте, чтобы снять эту двусмысленность.   -  person chqrlie    schedule 22.07.2020


Ответы (2)


Если тип операнда представляет собой тип массива переменной длины, кажется, что нет никакой цели для оценки аргумента, поскольку размер может быть определен из определения типа, как это предусмотрено в 6.7.6.2 Массив. деклараторы, что Размер каждого экземпляра типа массива переменной длины не меняется в течение его жизни.

Но этот размер неизвестен до тех пор, пока массив не будет создан во время выполнения. Некоторая оценка должна выполняться во время выполнения. Какой именно должна быть эта оценка, не уточняется.

Следует ли изменить язык стандарта C для уточнения?

Я думаю, что да. Я считаю следующую идиому невероятно полезной для динамического выделения 2D-массивов, где количество строк и столбцов неизвестно до времени выполнения:

int rows, cols;
...
T (*arr)[cols] = malloc( sizeof *arr * rows );

Однако, как в настоящее время формулируется Стандарт, это (скорее всего) вызывает неопределенное поведение, потому что я оцениваю *arr во время выполнения, но arr в этот момент не инициализирован (и, скорее всего, недействителен). Вам не нужно разыменовывать arr, чтобы получить размер типа массива, но, к сожалению, язык стандарта не настолько детализирован. Я хотел бы видеть язык, похожий на Если тип операнда является типом массива переменной длины, операнд оценивается с целью получения только размера массива.

person John Bode    schedule 21.07.2020
comment
Это всего лишь один из многих случаев, когда достаточно педантичное прочтение частей Стандарта может охарактеризовать действия как Неопределенное Поведение, имеющие одно значение, которое в противном случае можно было бы вывести, прочитав другие части Стандарта и документацию по платформе. Авторы стандарта ожидали, что составители компиляторов будут отдавать приоритет определенному поведению в тех случаях, когда у них нет причин поступать иначе, и поэтому считали необходимым только обеспечить, чтобы правила определяли обычное поведение в тех случаях, когда у реализации могут быть причины отклоняться от них. их. - person supercat; 22.07.2020

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

person supercat    schedule 21.07.2020
comment
Таким образом, Стандарт преднамеренно двусмыслен в отношении крайних случаев, связанных с переменно-модифицируемыми типами... Я думаю, что он должен быть еще более двусмысленным и указать, что Если тип операнда является массивом переменной длины тип, операнд может оцениваться с целью определения размера типа. Это сделало бы еще худшие угловые случаи, связанные с оператором запятой, неопределенными, такими как int n=1, a[n]; (void)sizeof *(printf("Gotcha!"), &a); - person chqrlie; 22.07.2020
comment
@chqrlie: Было бы полезно, если бы Стандарт определял терминологию для различных способов использования выражений, включая разрешение [что делается с lvalue в левой части оператора присваивания или в правой части &, или в результате неявного распада массива] и size [что делается с помощью sizeof]. Затем можно указать эффекты оценки, разрешения и определения размеров выражений в терминах того, какие части оцениваются, разрешаются и измеряются. - person supercat; 22.07.2020