c - a[a[0]]=1은 정의되지 않은 동작을 생성합니까?




language-lawyer c99 (4)

이 C99 코드는 정의되지 않은 동작을 생성합니까?

#include <stdio.h>

int main() {
  int a[3] = {0, 0, 0};
  a[a[0]] = 1;
  printf("a[0] = %d\n", a[0]);
  return 0;
}

명령문에서 a[a[0]] = 1; a[0] 읽고 수정합니다.

ISO / IEC 9899의 n1124 초안을 보았습니다 (6.5 표현식).

이전과 다음 시퀀스 포인트 사이에서 객체는 표현식의 평가에 의해 저장된 값을 최대 한 번 수정해야합니다. 더욱이, 이전 값은 저장 될 값을 결정하기 위해서만 판독되어야한다.

수정 될 객체 자체를 결정하기 위해 객체를 읽는 것은 언급되지 않습니다. 따라서이 명령문은 정의되지 않은 동작을 생성 할 수 있습니다.

그러나 나는 이상하다고 느낀다. 실제로 정의되지 않은 동작이 발생합니까?

(다른 ISO C 버전에서도이 문제에 대해 알고 싶습니다.)


이 C99 코드는 정의되지 않은 동작을 생성합니까?

아니요. 정의되지 않은 동작을 생성하지 않습니다. a[0] 은 두 시퀀스 포인트 사이에서 한 번만 수정됩니다 (첫 번째 시퀀스 포인트는 이니셜 라이저의 끝에 있습니다. int a[3] = {0, 0, 0}; 두 번째는 전체 표현식 뒤에 있음 a[a[0]] = 1 ).

수정 될 객체 자체를 결정하기 위해 객체를 읽는 것은 언급되지 않습니다. 따라서이 명령문은 정의되지 않은 동작을 생성 할 수 있습니다.

객체는 자신과 완벽하게 정의 된 동작을 수정하기 위해 두 번 이상 읽을 수 있습니다. 이 예를보십시오

int x = 10;
x = x*x + 2*x + x%5;   

인용문의 두 번째 진술은 다음과 같이 말합니다.

더욱이, 이전 값 은 저장 될 값을 결정하기 위해서만 판독되어야한다.

객체 x 자체의 값을 결정하기 위해 위 식의 모든 x 를 읽습니다.

참고 : 질문에 언급 된 인용문에는 두 부분이 있습니다. 첫 번째 부분은 다음과 같습니다 . 이전과 다음 시퀀스 지점 사이에서 객체는 표현식의 평가에 의해 저장된 값을 최대 한 번 수정해야합니다. ,
따라서 다음과 같은 표현

i = i++;

UB (이전 시퀀스 포인트와 다음 시퀀스 포인트 간의 두 가지 수정)에 해당합니다.

두 번째 부분은 : 또한, 저장 될 값을 결정하기 위해 이전 값은 읽기 전용이어야한다. 따라서 다음과 같은 표현

a[i++] = i;
j = (i = 2) + i;  

UB를 호출하십시오. 두 표현식에서 i 는 이전 및 다음 시퀀스 포인트 사이에서 한 번만 수정되지만 가장 오른쪽의 읽기는 i 저장 될 값을 결정하지 않습니다.

C11 표준에서 이것은 다음으로 변경되었습니다.

6.5 표현 :

스칼라 오브젝트의 부작용이 동일한 스칼라 오브젝트의 다른 부작용 또는 동일한 스칼라 오브젝트의 값을 사용한 값 계산과 관련하여 순서가 지정되지 않으면 동작이 정의되지 않습니다. [...]

a[a[0]] = 1 , a[0]a[0] 부작용은 하나 뿐이며 인덱스 a[0] 의 값 계산은 a[a[0]] 의 값 계산 전에 시퀀싱됩니다.


저장 될 값을 결정하기 위해 이전 값은 읽기 전용이어야한다.

이것은 약간 모호하고 혼동을 일으켰 기 때문에 C11이이를 던져 새로운 시퀀싱 모델을 도입했습니다.

