평가 순서

평가 순서 (Order of evaluation)

C에서 어떤 연산자의 피연산자들이 어느 순서로 평가될지는, 함수 호출에서 인자들의 평가 순서나 한 표현식 속 부분 표현식들의 평가 순서까지 포함해서 명세되지 않아요(specified). 컴파일러는 자기 마음대로 아무 순서나 골라서 평가할 수 있고, 같은 표현식을 다시 평가할 때는 또 다른 순서를 골라도 돼요.

여기서 주의할 게 하나 있어요. C에는 좌에서 우, 우에서 좌로 평가한다는 개념 자체가 없어요. 이걸 연산자의 **결합 방향(associativity)**과 혼동하면 안 돼요. 예를 들어 f1() + f2() + f3()+ 연산자가 좌결합이라 (f1() + f2()) + f3()처럼 괄호가 묶여요. 그런데 실행 시점에 f3() 호출이 가장 먼저 일어날 수도, 가장 나중에 일어날 수도, f1()f2() 사이에 일어날 수도 있어요.

출처: cppreference

본문

정의

평가 (Evaluations)

컴파일러가 각 표현식이나 부분 표현식에 대해 수행하는 평가는 두 종류가 있어요. 둘 다 생략될 수 있다는 점도 기억해 두면 좋아요.

  • 값 계산 (value computation): 표현식이 돌려주는 값을 계산하는 일이에요. 객체의 정체를 알아내는 일(lvalue 평가)이나, 객체에 이전에 저장된 값을 읽는 일(rvalue 평가)이 여기 속해요.
  • 부수 효과 (side effect): volatile lvalue가 가리키는 객체에 접근(읽거나 쓰기), 객체 수정(쓰기), 원자 동기화(C11부터), 파일 수정, 부동소수점 환경 수정(지원한다면), 또는 그런 동작을 하는 함수를 호출하는 일을 말해요.

표현식이 아무 부수 효과도 만들지 않고, 컴파일러가 그 값을 안 쓰는 걸 확실히 알 수 있으면, 그 표현식은 평가되지 않을 수도 있어요.

순서 (Ordering)

sequenced-before(순서 확정)은 같은 스레드 안의 평가들 사이에 존재하는 비대칭·전이·쌍별 관계예요. 원자 타입이나 메모리 배리어가 관여하면 스레드 사이로도 확장될 수 있어요.

  • 부분 표현식 E1E2 사이에 시퀀스 포인트가 있으면, E1의 값 계산과 부수 효과는 E2의 모든 값 계산과 부수 효과보다 sequenced-before 돼요.
  • (C11부터) 평가 A가 평가 B보다 sequenced-before면, B가 시작되기 전에 A의 평가가 끝나요.
  • (C11부터) A가 B보다 sequenced-before가 아니고, B가 A보다 sequenced-before면, A가 시작되기 전에 B의 평가가 끝나요.
  • (C11부터) A가 B보다도 B가 A보다도 sequenced-before가 아니면, 두 가지 가능성이 있어요.
    • A와 B가 비순서(unsequenced): 어떤 순서로도 수행될 수 있고 겹칠 수도 있어요. 단일 실행 스레드 안에서 컴파일러가 A와 B를 구성하는 CPU 명령을 뒤섞을 수도 있어요.
    • A와 B가 불확정 순서(indeterminably-sequenced): 어떤 순서로도 수행될 수 있지만 겹치진 않아요. A가 B보다 먼저 끝나거나, B가 A보다 먼저 끝나요. 같은 표현식을 다음에 또 평가할 때는 순서가 반대가 될 수도 있어요.

