평가 순서

평가 순서 (Order of Evaluation)

어떤 표현식의 어느 부분이 언제 평가되는지에 대한 규칙이에요. 특히 함수 인자(argument)의 평가 순서가 C++에서는 **미지정(unspecified)**이라는 게 핵심이에요. "좌에서 우"라는 익숙한 개념이 여기엔 없어요.

출처: cppreference

본문

어떤 표현식의 어느 부분이든 평가 순서, 함수 인자의 평가 순서를 포함해 미지정이에요 (아래 나열한 몇 가지 예외 제외). 컴파일러는 피연산자와 다른 하위 표현식을 어떤 순서로든 평가할 수 있고, 같은 표현식을 다시 평가할 때는 다른 순서를 선택할 수도 있어요.

C++에는 좌에서 우 또는 우에서 좌 평가라는 개념이 없어요. 이걸 연산자의 좌결합성/우결합성과 혼동하면 안 돼요. a() + b() + c()는 operator+의 좌결합성 때문에 (a() + b()) + c()로 파싱되지만, 런타임에는 c()가 먼저, 마지막, 또는 a()b() 사이에 평가될 수 있어요:

#include <cstdio>

int a() { return std::puts("a"); }
int b() { return std::puts("b"); }
int c() { return std::puts("c"); }

void z(int, int, int) {}

int main()
{
    z(a(), b(), c());       // all 6 permutations of output are allowed
    return a() + b() + c(); // all 6 permutations of output are allowed
}

가능한 출력:

b
c
a
c
a 
b

"Sequenced before" 규칙 (C++11 이후)

표현식의 평가

각 표현식의 평가는 다음을 포함해요:

  • 값 계산(Value computations): 표현식이 반환하는 값을 계산. 여기에는 객체의 정체성 판별(glvalue 평가, 예를 들어 표현식이 어떤 객체에 대한 참조를 반환하는 경우)이나, 객체에 이전에 할당된 값 읽기(prvalue 평가, 예를 들어 표현식이 숫자나 다른 값을 반환하는 경우)가 포함될 수 있어요.
  • 부수 효과의 시작(Initiation of side effects): volatile glvalue가 가리키는 객체에 대한 접근(읽기/쓰기), 객체에 대한 수정(쓰기), 라이브러리 I/O 함수 호출, 또는 그런 연산 중 하나라도 수행하는 함수 호출.

순서 (Ordering)

Sequenced before는 같은 스레드 안에서 평가 A와 B 사이의 비대칭적이고 추이적인 쌍별 관계예요.

  • A가 B 앞에 sequenced before라면(동등하게 B는 A 뒤에 sequenced after), A의 평가가 완료된 후에 B의 평가가 시작돼요.
  • A가 B 앞에 sequenced before가 아니고 B가 A 앞에 sequenced before라면, B의 평가가 완료된 후에 A의 평가가 시작돼요.
  • A가 B 앞에 sequenced before가 아니고 B도 A 앞에 sequenced before가 아니라면, 두 가지 가능성이 있어요.
    • A와 B의 평가는 비순서화(unsequenced): 어떤 순서로든 수행될 수 있고 겹칠 수 있어요 (단일 실행 스레드 안에서 컴파일러가 A와 B를 이루는 CPU 명령을 섞을 수 있어요).
    • A와 B의 평가는 불확정적으로 순서화(indeterminately sequenced): 어떤 순서로든 수행될 수 있지만 겹치지는 않아요 — A가 B보다 먼저 완료되거나, B가 A보다 먼저 완료되거나. 같은 표현식을 다음에 평가할 때는 그 반대 순서가 될 수 있어요.

따로 언급하지 않는 한, 개별 연산자의 피연산자와 개별 표현식의 하위 표현식 평가는 비순서화돼요.

(C++26 이후) 표현식을 **핵심 상수 표현식(core constant expression)**으로 평가하는 동안에는, 그렇지 않으면 비순서화되거나 불확정적으로 순서화되는 하위 피연산자와 하위 표현식이 **어휘 순서(lexical order)**로 평가돼요.

