C에서 배열 인덱스의 평가 순서 (식과 비교)


47

이 코드를 보면 :

static int global_var = 0;

int update_three(int val)
{
    global_var = val;
    return 3;
}

int main()
{
    int arr[5];
    arr[global_var] = update_three(2);
}

어떤 배열 항목이 업데이트됩니까? 0 또는 2?

이 특정 경우의 작동 우선 순위를 나타내는 C 사양의 일부가 있습니까?


21
이것은 정의되지 않은 행동 냄새가납니다. 확실히 의도적으로 코딩해서는 안되는 것입니다.
Fiddling Bits

1
나는 그것이 나쁜 코딩의 예라고 동의합니다.
Jiminion

4
일부 일화 결과 : godbolt.org/z/hM2Jo2
Bob__

15
이것은 배열 인덱스 또는 작업 순서와 관련이 없습니다. C 스펙이 "시퀀스 포인트"라고 부르는 것과 관련이 있으며, 특히 할당 표현식이 왼손 표현식과 오른손 표현식 사이에 시퀀스 지점을 작성하지 않으므로 컴파일러는 자유롭게 할 수 있습니다. 선택합니다.
이 다니엘 크로커

4
clang이 코드 조각이 경고 IMHO를 트리거하도록 기능 요청을보고해야합니다 .
malat

답변:


51

왼쪽 및 오른쪽 피연산자의 순서

에서 할당을 수행하려면 arr[global_var] = update_three(2)C 구현에서 피연산자를 평가하고 부작용으로 왼쪽 피연산자의 저장된 값을 업데이트해야합니다. C 2018 6.5.16 (할당에 관한 것) 단락 3은 왼쪽과 오른쪽 피연산자에 시퀀싱이 없음을 알려줍니다.

피연산자의 평가는 순서가 없습니다.

이것은 C 구현이 먼저 lvalue를 arr[global_var] 먼저 계산하고 ( lvalue 를 계산 함으로써이 표현이 무엇을 의미하는지 파악하는 것을 의미 함) 평가 update_three(2)하고 마지막으로 후자의 값을 전자에 할당한다는 것을 의미합니다. 또는 update_three(2)먼저 평가 한 다음 lvalue를 계산 한 후 전자를 후자로 할당합니다. 또는 lvalue와 update_three(2)일부 혼합 된 방식 으로 평가 한 다음 올바른 값을 왼쪽 lvalue에 할당합니다.

6.5.16 3도 다음과 같이 말하고 있기 때문에 모든 경우에, lvalue에 대한 값의 할당은 마지막에 와야합니다.

… 왼쪽 피연산자의 저장된 값을 업데이트 할 때의 부작용은 왼쪽 및 오른쪽 피연산자의 값 계산 후에 순서가 정해집니다…

시퀀싱 위반

일부 global_var는 6.5 2를 위반 하여 사용 및 별도로 업데이트 하여 정의되지 않은 동작에 대해 숙고 할 수 있습니다 .

스칼라 객체의 부작용이 동일한 스칼라 객체의 다른 부작용 또는 동일한 스칼라 객체의 값을 사용한 값 계산과 관련하여 순서가 지정되지 않으면 동작이 정의되지 않습니다…

x + x++C 표준에 의해 정의되지 않은 것과 같은 표현식의 동작은 x시퀀싱없이 동일한 표현식에서 값을 사용 하고 개별적으로 수정 하기 때문에 많은 C 실무자에게 매우 친숙합니다 . 그러나이 경우 시퀀싱을 제공하는 함수 호출이 있습니다. 함수 호출에서 global_var사용되고 arr[global_var]업데이 트됩니다 update_three(2).

6.5.2.2 10은 함수가 호출되기 전에 시퀀스 포인트가 있다고 알려줍니다.

함수 지정자 및 실제 인수를 평가 한 후 실제 호출 전에 순서 포인트가 있습니다.

함수 내부, global_var = val;A는 전체 표현 , 그래서이다 3에서 return 3;6.8 4 당 :

