평가 순서
평가 순서 (Order of evaluation)
C에서 어떤 연산자의 피연산자들이 어느 순서로 평가될지는, 함수 호출에서 인자들의 평가 순서나 한 표현식 속 부분 표현식들의 평가 순서까지 포함해서 명세되지 않아요(specified). 컴파일러는 자기 마음대로 아무 순서나 골라서 평가할 수 있고, 같은 표현식을 다시 평가할 때는 또 다른 순서를 골라도 돼요.
여기서 주의할 게 하나 있어요. C에는 좌에서 우, 우에서 좌로 평가한다는 개념 자체가 없어요. 이걸 연산자의 **결합 방향(associativity)**과 혼동하면 안 돼요. 예를 들어 f1() + f2() + f3()는 + 연산자가 좌결합이라 (f1() + f2()) + f3()처럼 괄호가 묶여요. 그런데 실행 시점에 f3() 호출이 가장 먼저 일어날 수도, 가장 나중에 일어날 수도, f1()과 f2() 사이에 일어날 수도 있어요.
출처: cppreference
본문
정의
평가 (Evaluations)
컴파일러가 각 표현식이나 부분 표현식에 대해 수행하는 평가는 두 종류가 있어요. 둘 다 생략될 수 있다는 점도 기억해 두면 좋아요.
- 값 계산 (value computation): 표현식이 돌려주는 값을 계산하는 일이에요. 객체의 정체를 알아내는 일(lvalue 평가)이나, 객체에 이전에 저장된 값을 읽는 일(rvalue 평가)이 여기 속해요.
- 부수 효과 (side effect):
volatilelvalue가 가리키는 객체에 접근(읽거나 쓰기), 객체 수정(쓰기), 원자 동기화(C11부터), 파일 수정, 부동소수점 환경 수정(지원한다면), 또는 그런 동작을 하는 함수를 호출하는 일을 말해요.
표현식이 아무 부수 효과도 만들지 않고, 컴파일러가 그 값을 안 쓰는 걸 확실히 알 수 있으면, 그 표현식은 평가되지 않을 수도 있어요.
순서 (Ordering)
sequenced-before(순서 확정)은 같은 스레드 안의 평가들 사이에 존재하는 비대칭·전이·쌍별 관계예요. 원자 타입이나 메모리 배리어가 관여하면 스레드 사이로도 확장될 수 있어요.
- 부분 표현식
E1과E2사이에 시퀀스 포인트가 있으면,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보다 먼저 끝나요. 같은 표현식을 다음에 또 평가할 때는 순서가 반대가 될 수도 있어요.
규칙
-
모든 함수 인자와 함수 지정자(function designator)를 평가한 뒤, 실제 함수 호출 이전에 시퀀스 포인트가 하나 있어요.
-
다음 이항 연산자들에서는 첫(왼쪽) 피연산자를 평가한 뒤, 두 번째(오른쪽) 피연산자를 평가하기 전에 시퀀스 포인트가 있어요:
&&(논리 AND),||(논리 OR),,(쉼표). -
조건 연산자
?:에서는 첫(왼쪽) 피연산자를 평가한 뒤, 두 번째나 세 번째 피연산자(어느 쪽이 평가되든)를 평가하기 전에 시퀀스 포인트가 있어요. -
완전한 표현식(full expression — 부분 표현식이 아닌 표현식. 보통 세미콜론으로 끝나거나
if/switch/while/do의 제어문이 되는 것)을 평가한 뒤, 다음 완전한 표현식 이전에 시퀀스 포인트가 있어요. -
(C99부터) 완전한 선언자(declarator) 끝에 시퀀스 포인트가 있어요.
-
(C99부터) 라이브러리 함수가 반환하기 직전에 시퀀스 포인트가 있어요.
-
(C99부터) 형식화된 입출력에서 각 변환 지정자와 연관된 동작 뒤에 시퀀스 포인트가 있어요. 특히 scanf가 같은 변수의 다른 필드에 값을 쓰거나, printf가
%n으로 같은 변수를 두 번 이상 읽고 수정하는 것은 잘 정의된 동작이에요. -
(C99부터) 라이브러리 함수 qsort와 bsearch가 비교 함수를 호출하기 전과 직후 각각 시퀀스 포인트가 있고, 비교 함수 호출과 qsort가 관련 객체를 움직이는 동작 사이에도 시퀀스 포인트가 있어요.
-
(C11부터) 어떤 연산자의 피연산자들에 대한 값 계산(부수 효과는 아님)은 그 연산자 결과의 값 계산(부수 효과는 아님)보다 먼저 순서가 확정돼요.
-
(C11부터) 직접 대입 연산자와 모든 복합 대입 연산자의 부수 효과(왼쪽 인자 수정)는 왼쪽·오른쪽 인자 모두의 값 계산(부수 효과는 아님)보다 나중에 순서가 확정돼요.
-
(C11부터) 후치 증가·후치 감소 연산자의 값 계산은 그 부수 효과보다 앞에 순서가 확정돼요.
-
(C11부터) 어느 다른 함수 호출보다도 앞에나 뒤에 순서가 확정되지 않은 함수 호출은 불확정 순서로 처리돼요. 서로 다른 함수 호출을 구성하는 CPU 명령은, 함수가 인라인화돼도, 섞일 수 없어요.
-
(C11부터) 초기화 목록 표현식에서 모든 평가는 불확정 순서예요.
-
(C11부터) 불확정 순서 함수 호출과 관련해, 복합 대입 연산자의 동작과 증가·감소 연산자의 전치·후치 형태는 모두 단일 평가예요.
정의되지 않은 동작
- 스칼라 객체에 대한 부수 효과가 같은 스칼라 객체에 대한 다른 부수 효과와 비순서(unsequenced)면, 동작은 정의되지 않아요.
i = ++i + i++; // undefined behavior
i = i++ + 1; // undefined behavior
f(++i, ++i); // undefined behavior
f(i = -1, i = -1); // undefined behavior
- 스칼라 객체에 대한 부수 효과가 같은 스칼라 객체의 값을 사용하는 값 계산과 비순서면, 동작은 정의되지 않아요.
f(i, i++); // undefined behavior
a[i] = i++; // undefined behavior
- 위 규칙들은, 부분 표현식들의 허용 가능한 순서 중 하나라도 그런 비순서 부수 효과를 허용하는 한 적용돼요.
연산자 우선순위는 소스 코드 표현으로부터 표현식이 어떻게 구성되는지를 정의해요.
더 알아보기
- 평가 순서와 함께 값 범주(lvalue, rvalue)가 표현식의 의미를 어떻게 정하는지 보면 좋아요.
- 연산자 우선순위 문서를 보면
f1() + f2() + f3()같은 표현식이 어떻게 묶이는지 알 수 있어요. - cppreference의 평가 순서 원문에서 C++ 쪽 동작까지 비교해 볼 수 있어요.