Соответствует ли формальное определение ограничения C11 реализации?

Пытаясь ответить на недавний вопрос (Передача ограниченных квалифицированных указателей на функции?) , я не смог найти насколько стандарт С11 согласуется с практикой.

Я не пытаюсь ссылаться на стандарт или что-то в этом роде, большинство вещей, которые выглядят непоследовательными, я просто неправильно понимаю, но мой вопрос лучше всего формулировать как аргумент против определения, используемого в стандарте, так что вот оно.

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

// set C to componentwise sum and D to componentwise difference

void dif_sum(float* restrict C, float* restrict D, size_t n)
{
     size_t i = 0;
     while(i<n) C[i] = C[i] - D[i],
                D[i] += C[i] + D[i],
                ++i;
}



// set A to componentwise sum of squares of A and B
// set B to componentwise product of A and B

void prod_squdif(float* restrict A, float* restrict B, size_t n)
{
     size_t i = 0;
     float x;
     dif_sum(A,B,n);
     while(i<n) x = ( (A[i]*=A[i]) - B[i]*B[i] )/2,
                A[i] -= x,
                B[i++] = x/2;
}

Что кажется общим пониманием, так это то, что ограничивающие указатели должны ссылаться на независимое пространство в своем блоке объявления. Таким образом, prod_sqdif действителен, потому что ничто лексически в его определяющем блоке не обращается к массивам, идентифицированным A или B, кроме этих указателей.

Чтобы продемонстрировать мою озабоченность стандартом, вот стандартное формальное определение ограничения (согласно проекту комитета, если у вас есть окончательная версия и она отличается, дайте мне знать!):

6.7.3.1 Формальное определение ограничения

1 Пусть D будет объявлением обычного идентификатора, который предоставляет средства для обозначения объекта P как указателя с ограничением на тип T.

2 Если D появляется внутри блока и не имеет внешнего класса хранения, пусть B обозначает блок. Если D появляется в списке объявлений параметров определения функции, пусть B обозначает соответствующий блок. В противном случае пусть B обозначает блок main (или блок любой функции, вызываемой при запуске программы в автономной среде).

3 В дальнейшем говорят, что выражение указателя E основано на объекте P, если (в какой-то момент выполнения B перед вычислением E) P модифицируется так, чтобы он указывал на копию объекта массива, в котором он ранее point изменит значение E. Обратите внимание, что «основанный» определен только для выражений с типами указателей.

4 Во время каждого выполнения B пусть L будет любым значением l, которое имеет &L на основе P. Если L используется для доступа к значению объекта X, которое он обозначает, и X также модифицируется (любыми способами), то следующие требования apply: T не должен быть const-квалифицированным. Любое другое lvalue, используемое для доступа к значению X, также должно иметь свой адрес, основанный на P. Каждый доступ, который изменяет X, также должен рассматриваться как модифицирующий P для целей настоящего подпункта. Если P присваивается значение выражения указателя E, которое основано на другом ограниченном объекте указателя P2, связанном с блоком B2, то либо выполнение B2 должно начаться до выполнения B, либо выполнение B2 должно закончиться до выполнения B2. назначение. Если эти требования не выполняются, то поведение не определено.

5 Здесь выполнение B означает ту часть выполнения программы, которая соответствует времени жизни объекта скалярного типа и продолжительности автоматического хранения, связанного с B.

6 Переводчик волен игнорировать любое или все последствия использования слова limited, связанные с псевдонимами.

[Примеры не включены, поскольку они не имеют формального значения.]

Идентификация выполнения B с лексически содержащимися в нем выражениями может рассматриваться как поддерживаемая следующей выдержкой из 6.2.4, пункт 6:

"...Вход во вложенный блок или вызов функции приостанавливает, но не заканчивает выполнение текущего блока..."

Однако в части 5 формального определения ограничения блок B явно определяется как соответствующий сроку жизни объекта с автоматическим хранением, объявленным в B (в моем примере B — это тело prod_squdif). Это явно переопределяет любое определение выполнения блока, найденное в другом месте стандарта. Следующая выдержка из стандарта определяет время жизни объекта.