전체 표현은 또 다른 표현의 일부도 아니고 선언자 또는 추상 선언자의 일부가 아닌 표현이다 ...

그런 다음이 두 표현식 사이에 다시 6.8 4에 따라 시퀀스 포인트가 있습니다.

… 완전한 표현의 평가와 평가 될 다음의 완전한 표현의 평가 사이에는 순서 점이 있습니다.

따라서 C 구현은 arr[global_var]먼저 평가 한 다음 함수 호출을 수행 할 수 있습니다 .이 경우 함수 호출 이전에 하나가 있기 때문에 그 사이에 시퀀스 포인트가 있거나 함수 호출에서 평가 global_var = val;한 다음에 arr[global_var]있을 수 있습니다. 전체 표현식 뒤에 하나가 있기 때문에 이들 사이의 시퀀스 포인트 따라서 동작은 지정되지 않습니다. 두 가지 중 하나가 먼저 평가 될 수 있지만 정의되지는 않았습니다.


24

여기에 결과가 지정되어 있지 않습니다 .

하위 표현식을 그룹화하는 방법을 나타내는 표현식의 연산 순서는 잘 정의되어 있지만 평가 순서는 지정되지 않았습니다. 이 경우 global_var먼저 읽거나 호출을 update_three먼저 수행 할 수 있지만 어느 것을 알 수있는 방법이 없습니다.

함수 호출시 수정 점을 포함하여 함수의 모든 명령문과 같이 시퀀스 포인트를 도입하기 때문에 여기 에는 정의 되지 않은 동작 이 없습니다global_var .

명확히하기 위해 C 표준 은 섹션 3.4.3에서 정의 되지 않은 동작 을 다음과 같이 정의합니다 .

정의되지 않은 행동

휴대 할 수 없거나 잘못된 프로그램 구성 또는 잘못된 데이터 사용시이 국제 표준이 요구하지 않는 행동

섹션 3.4.4에서 지정되지 않은 동작 을 다음 과 같이 정의합니다 .

불특정 행동

불특정 한 가치의 사용, 또는이 국제 표준이 둘 이상의 가능성을 제공하고 어떠한 경우에도 선택되는 추가 요구 사항을 부과하지 않는 기타 행동

표준에 따르면 함수 인수의 평가 순서가 지정되어 있지 않습니다.이 경우 arr[0]3 arr[2]으로 설정 되거나 3으로 설정됩니다.


“함수 호출시 시퀀스 포인트가 도입되었습니다”는 충분하지 않습니다. 왼쪽 피연산자가 먼저 평가되면 시퀀스 포인트가 함수의 평가에서 왼쪽 피연산자를 분리하므로 충분합니다. 그러나 함수 호출 후 왼쪽 피연산자가 평가되면 함수 호출로 인한 시퀀스 포인트는 함수의 평가와 왼쪽 피연산자의 평가 사이에 있지 않습니다. 전체 표현식을 분리하는 시퀀스 포인트도 필요합니다.
Eric Postpischil

2
@EricPostpischil C11 이전의 용어에는 함수의 시작과 종료에 대한 시퀀스 포인트가 있습니다. C11 용어에서 전체 함수 본문은 호출 컨텍스트와 관련하여 불확실하게 시퀀싱됩니다. 이들은 서로 다른 용어를 사용하여 동일한 것을 지정합니다
MM

이것은 절대적으로 잘못입니다. 과제의 인수에 대한 평가 순서는 지정되어 있지 않습니다. 이 특정 할당의 결과는 신뢰할 수없는 내용으로, 이식 불가능하고 본질적으로 잘못된 (시맨틱 또는 의도 된 결과와 일치하지 않는) 배열을 생성하는 것입니다. 정의되지 않은 행동의 완벽한 사례.
kuroi neko

1
@kuroineko 출력이 다를 수 있다고해서 자동적으로 정의되지 않은 동작을 만들지는 않습니다. 이 표준은 정의되지 않은 동작과 지정되지 않은 동작에 대해 서로 다른 정의를 가지고 있으며,이 상황에서는 후자가됩니다.
dbush