규칙

  1. 각 **전체 표현식(full-expression)**은 다음 전체 표현식보다 앞에 sequenced before예요.
  2. 어떤 연산자의 피연산자들의 값 계산(부수 효과는 아님)은 그 연산자 결과의 값 계산보다 앞에 sequenced before예요 (부수 효과는 아님).
  3. 함수 func를 호출할 때(인라인 여부와 무관, 명시적 함수 호출 문법 사용 여부와 무관), 다음 목록의 각 항목은 다음 항목보다 앞에 sequenced before예요.
    • 모든 인자 표현식과 func를 가리키는 후위 표현식
    • (C++26 이후) func의 모든 전제 조건 단언(precondition assertion)
    • func 본문의 모든 표현식 또는 문장
    • (C++26 이후) func의 모든 후조건 단언(postcondition assertion)
  4. 내장 후위 증가/감소 연산자의 값 계산은 그 부수 효과보다 앞에 sequenced before예요.
  5. 내장 전위 증가/감소 연산자의 부수 효과는 그 값 계산보다 앞에 sequenced before예요 (복합 할당으로서의 정의에서 오는 암묵적 규칙).
  6. 내장 논리 AND 연산자 &&, 논리 OR 연산자 ||, 그리고 내장 콤마 연산자 ,첫(왼쪽) 피연산자는 두 번째(오른쪽) 피연산자보다 앞에 sequenced before예요.
  7. 조건 연산자 ?:첫 번째 피연산자는 두 번째 또는 세 번째 피연산자보다 앞에 sequenced before예요.
  8. 내장 할당 연산자와 모든 내장 복합 할당 연산자의 부수 효과(왼쪽 피연산자의 수정)는 왼쪽과 오른쪽 피연산자 모두의 값 계산 이후에 sequenced after되고, 할당 표현식의 값 계산보다 앞에 sequenced before예요 (즉, 수정된 객체에 대한 참조를 반환하기 전에).
  9. 리스트 초기화에서, 주어진 초기화 절의 모든 값 계산과 부수 효과는 그 뒤의 초기화 절과 연관된 모든 값 계산과 부수 효과보다 앞에 sequenced before예요.
  10. 다른 표현식 평가(다른 함수 호출일 수 있음)에 대해 sequenced before도 sequenced after도 아닌 함수 호출은, 그 평가에 대해 불확정적으로 순서화돼요 (프로그램은 함수 호출을 구성하는 CPU 명령이 다른 표현식의 평가(다른 함수 호출 포함)를 구성하는 명령과 섞이지 않은 것처럼 동작해야 해요 — 함수가 인라인되었어도).

(C++17 이후) 규칙 10에는 예외가 하나 있어요. std::execution::par_unseq 실행 정책 하에서 실행되는 표준 라이브러리 알고리즘에 의한 함수 호출은 비순서화되고 서로 임의로 섞일 수 있어요.

  1. 할당 함수(operator new)에 대한 호출new 표현식에서 생성자 인자의 평가에 대해 불확정적으로 순서화(C++17 이전) / sequenced before(C++17 이후)예요.
  2. 함수에서 돌아올 때, 함수 호출 결과인 임시 객체의 복사 초기화return 문의 피연산자 끝에서 모든 임시 객체의 소멸보다 앞에 sequenced before인데, 이 소멸은 return 문을 포함하는 블록의 지역 변수 소멸보다 앞에 sequenced before예요.

(C++17 이후) 13) 함수 호출 표현식에서, 함수를 이름 짓는 표현식은 모든 인자 표현식과 모든 기본 인자보다 앞에 sequenced before예요. 14) 함수 호출에서, 각 매개변수의 초기화의 값 계산과 부수 효과는 다른 매개변수의 값 계산과 부수 효과에 대해 불확정적으로 순서화돼요. 15) 모든 오버로드된 연산자는 연산자 표기법으로 호출할 때 오버로드하는 내장 연산자의 순서 규칙을 따라요. 16) 첨자 표현식 E1[E2]에서 E1E2보다 앞에 sequenced before예요. 17) 포인터-멤버 표현식 E1.*E2 또는 E1->*E2에서 E1E2보다 앞에 sequenced before예요. 18) 시프트 연산자 표현식 E1 << E2E1 >> E2에서 E1E2보다 앞에 sequenced before예요. 19) 모든 단순 할당 표현식 E1 = E2와 모든 복합 할당 표현식 E1 @= E2에서 E2E1보다 앞에 sequenced before예요. 20) 비-애그리게이트 클래스의 직접 초기화에서, (괄호로 싸인) 초기화자의 모든 표현식은 함수 호출처럼 평가돼요 (불확정적으로 순서화). (C++17 이후)