그것이 말하려는 것은 이전 값을 읽는 것이 새로운 값을 쓰는 것보다 더 일찍 발생한다는 것을 보장한다면 괜찮습니다. 그렇지 않으면 UB입니다. 물론 새 값을 작성하기 전에 계산해야합니다.

(물론 내가 방금 쓴 설명은 표준 텍스트보다 더 모호한 것으로 보일 것입니다!)

예를 들어 x = x + 5x + 5 를 먼저 모르면 x x + 5 를 계산할 수 없기 때문에 정확합니다. 그러나 i 에 저장할 새 값을 계산하기 위해 왼쪽에있는 i 를 읽을 필요가 없기 때문에 a[i] = i++ 가 잘못되었습니다. ( i 의 두 번의 읽기는 별도로 고려됩니다).

지금 코드로 돌아가십시오. 어레이 인덱스를 결정하기 a[0] 의 읽기가 쓰기 전에 발생하기 때문에 잘 정의 된 동작이라고 생각합니다.

우리는 어디에 쓸지를 결정할 때까지 쓸 수 없습니다. 그리고 우리는 a[0] 읽은 다음에 쓸 곳을 모른다. 따라서 읽기는 쓰기보다 먼저 이루어져야하므로 UB가 없습니다.

누군가 시퀀스 포인트에 대해 언급했습니다. C99에는이 표현식에 시퀀스 포인트가 없으므로 시퀀스 포인트가이 설명에 포함되지 않습니다.


부작용은 객체 ( 1)의 변형을 포함한다.

C 표준에 따르면 객체에 대한 부작용이 동일한 객체에 대한 부작용이나 동일한 객체에 대한 값을 사용한 값 계산으로 순서가 지정되지 않은 경우 동작이 정의되지 않습니다.

이 표현식에서 객체 a[0] 은 수정되고 (부작용), 그 값 (값 계산)은 인덱스를 결정하는 데 사용됩니다. 이 표현식은 정의되지 않은 동작을 생성하는 것으로 보입니다.

a[a[0]] = 1

그러나 표준에서 대입 연산자의 텍스트는 왼쪽 피연산자가 수정되기 전에 operator = 의 왼쪽 및 오른쪽 피연산자 모두의 값 계산이 시퀀싱된다고 설명합니다.

따라서 동일한 객체의 값 계산 후 수정 (부작용)이 시퀀싱되므로 첫 번째 규칙 1을 위반하지 않기 때문에 동작이 정의됩니다.

1 (ISO / IEC 9899 : 201x 5.1.2.3 프로그램 실행 2에서 인용) :
휘발성 개체에 액세스하거나, 개체를 수정하거나, 파일을 수정하거나, 이러한 작업을 수행하는 함수를 호출하면 실행 환경의 상태가 변경되는 모든 부작용이 발생합니다.

2 (ISO / IEC 9899 : 201x 6.5 식 2에서 인용) :
스칼라 오브젝트의 부작용이 동일한 스칼라 오브젝트의 다른 부작용 또는 동일한 스칼라 오브젝트의 값을 사용한 값 계산과 관련하여 순서가 지정되지 않으면 동작이 정의되지 않습니다.

3 (ISO / IEC 9899 : 201x 6.5.16 할당 연산자 3에서 인용) :
왼쪽 피연산자의 저장된 값을 업데이트하는 부작용은 왼쪽 및 오른쪽 피연산자의 값 계산 후에 시퀀싱됩니다. 피연산자의 평가는 순서가 없습니다.


a[0] 에 유효한 배열 인덱스가 아닌 값이 포함되어 있지 않으면 값이 올바르게 정의됩니다 (예 : 코드에서 음수가 아니고 3 초과하지 않음). 더 읽기 쉽고 동등한 코드로 변경할 수 있습니다

 index = a[0];
 a[index] = 1;    /* still UB if index < 0 || index >= 3 */

a[a[0]] = 1 a[0] 먼저 평가해야합니다. a[0] 이 0이면 a[0] 이 수정됩니다. 그러나 컴파일러가 표준을 준수하지 않는 경우에는 값을 읽기 전에 평가 순서를 변경하고 a[0] 수정할 수 a[0] 방법이 없습니다.





undefined-behavior