@EricPostpischil 여기에 시퀀스 포인트가 있습니다 (C11 유익한 부록 C에서) : "함수 지정자의 평가와 함수 호출의 실제 인수와 실제 호출 사이 (6.5.2.2)", "전체 표현식의 평가 사이 다음 전체 표현식을 평가합니다 ... /-/ ... 반환 문의 (선택 사항) 표현식 (6.8.6.4) " 그리고 각 세미콜론에서도 완전한 표현이기 때문에.
룬딘

1

시도하고 항목 0을 업데이트했습니다.

그러나이 질문에 따르면 : 표현의 오른쪽항상 먼저 평가됩니다.

평가 순서는 지정되지 않은 순서입니다. 따라서 이와 같은 코드는 피해야한다고 생각합니다.


항목 0에서도 업데이트를 받았습니다.
Jiminion

1
동작은 정의되지 않았지만 지정되지 않았습니다. 당연히 어느 쪽이든 피해야합니다.
Antti Haapala

내가 편집 한 @AnttiHaapala
Mickael B.

1
흠 아 그리고 그것은 순서가 아닌 결정되지 않은 순서대로 ... 대기열에 무작위로 서있는 두 사람이 결정되지 않은 순서로 있습니다. Smith 요원 내부의 Neo는 순서가 없으며 정의되지 않은 동작이 발생합니다.
Antti Haapala

0

할당 할 값을 갖기 전에 할당을위한 코드를 생성하는 것이별로 의미가 없으므로 대부분의 C 컴파일러는 먼저 함수를 호출하고 결과를 어딘가에 저장 (등록, 스택 등)하는 코드를 생성 한 다음 코드를 생성합니다. 이 값을 최종 대상에 기록하므로 전역 변수가 변경된 후 읽습니다. 이것을 표준이 아니라 순수한 논리에 의해 정의 된 "자연 질서"라고하자.

그러나 최적화 과정에서 컴파일러는 값을 어딘가에 임시 저장하는 중간 단계를 제거하고 최종 결과에 가능한 한 직접 함수 결과를 쓰려고 시도합니다.이 경우 종종 색인을 먼저 읽어야합니다 함수 결과를 어레이로 직접 옮길 수 있습니다. 이로 인해 전역 변수가 변경되기 전에 읽힐 수 있습니다.

따라서 이것은 최적화가 수행되는지 여부 와이 최적화가 얼마나 공격적인 지에 따라 결과가 다를 수있는 매우 나쁜 속성의 기본적으로 정의되지 않은 동작입니다. 다음 코드 중 하나를 사용하여 해당 문제를 해결하는 것은 개발자의 임무입니다.

int idx = global_var;
arr[idx] = update_three(2);

또는 코딩 :

int temp = update_three(2);
arr[global_var] = temp;

일반적으로 전역 변수가 존재하지 않는 한 const(또는 그렇지 않으면 코드가 부작용으로 변경되지 않는다는 것을 알지 못한다면) 다중 스레드 환경에서와 같이 코드에서 직접 사용해서는 안됩니다. 이조 차도 정의되지 않을 수 있습니다 :

int result = global_var + (2 * global_var);
// Is not guaranteed to be equal to `3 * global_var`!

컴파일러가 두 번 읽을 수 있고 다른 스레드가 두 읽기 사이의 값을 변경할 수 있기 때문입니다. 그러나 최적화는 코드가 한 번만 읽게하므로 다른 스레드의 타이밍에 따라 다른 결과가 다시 나타날 수도 있습니다. 따라서 사용하기 전에 전역 변수를 임시 스택 변수에 저장하면 두통이 훨씬 줄어 듭니다. 컴파일러가 이것이 안전하다고 생각하면, 심지어 그것을 최적화하고 전역 변수를 직접 사용하므로 결과적으로 성능이나 메모리 사용에 차이가 없을 수 있습니다.