6.2.4 Сроки хранения объектов, п.2

Время жизни объекта — это часть выполнения программы, в течение которой для него гарантированно резервируется память. Объект существует, имеет постоянный адрес и сохраняет свое последнее сохраненное значение на протяжении всей своей жизни. 34) Если на объект ссылаются за пределами его времени жизни, поведение не определено. Значение указателя становится неопределенным, когда объект, на который он указывает (или только что прошедший), достигает конца своего времени существования.

Тогда выполнение dif_sum явно входит в выполнение B. Не думаю, что тут есть какие-то вопросы. Но тогда значения lvalue в dif_sum, которые считывают и изменяют элементы A и B (через C и D), явно не основаны на A и B (они следуют точкам последовательности, где A и B могли быть переназначены на копии их содержимого без изменения местоположения, идентифицированные lvalues). Это неопределенное поведение. Обратите внимание, что то, какие значения lvalue или точки последовательности обсуждаются в пункте 4, не ограничено; как указано, нет причин ограничивать lvalue и порядковые точки теми, которые лексически соответствуют блоку B, и поэтому lvalue и порядковые точки внутри вызова функции играют точно так же, как и в теле вызывающей функции.

С другой стороны, общепринятое использование ограничения, по-видимому, подразумевается тем фактом, что формальное определение явно позволяет присваивать C и D значения A и B. Это предполагает, что некоторый осмысленный доступ к A и B через C и D является разрешается. Однако, как утверждалось выше, такой доступ не определен для любого элемента, измененного с помощью внешнего или внутреннего вызова функции и, по крайней мере, прочитанного с помощью внутреннего вызова. Это кажется противоречащим очевидному намерению разрешить присвоение в первую очередь.

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

Таким образом, интерпретируя выполнение B как выполнение каждого оператора в течение времени существования автоматического хранилища B, вызовы функций не могут работать с содержимым переданных им указателей ограничения.

Мне кажется неизбежным, что должно быть какое-то исключение, указывающее, что чтение и запись внутри функций или подблоков не учитываются, но самое большее одно назначение в таком подблоке (и других подблоках, рекурсивно) может быть основано на каком-либо конкретном ограничении. указатель во внешнем блоке.

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

РЕДАКТИРОВАТЬ: Как уже отмечалось, нарушение контракта с ограничением приводит к неопределенному поведению. Мой вопрос не о том, что происходит, когда контракт нарушается. Мой вопрос можно переформулировать так:

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

Я ищу ответ, основанный на стандарте, поскольку я согласен с тем, что указатели ограничения должны передаваться через вызовы функций. Просто кажется, что это не следствие формального определения в стандарте.

РЕДАКТИРОВАТЬ

Я думаю, что основная проблема с передачей моего вопроса связана с определением «на основе». Здесь я попытаюсь изложить свой вопрос немного иначе.

Ниже приведено неофициальное отслеживание конкретного вызова prod_squdif. Это не код C, это просто неформальное описание выполнения функционального блока.

Обратите внимание, что это выполнение включает в себя выполнение вызываемой функции в соответствии с пунктом 5 формального определения ограничения: «Здесь выполнение B означает ту часть выполнения программы, которая соответствует времени жизни объекта со скалярным типом и продолжительность автоматического хранения, связанная с B."

// 1. prod_squdif is called
prod_squdif( (float[1]){2}, (float[1]){1}, 1 )

// 2. dif_sum is called
dif_sum(A,B,n)   // assigns C=A and D=B

// 3. while condition is evaluated
0<1   // true

// 4. 1st assignment expression
C[0] = C[0] - D[0]   // C[0] == 0

// 5. 2nd assignment expression
D[0] += C[0] + D[0]   // D[0] == 1

// 6. increment
++i // i == 1

// 7. test
1<1   // false

// return to calling function

// 8. test
0<1   // true