규칙

  1. 모든 함수 인자와 함수 지정자(function designator)를 평가한 뒤, 실제 함수 호출 이전에 시퀀스 포인트가 하나 있어요.

  2. 다음 이항 연산자들에서는 첫(왼쪽) 피연산자를 평가한 뒤, 두 번째(오른쪽) 피연산자를 평가하기 전에 시퀀스 포인트가 있어요: &&(논리 AND), ||(논리 OR), ,(쉼표).

  3. 조건 연산자 ?:에서는 첫(왼쪽) 피연산자를 평가한 뒤, 두 번째나 세 번째 피연산자(어느 쪽이 평가되든)를 평가하기 전에 시퀀스 포인트가 있어요.

  4. 완전한 표현식(full expression — 부분 표현식이 아닌 표현식. 보통 세미콜론으로 끝나거나 if/switch/while/do제어문이 되는 것)을 평가한 뒤, 다음 완전한 표현식 이전에 시퀀스 포인트가 있어요.

  5. (C99부터) 완전한 선언자(declarator) 끝에 시퀀스 포인트가 있어요.

  6. (C99부터) 라이브러리 함수가 반환하기 직전에 시퀀스 포인트가 있어요.

  7. (C99부터) 형식화된 입출력에서 각 변환 지정자와 연관된 동작 뒤에 시퀀스 포인트가 있어요. 특히 scanf가 같은 변수의 다른 필드에 값을 쓰거나, printf가 %n으로 같은 변수를 두 번 이상 읽고 수정하는 것은 잘 정의된 동작이에요.

  8. (C99부터) 라이브러리 함수 qsort와 bsearch가 비교 함수를 호출하기 전과 직후 각각 시퀀스 포인트가 있고, 비교 함수 호출과 qsort가 관련 객체를 움직이는 동작 사이에도 시퀀스 포인트가 있어요.

  9. (C11부터) 어떤 연산자의 피연산자들에 대한 값 계산(부수 효과는 아님)은 그 연산자 결과의 값 계산(부수 효과는 아님)보다 먼저 순서가 확정돼요.

  10. (C11부터) 직접 대입 연산자와 모든 복합 대입 연산자의 부수 효과(왼쪽 인자 수정)는 왼쪽·오른쪽 인자 모두의 값 계산(부수 효과는 아님)보다 나중에 순서가 확정돼요.

  11. (C11부터) 후치 증가·후치 감소 연산자의 값 계산은 그 부수 효과보다 앞에 순서가 확정돼요.

  12. (C11부터) 어느 다른 함수 호출보다도 앞에나 뒤에 순서가 확정되지 않은 함수 호출은 불확정 순서로 처리돼요. 서로 다른 함수 호출을 구성하는 CPU 명령은, 함수가 인라인화돼도, 섞일 수 없어요.

  13. (C11부터) 초기화 목록 표현식에서 모든 평가는 불확정 순서예요.

  14. (C11부터) 불확정 순서 함수 호출과 관련해, 복합 대입 연산자의 동작과 증가·감소 연산자의 전치·후치 형태는 모두 단일 평가예요.

정의되지 않은 동작

  1. 스칼라 객체에 대한 부수 효과가 같은 스칼라 객체에 대한 다른 부수 효과와 비순서(unsequenced)면, 동작은 정의되지 않아요.
i = ++i + i++; // undefined behavior
i = i++ + 1; // undefined behavior
f(++i, ++i); // undefined behavior
f(i = -1, i = -1); // undefined behavior
  1. 스칼라 객체에 대한 부수 효과가 같은 스칼라 객체의 값을 사용하는 값 계산과 비순서면, 동작은 정의되지 않아요.
f(i, i++); // undefined behavior
a[i] = i++; // undefined behavior
  1. 위 규칙들은, 부분 표현식들의 허용 가능한 순서 중 하나라도 그런 비순서 부수 효과를 허용하는 한 적용돼요.

연산자 우선순위는 소스 코드 표현으로부터 표현식이 어떻게 구성되는지를 정의해요.

더 알아보기

  • 평가 순서와 함께 값 범주(lvalue, rvalue)가 표현식의 의미를 어떻게 정하는지 보면 좋아요.
  • 연산자 우선순위 문서를 보면 f1() + f2() + f3() 같은 표현식이 어떻게 묶이는지 알 수 있어요.
  • cppreference의 평가 순서 원문에서 C++ 쪽 동작까지 비교해 볼 수 있어요.