(C++20 이후) 21) 애그리게이트의 직접 초기화에서, 각 애그리게이트 요소의 초기화는 다음 요소의 초기화보다 앞에 sequenced before예요.

정의되지 않은 동작

다음 경우의 동작은 정의되지 않아요:

  1. 메모리 위치에 대한 부수 효과가 같은 메모리 위치에 대한 다른 부수 효과와 비순서화된 경우:
i = ++i + 2;       // well-defined
i = i++ + 2;       // undefined behavior until C++17
f(i = -2, i = -2); // undefined behavior until C++17
f(++i, ++i);       // undefined behavior until C++17, unspecified after C++17
i = ++i + i++;     // undefined behavior
  1. 메모리 위치에 대한 부수 효과가 같은 메모리 위치의 어떤 객체의 값을 사용하는 값 계산과 비순서화된 경우:
cout << i << i++; // undefined behavior until C++17
a[i] = i++;       // undefined behavior until C++17
n = ++i + i;      // undefined behavior
  1. 메모리 위치에서 객체의 수명을 시작/종료하는 것이 다음 연산과 비순서화된 경우:
    • 같은 메모리 위치에 대한 부수 효과
    • 같은 메모리 위치의 어떤 객체의 값을 사용하는 값 계산
    • 그 메모리 위치와 겹치는 저장 공간을 차지하는 객체의 수명 시작/종료
union U { int x, y; } u;
(u.x = 1, 0) + (u.y = 2, 0); // undefined behavior

시퀀스 포인트 규칙 (C++11 이전)

Pre-C++11 정의

표현식의 평가는 부수 효과를 만들 수 있는데, 부수 효과란 volatile lvalue가 가리키는 객체 접근, 객체 수정, 라이브러리 I/O 함수 호출, 또는 그런 연산 중 하나라도 수행하는 함수 호출을 말해요.

**시퀀스 포인트(sequence point)**란, 실행 시퀀스에서 이전 평가들의 모든 부수 효과가 완료되고, 이후 평가의 부수 효과가 시작되지 않은 지점을 말해요.

Pre-C++11 규칙

  1. 각 전체 표현식의 끝(보통 세미콜론)에 시퀀스 포인트가 있어요.
  2. 함수를 호출할 때(인라인 여부와 함수 호출 문법 사용 여부와 무관), 모든 함수 인자(있을 경우)의 평가 후, 함수 본문의 어떤 표현식/문장 실행 전에 시퀀스 포인트가 있어요.
  3. 함수에서 돌아올 때, 함수 호출 결과의 복사 초기화 후, return 문 끝의 모든 임시 객체 소멸 전에 시퀀스 포인트가 있어요.
  4. 함수의 반환 값 복사 후, 함수 밖의 어떤 표현식 실행 전에 시퀀스 포인트가 있어요.
  5. 함수 실행이 시작되면, 호출된 함수 실행이 완료될 때까지 호출 함수의 어떤 표현식도 평가되지 않아요 (함수는 서로 섞일 수 없어요).
  6. 다음 네 가지 표현식을 내장(오버로드되지 않은) 연산자로 평가할 때, 표현식 a의 평가 후에 시퀀스 포인트가 있어요.
a && b
a || b
a ? b : c
a , b

Pre-C++11 정의되지 않은 동작

다음 경우의 동작은 정의되지 않아요:

  1. 이전과 다음 시퀀스 포인트 사이에, 어떤 메모리 위치의 객체 값이 표현식의 평가에 의해 두 번 이상 수정된 경우:
