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

이 코드를 보면 :

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 사양의 일부가 있습니까?



답변

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

에서 할당을 수행하려면 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]있을 수 있습니다. 전체 표현식 뒤에 하나가 있기 때문에 이들 사이의 시퀀스 포인트 따라서 동작은 지정되지 않습니다. 두 가지 중 하나가 먼저 평가 될 수 있지만 정의되지는 않았습니다.


답변

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

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

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

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

정의되지 않은 행동

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

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

불특정 행동

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

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


답변

시도하고 항목 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는 트릭으로 인해 계산 속도가 느려집니다.)


답변

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

나는 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 코드의 예를 지시 할 수 있습니까?


답변