// 9. 1st assignment expression
x = ( (A[0]*=A[0]) - B[1]*B[1] )/2   // x == -.5

// 10. 2nd assignment expression
A[0] -= -.5   // A[0] == .5

// 11. 3rd assignment expression
B[i++/*evaluates to 0*/] = -.5/2   // B[0] == -.25

// 12. test
1<1   // false

// prod_squdif returns

Итак, тест на ограничение контракта дается пунктом 4 в формальном определении ограничения: «Во время каждого выполнения B пусть L будет любым lvalue, имеющим &L на основе P. Если L используется для доступа к значению объекта X, который он обозначает, и X также изменен (любым способом), то применяются следующие требования: ... Каждое другое lvalue, используемое для доступа к значению X, также должно иметь свой адрес, основанный на P..."

Пусть L будет lvalue слева от части выполнения, отмеченной '4' выше (C[0]). Основан ли &L на A? То есть, C основан на A?

См. пункт 3 формального определения ограничения: «... говорят, что выражение указателя E основано на объекте P, если (в какой-то момент выполнения B перед вычислением E) изменение P так, чтобы оно указывало на a копия объекта массива, на который он ранее указывал, изменит значение E...".

Возьмем за точку последовательности конец пункта 3 выше. (В этой точке последовательности) изменение A для указания на копию объекта массива, на который он ранее указывал, НЕ изменит значение C.

Таким образом, C не основан на A. Таким образом, A[0] модифицируется lvalue, не основанным на A. Поскольку он также считывается lvalue, основанным на A (элемент 10), это поведение undefined.

Мой вопрос: правильно ли поэтому сделать вывод, что мой пример вызывает неопределенное поведение и, следовательно, формальное определение ограничения не согласуется с общей реализацией?