(어떤 누군가가 왜 누군가 x + 2 * x대신 3 * xCPU를 추가 해야하는지 묻기 위해 컴파일러가 비트 시프트 ( 2 * x == x << 1) 로 변환 할 때 2의 거듭 제곱으로 곱셈을 수행하지만 임의의 숫자의 곱셈은 매우 느릴 수 있습니다 따라서 3을 곱하는 대신 x를 1로 비트 시프트하고 x를 결과에 추가하여 훨씬 더 빠른 코드를 얻을 수 있습니다. 심지어 현대적인 목표가 아니라면 3을 곱하고 공격적인 최적화를 설정하면 최신 컴파일러가 그 트릭을 수행합니다. 곱셈이 덧셈과 똑같이 빠른 CPU는 트릭으로 인해 계산 속도가 느려집니다.)


2
정의되지 않은 동작은 아닙니다. 표준은 가능성을 열거하고 그 중 하나를 선택합니다
Antti Haapala

컴파일러는 3 * xx를 두 번 읽지 않습니다 . x를 한 번 읽은 다음 x로 읽는 레지스터에서 x + 2 * x 메소드를 수행 할 수 있습니다.
MM

6
@Mecki "코드를보고 결과를 말할 수 없다면 결과는 정의되지 않습니다" - 정의되지 않은 동작 은 C / C ++에서 매우 특정한 의미를 지닙니다. 다른 응답자들은 왜이 특정 인스턴스가 지정되지 않았지만 정의 되지 않았는지를 설명했습니다 .
marcelm

3
원래 질문의 범위를 벗어나더라도 컴퓨터 내부에 빛을 비추려는 의도에 감사드립니다. 그러나 UB는 매우 정확한 C / C ++ 전문 용어이므로 특히 언어 기술에 대한 질문이있을 때는주의해서 사용해야합니다. 적절한 "지정되지 않은 행동"이라는 용어를 대신 사용하여 답을 크게 향상시킬 수 있습니다.
kuroi neko

2
@Mecki는 " 정의되지 않은 영어 언어의 아주 특별한 의미가있다 "...하지만 레이블이 질문에 language-lawyer문제의 언어가있다, 자신 은 "매우 특별한 의미" 정의되지 않은를 , 당신은 단지 사용하지 않음으로써 원인 혼란을거야 언어의 정의.
TripeHound

-1

전역 편집 : 죄송합니다, 나는 모두 해고되었고 많은 넌센스를 썼습니다. 낡은 괴짜 란트.

나는 C가 절약되었다고 생각하고 싶었지만 C11 이래로 C ++과 동등한 수준이되었습니다. 분명히, 컴파일러가 표현식에서 부작용으로 무엇을하는지 알기 위해서는 이제 "동기화 지점 앞에 위치"에 기반한 코드 시퀀스의 부분 순서와 관련된 약간의 수학 수수께끼를 풀어야합니다.

K & R 시대에 몇 가지 중요한 실시간 임베디드 시스템을 설계하고 구현했습니다. (엔진을 점검하지 않은 경우 가장 가까운 벽에 충돌하는 사람들을 보낼 수있는 전기 자동차 컨트롤러 포함) 제대로 명령하지 않으면 사람들을 펄프에 박힐 수있는 로봇, 그리고 무해하지만 수십 개의 프로세서가 1 % 미만의 시스템 오버 헤드로 데이터 버스를 빨아 들일 수있는 시스템 계층).

정의되지 않은 것과 지정되지 않은 것 사이의 차이를 얻기에는 너무 멍청하거나 어리석지 만 동시 실행과 데이터 액세스가 무엇을 의미하는지 여전히 잘 알고 있다고 생각합니다. 필자의 의견으로는, C ++에 대한 이러한 집착과 이제는 동기화 문제를 극복하는 애완 동물 언어를 가진 C 녀석은 값 비싼 꿈입니다. 동시 실행이 무엇인지 알고 있고, 이러한 기즈모가 필요하지 않거나 필요하지 않으며, 세계를 크게 엉망으로 만들지 않을 것입니다.