i = ++i + i++;     // undefined behavior
i = i++ + 1;       // undefined behavior
i = ++i + 1;       // undefined behavior
++ ++i;            // undefined behavior
f(++i, ++i);       // undefined behavior
f(i = -1, i = -1); // undefined behavior
  1. 이전과 다음 시퀀스 포인트 사이에, 표현식의 평가에 의해 값이 수정된 객체에 대해, 저장할 값을 결정하는 것 외의 방식으로 그 이전 값을 접근한 경우:
cout << i << i++; // undefined behavior
a[i] = i++;       // undefined behavior

결함 보고 (Defect reports)

다음의 동작 변경 결함 보고는 이전에 발표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 1885 C++11 함수 반환 시 자동 변수 소멸의 순서가 명시되지 않았음 순서 규칙 추가
CWG 1949 C++11 C++ 표준에서 "sequenced after"가 쓰였으나 정의되지 않았음 "sequenced before"의 역으로 정의됨
CWG 1953 C++11 메모리 위치와 관련된 부수 효과와 값 계산이 같은 메모리 위치에서 객체 수명 시작/종료와 비순서화될 수 있었음 이 경우 동작은 정의되지 않음
CWG 2146 C++98 정의되지 않은 동작을 다루는 경우가 비트 필드를 고려하지 않았음 고려됨

참고 문헌

  • C++23 표준 (ISO/IEC 14882:2024): 6.9.1 Program execution [intro.execution], 7.6.1.6 Increment and decrement [expr.post.incr], 7.6.2.8 New [expr.new], 7.6.14 Logical AND operator [expr.log.and], 7.6.15 Logical OR operator [expr.log.or], 7.6.16 Conditional operator [expr.cond], 7.6.19 Assignment and compound assignment operators [expr.ass], 7.6.20 Comma operator [expr.comma], 9.4.5 List-initialization [dcl.init.list]
  • C++20 표준 (ISO/IEC 14882:2020): 6.9.1 Program execution [intro.execution], 7.6.1.5 Increment and decrement [expr.post.incr], 7.6.2.7 New [expr.new], 7.6.14 Logical AND operator [expr.log.and], 7.6.15 Logical OR operator [expr.log.or], 7.6.16 Conditional operator [expr.cond], 7.6.19 Assignment and compound assignment operators [expr.ass], 7.6.20 Comma operator [expr.comma], 9.4.4 List-initialization [dcl.init.list]
  • C++17 표준 (ISO/IEC 14882:2017): 4.6 Program execution [intro.execution], 8.2.6 Increment and decrement [expr.post.incr], 8.3.4 New [expr.new], 8.14 Logical AND operator [expr.log.and], 8.15 Logical OR operator [expr.log.or], 8.16 Conditional operator [expr.cond], 8.18 Assignment and compound assignment operators [expr.ass], 8.19 Comma operator [expr.comma], 11.6.4 List-initialization [dcl.init.list]
  • C++14 표준 (ISO/IEC 14882:2014): 1.9 Program execution [intro.execution], 5.2.6 Increment and decrement [expr.post.incr], 5.3.4 New [expr.new], 5.14 Logical AND operator [expr.log.and], 5.15 Logical OR operator [expr.log.or], 5.16 Conditional operator [expr.cond], 5.17 Assignment and compound assignment operators [expr.ass], 5.18 Comma operator [expr.comma], 8.5.4 List-initialization [dcl.init.list]
  • C++11 표준 (ISO/IEC 14882:2011): 1.9 Program execution [intro.execution], 5.2.6 Increment and decrement [expr.post.incr], 5.3.4 New [expr.new], 5.14 Logical AND operator [expr.log.and], 5.15 Logical OR operator [expr.log.or], 5.16 Conditional operator [expr.cond], 5.17 Assignment and compound assignment operators [expr.ass], 5.18 Comma operator [expr.comma], 8.5.4 List-initialization [dcl.init.list]

더 알아보기 (Learn more)

  • 연산자 우선순위 — 표현식이 소스 코드 표현에서 어떻게 만들어지는지 정의
  • 표현식 — C++ 표현식의 일반적인 성질