person Kyle    schedule 01.04.2016    source источник
comment
Я не понимаю. Конечно, время жизни объекта с автоматической продолжительностью хранения продолжается во время выполнения функции, вызываемой в блоке, в котором объект находится в области видимости. В противном случае вы не могли бы передать его адрес вызываемой функции.   -  person EOF    schedule 01.04.2016
comment
Мне непонятно, как вы пришли к какому-либо заключению. Однако у меня сложилось впечатление, что вы считаете выполнение dif_sum частью блока, в котором оно вызывается. Если это так, ваше предварительное условие уже неверно.   -  person too honest for this site    schedule 01.04.2016
comment
@EOF Да, я не оспариваю этого. Тот факт, что время жизни сохраняется в вызываемой функции, а также тот факт, что выполнение B (как определено выше) привязано к этому времени жизни, показывает, что ограничения на lvalue сохраняются и в вызове функции.   -  person Kyle    schedule 01.04.2016
comment
@Olaf, выполнение dif_sum, насколько я могу судить, считается частью выполнения вызывающей функции для этих целей в соответствии с пунктом 5 формального определения ограничения. Вот чего я не понимаю. Выполнение dif_sum — это часть выполнения программы, соответствующая продолжительности автоматического хранения вызывающей функции.   -  person Kyle    schedule 01.04.2016
comment
Но он не является частью контекста блока, т.е. не имеет доступа к локальным переменным. IOW: он не становится частью блока. У меня смутное впечатление, что вы путаете область (структура времени компиляции), время жизни (не знаю, как это связано с проблемой) и цепочку вызовов (проблема времени выполнения). Извините, не уверен, как я могу быть яснее, что я имею в виду. Поскольку я не уверен, что это просто я не понимаю ваш вопрос или он действительно не ясен, я оставлю это обсуждение носителям языка.   -  person too honest for this site    schedule 01.04.2016
comment
Ваш вопрос будет улучшен, если вы сможете опубликовать MCVE, показывающий поведение, которое, как вы утверждаете, не определено (но вы считаете, что оно должно быть определено).   -  person M.M    schedule 01.04.2016
comment
Точки последовательности не имеют к этому никакого отношения   -  person M.M    schedule 01.04.2016
comment
@MM, свойство выражения, основанного на указателе, указано в пункте 3 выше. В этом контексте C не основан на A. Возьмите точку следования в вызываемой функции, но до использования C. В этой точке последовательности скопируйте A в новое пространство и переназначьте A этому массиву. Выражения lvalue, основанные на C, в результате не меняются. Итак, как я понимаю, на основе определения это означает, что эти lvalue не основаны на A.   -  person Kyle    schedule 01.04.2016
comment
Используйте код, чтобы показать, что вы имеете в виду, вместо того, чтобы описывать некоторые возможности   -  person M.M    schedule 01.04.2016
comment
@MM, поэтому я считаю точки последовательности важными, они являются частью определения, основанного на пункте 3 выше.   -  person Kyle    schedule 01.04.2016
comment
@M.M Я был бы рад, но я не совсем понимаю, что вы хотели бы видеть. У меня не было проблем с этой проблемой, которые я мог бы показать вам явно, я просто беспокоюсь о своем понимании стандарта. Что я могу добавить к своим двум функциям, чтобы указать на мою озабоченность? Мне кажется, что эти две функции демонстрируют поведение, которое обычно понимается как определенное. Я сделал все возможное, чтобы объяснить, почему я считаю, что стандарт называет поведение prod_dif неопределенным. Я буду рад включить все, что, по вашему мнению, может помочь.   -  person Kyle    schedule 01.04.2016
comment
Вы говорите о копировании A в новое пространство и переназначении A и т. д., но ваш пример кода не включает переназначение A. Вы утверждаете, что ваш образец кода не определен или что? Вы должны показать MCVE, демонстрирующий проблему, о которой вы говорите.   -  person M.M    schedule 01.04.2016
comment
Копия — это ссылка на пункт 3 в определении: изменение P так, чтобы оно указывало на копию объекта массива, на который он ранее указывал, изменит значение E. Это гипотетическая возможность копирования, обсуждаемая в определении.   -  person Kyle    schedule 01.04.2016
comment
Может быть, вы могли бы дочитать до конца страницы, где сноска раскрывает ваше непонимание? 137) Другими словами, E зависит от значения самого P, а не от значения объекта, на который косвенно ссылается P. Например, если идентификатор p имеет тип (int **restrict), то выражения указателя p и p+1 основаны на ограниченном объекте указателя, обозначенном p, а выражения указателя *p и p[1] — нет.   -  person EOF    schedule 01.04.2016
comment
@EOF, сноски формально не являются частью стандарта. Кроме того, спорно, применимо ли это к C. C основан на значении A, когда оно присваивается, но последующие выражения, использующие C, основаны на значении C, а не на значении, которое все еще хранится в A. Это различие проводится пунктом 3 определения. Например, если бы A было присвоено A_ в вызывающей функции, выражения, основанные на A_, не были бы основаны на A, даже если исходное присвоение было основано на A.   -  person Kyle    schedule 01.04.2016
comment
@ Олаф, пожалуйста, продолжай, если можешь. То, что вы говорите, звучит как возможное объяснение. Единственное, что меня беспокоит, это то, что пункт 5 определения, по-видимому, относится к выполнению программы, которая будет включать в себя содержимое вызовов функций. Кроме того, предполагается, что подблоки ведут себя как вызовы функций в примерах, следующих за формальным определением, а подблоки кажутся структурами времени компиляции. Буду признателен, если у вас есть что сказать по этому вопросу.   -  person Kyle    schedule 01.04.2016
comment
Я думаю, вы имеете в виду, если бы A_ были назначены A. (т.е. A = A_;) Но на самом деле ваш код этого не делает. И это никоим образом не актуально. Вы не предоставите пример кода, поэтому я предполагаю, что вы описываете что-то вроде dif_sum(A,B,n); A = A_; Но поведение кода в dif_sum определялось тем, что происходит до этого момента, а не тем, что происходит потом. C основано на том, что было A, когда вы вызвали dif_sum, а не на каком-либо последующем назначении. C даже не существует к моменту достижения A = A_;.   -  person M.M    schedule 01.04.2016
comment
@MM, если какие-то конкретные примеры кода помогут, пожалуйста, дайте мне несколько указаний о том, что я могу предоставить. Однако я думаю, что проблема в том, что формальное определение ограничения включает гипотетические фрагменты кода (в пункте 3 гипотетическое копирование содержимого P и указание P на копию). Это относится не к чему-либо на самом деле в коде, а скорее к тому, что гипотетически может быть в коде. Я просто пытаюсь применить это гипотетическое правило к моей ситуации. Я не включил код, демонстрирующий такую ​​копию, потому что копия гипотетическая.   -  person Kyle    schedule 01.04.2016
comment
написание A = A_; перед вызовом dif_sum означало бы, что C имеет другое значение, чем оно было бы, если бы вы не написали это, поэтому C основано на A. (об этом говорит пункт 3)   -  person M.M    schedule 01.04.2016
comment
@Kyle: Я действительно хотел бы, но будь то тяжелый день здесь или просто нехватка правильных фраз, я не уверен, как выразить то, что я имею в виду на английском языке. Может кто подхватит мою мысль. Если я найду время, я вернусь к этому в ближайшие несколько дней.   -  person too honest for this site    schedule 01.04.2016
comment
Беда в том, что ИДК какой гипотетический код вы себе представляете. Я предлагаю вам написать код, который будет экземпляром этого гипотетического кода, чтобы мы могли его обсудить.   -  person M.M    schedule 01.04.2016
comment
@ Олаф, звучит хорошо, спасибо.   -  person Kyle    schedule 01.04.2016
comment
@ М.М., хорошо, я попытаюсь дополнить свой вопрос таким образом, чтобы продемонстрировать то, что я говорю. Пожалуйста, зайдите через 15 минут или около того.   -  person Kyle    schedule 01.04.2016
comment
@Kyle: обратите внимание, что в 6.7.3.1p3 говорится в какой-то точке последовательности, а не во всех точках последовательности. Можете ли вы найти точку следования перед некоторым выражением, используя значение C, для которого изменение значения A меняет значение C? Если да, то C основано на A.   -  person EOF    schedule 01.04.2016
comment
@EOF, спасибо. Я действительно хотел бы, чтобы весь вопрос не зависел от одного слова. Ух ты. Но это решает проблему для меня. Если вы хотите, чтобы ваш ответ был выбран, скопируйте его в ответ.   -  person Kyle    schedule 01.04.2016
comment
@Kyle: Ах, старый квантор существования против универсального квантора. Не уверен, как превратить это в ответ, который будет полезен кому-либо еще.   -  person EOF    schedule 01.04.2016
comment
Однако даже для выбранной вами точки последовательности вы пишете, что A[0] изменяется с помощью lvalue, не основанного на A. Поскольку оно также читается с помощью lvalue, основанного на A (пункт 10), однако в пункте 10 A имеет значение, на которое вы изменили его на шаге 3, поэтому элемент 10 не считывает тот же объект, который изменяет элемент 4.   -  person M.M    schedule 01.04.2016