이러한 눈물을 흘리는 메모리 장벽 추상화의 모든 트럭로드는 단순히 다중 CPU 캐시 시스템의 일시적인 한계로 인해 발생합니다. 이는 모두 뮤텍스 및 조건 변수 C ++와 같은 일반적인 OS 동기화 객체에 안전하게 캡슐화 될 수 있습니다 제공합니다.
이 캡슐화의 비용은 미세한 특정 CPU 명령어를 사용하여 얻을 수있는 것과 비교하여 성능이 약간 저하되는 경우가 있습니다. 키워드 (또는
volatile#pragma dont-mess-with-that-variable시스템 프로그래머로서, 케어)는 메모리 액세스 순서를 바꾸지 말라고 컴파일러에게 지시하기에 충분했을 것이다. 직접 asm 지시문으로 최적의 코드를 쉽게 생성하여 임시 CPU 특정 명령으로 저수준 드라이버 및 OS 코드를 뿌릴 수 있습니다. 기본 하드웨어 (캐시 시스템 또는 버스 인터페이스)의 작동 방식에 대한 친밀한 지식 없이는 쓸모없고 비효율적이거나 결함이있는 코드를 작성해야합니다.

volatile키워드와 Bob 의 미세한 조정은 모든 사람을 제외한 대부분의 저급 프로그래머의 삼촌 일 것입니다. 그 대신에, 일반적인 C ++ 수학 괴물 집단은 아직 이해하기 어려운 또 다른 추상화를 설계하는 현장 하루를 보냈으며, 존재하지 않는 문제를 찾는 솔루션을 설계하고 컴파일러의 사양으로 프로그래밍 언어의 정의를 착각하는 전형적인 경향을 가져 왔습니다.

이번에는 C의 기본 측면을 손상시키는 데 필요한 변경 사항이 있었는데, 이러한 "장벽"이 제대로 작동하려면 낮은 수준의 C 코드에서도 생성되어야했기 때문입니다. 그것은 무엇보다도 설명이나 정당화없이 표현의 정의에 혼란을 가져왔다.

결론적으로 컴파일러가이 터무니없는 C 조각에서 일관된 기계 코드를 생성 할 수 있다는 사실은 C ++ 사용자가 2000 년대 후반의 캐시 시스템의 잠재적 불일치에 대처하는 방식의 먼 결과 일뿐입니다.
C (표현 정의)의 한 가지 기본적인 측면을 엉망으로 만들었습니다. 따라서 캐시 시스템에 대해 망설이지 않고 정당하게도 많은 C 프로그래머가 전문가를 설명해야합니다. 차이 a = b() + c()a = b + c.

이 불행한 배열이 어떻게 될지 추측하는 것은 어쨌든 시간과 노력의 손실입니다. 컴파일러가 무엇을 만들 것인지에 관계 없이이 코드는 병적으로 잘못되었습니다. 그것과 관련된 유일한 책임은 그것을 쓰레기통에 보내는 것입니다.
개념적으로, 부작용은 평가 전이나 후에 수정이 별도의 진술로 명시 적으로 이루어 지도록하려는 사소한 노력으로 항상 표현에서 벗어날 수 있습니다.
이런 종류의 혼란스러운 코드는 컴파일러가 아무것도 최적화하지 못했을 때 80 년대에 정당화되었을 수 있습니다. 그러나 이제 컴파일러는 대부분의 프로그래머보다 더 영리 해졌습니다. 남아있는 것은 모두 엉뚱한 코드입니다.

나는 또한이 정의되지 않은 / 지정되지 않은 토론의 중요성을 이해하지 못한다. 일관된 동작으로 코드를 생성하기 위해 컴파일러에 의존하거나 그렇지 않을 수 있습니다. 당신이 그것을 정의되지 않은지 또는 불특정이라고 부르는지 여부는 논란의 여지가 있습니다.

내가 알기로는 C는 이미 K & R 상태에서 충분히 위험하다. 유용한 진화는 상식 안전 조치를 추가하는 것입니다. 예를 들어,이 고급 코드 분석 도구를 사용하면 스펙은 잠재적으로 극단적으로 신뢰할 수없는 코드를 자동으로 생성하는 대신 컴파일러가 최소한 bonkers 코드에 대한 경고를 생성하도록 구현해야합니다.
그러나 대신에 사람들은 C ++ 17에서 고정 평가 순서를 정의하기로 결정했습니다. 이제 새로운 소프트웨어가 난독 화를 결정적인 방식으로 간절히 처리 할 것이라는 확신을 바탕으로 의도적으로 코드에 부작용을두기 위해 모든 소프트웨어가 비효율적입니다.

K & R은 컴퓨팅 세계에서 놀라운 일 중 하나였습니다. 20 달러에 대해 당신은 언어의 포괄적 인 사양을 얻었습니다 (단독의 개인 이이 책을 사용하여 완전한 컴파일러를 작성하는 것을 보았습니다), 훌륭한 참조 매뉴얼 (목차는 일반적으로 질문)과 합리적으로 언어를 사용하도록 가르치는 교과서입니다. 언어를 남용하여 매우 어리석은 일을 할 수있는 수많은 방법에 대한 이론적 근거, 예 및 현명한 경고문으로 완성하십시오.

작은 이익을 얻기 위해 그 유산을 파괴하는 것은 나에게 잔인한 낭비처럼 보입니다. 그러나 다시 한 번 요점을 완전히 보지 못할 수도 있습니다. 어쩌면 어떤 종류의 영혼이 이러한 부작용을 크게 활용하는 새로운 C 코드의 예를 지시 할 수 있습니까?


동일한 표현식 C17 6.5 / 2에서 동일한 객체에 부작용이있는 경우 정의되지 않은 동작입니다. 이들은 C17 6.5.18 / 3에 따라 순서가 없습니다. 그러나 6.5 / 2의 텍스트 "스칼라 객체의 부작용이 동일한 스칼라 객체의 다른 부작용이나 동일한 스칼라 객체의 값을 사용한 값 계산에 비해 순서가 맞지 않으면 동작이 정의되지 않습니다." 함수 내부의 값 계산은 할당되지 않은 피연산자가있는 할당 연산자에 관계없이 배열 인덱스 액세스 이전 또는 이후에 시퀀싱되므로 적용되지 않습니다.
룬딘

함수 호출은 "시퀀스없는 액세스에 대한 뮤텍스"와 같은 역할을합니다. 쉼표 연산자와 같은 모호한 0,expr,0.
룬딘

"정의되지 않은 행동은 구현 자에게 진단하기 어려운 특정 프로그램 오류를 포착하지 못하도록 라이센스를 부여합니다. 또한 가능한 언어 확장이 가능한 영역을 식별합니다. 구현자는 공식적으로 정의되지 않은 행동의 정의 " 표준은 엄격하게 준수하지 않는 유용한 프로그램을 무시해서는 안된다고 말했다. 필자는 표준 작성자 대부분이 양질의 컴파일러를 작성하려는 사람들이 분명하다고 생각했을 것이라고 생각합니다.
supercat

... UB를 고객에게 가능한 한 유용하게 만들 수있는 기회로 UB를 사용해야합니다. "저는 표준이 그것을 유용하게 처리 할 필요가 없기 때문에 컴파일러 작성자가이 코드를 다른 사람보다 덜 유용하게 처리합니다." 표준에 의해 요구되는 행동을하지 않는 프로그램을 유용하게 처리하는 것은 단지 깨진 프로그램의 작성을 촉진 할뿐입니다. "
supercat

나는 당신의 말에서 요점을 보지 못합니다. 컴파일러 특정 동작에 의존하는 것은 이식성이 보장되지 않습니다. 또한이 "추가 정의"를 언제든지 중단 할 수있는 컴파일러 제조업체에 대한 신의가 필요합니다. 컴파일러가 할 수있는 유일한 일은 경고를 생성하는 것입니다. 현명하고 지식이 풍부한 프로그래머가 같은 오류를 처리하기로 결정할 수 있습니다. 이 ISO 몬스터에서 볼 수있는 문제는 OP의 예와 같은 끔찍한 코드를 만드는 것입니다 (표현에 대한 K & R 정의와 비교할 때 매우 명확하지 않은 이유로).
kuroi neko
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.