Ответы (2)


Предположим, у нас есть функция с вложенными блоками:

void foo()
{
  T *restrict A = getTptr();
  {
    T *restrict B = A;
    {
      #if hypothetical
      A = copyT(A);
      #endif
      useTptr(B + 1);
    }
  }
}

Казалось бы, в точке вызова useTptr(B + 1) гипотетическое изменение на A больше не повлияет на значение B + 1. Однако можно найти другую точку последовательности, так что изменение A действительно влияет на значение B + 1:

void foo()
{
  T *restrict A = getTptr();
  #if hypothetical
  A = copyT(A);
  #endif    
  {
    T *restrict B = A;
    {
      useTptr(B + 1);
    }
  }
}

и проект стандарта C11 n1570 6.7.3.1 Формальное определение ограничения требует только наличия некоторых таких точек следования, а не того, чтобы все точки следования демонстрировали это поведение.

person EOF    schedule 01.04.2016
comment
Я думаю, что многие проблемы с псевдонимами указателей могли бы быть обработаны гораздо более четко, если бы Стандарт описывал, что разрешено делать компиляторам, и говорил, что программа имеет определенное поведение только в том случае, если никакое допустимое преобразование компилятора не приведет к Неопределенное поведение. Скажем, компилятору разрешено делать снимок всего, к чему может получить доступ указатель, в любое время между созданием указателя и его первым использованием, копировать этот снимок обратно в любое время между последним использованием указателя и уничтожением последнего полученного из него указателя. , либо путем перезаписи, либо путем выхода из области действия. - person supercat; 02.04.2016
comment
@supercat: я не нахожу ваше описание ничуть не убедительным. Насколько я могу судить, вы в основном разрешаете буферизацию записи в указатели до тех пор, пока указатель не станет недействительным. Это безумно. Можете ли вы представить, какие условия гонки вы получите? Такое поведение допустимо в многопоточных средах между потоками, потому что вы действительно не можете избежать его, но внутри последовательной согласованности потоков довольно важно, чтобы сделать программирование возможным, не сойдя с ума. - person EOF; 02.04.2016
comment
Мой язык не совсем соответствует поведению restrict, но он служит намеченной цели и должен быть достаточно ясным: при заданном {restrict *p = &foo; .... } операции над объектами, идентифицируемыми p, или указателями, производными от него [но не над указателями, от которых получен p] будут быть упорядоченным после открывающей фигурной скобки и перед закрывающей фигурной скобкой, но не упорядоченным относительно чего-либо между ними [включая указатели, из которых p получено]. Есть много обстоятельств, когда такая семантика просто идеальна, и я думаю, что описание вещей в таких терминах намного яснее, чем в Стандарте. - person supercat; 02.04.2016
comment
Я должен был уточнить, что должно быть различие между двумя вариантами ограничения: одним, когда обратная запись будет упорядочена относительно времени жизни исходного указателя (с ограничением на копии, существующим только до тех пор, пока исходная переменная), и другим что означало бы, что никакие копии указателя не будут записаны после следующего доступа к объекту любым другим способом, и никакая копия указателя не будет доступна после того, как объект был записан любым другим способом. - person supercat; 02.04.2016
comment
В современных конвейерных процессорах оптимизация часто требует изменения порядка выполнения независимых операторов, а это, в свою очередь, требует, чтобы компилятор был проинформирован о том, какие операторы можно считать независимыми. Отсутствие согласованности потоков в случаях, когда программист указывает, что вещи не должны быть упорядочены, не кажется мне тревожным по сравнению с отсутствием согласованности потоков, когда два типа структур имеют общую начальную последовательность, а указатель на один из них используется для доступа к части CIS другого - поведение, которым, кажется, гордятся некоторые авторы компиляторов. - person supercat; 02.04.2016

Я действительно не уверен, что именно ваш вопрос.

Похоже, вы спрашиваете:

В: Боже, будет ли по-прежнему применяться «ограничение», если я нарушу контракт «ограничение»? Как в примере «remove_zeroes()»?

Ответ, конечно, "Нет - не будет".

Вот две ссылки, которые могут прояснить обсуждение. Пожалуйста, обновите свой пост (а) более явными вопросами:

person paulsm4    schedule 01.04.2016
comment
Я имею в виду, что формальное определение не делает различий между точками последовательности и lvalue в вызываемой функции или подблоке и в первичном блоке. Затем это приводит к тому, что обращения к вызываемой функции или подблоку нарушают контракт. Кажется, что формальное определение вызывает непреднамеренные нарушения контракта из-за того, как он определен. - person Kyle; 01.04.2016
comment
Пример remove_zeros() просто указывает на то, что подблоки нельзя обрабатывать по-разному. Я собираюсь удалить этот пример, я думаю, что он скрывает мой вопрос. - person Kyle; 01.04.2016