C11에서 f(i++, i++)의 인자 평가 순서를 왼쪽부터 또는 오른쪽부터라고 확정할 수 없습니다. 더 중요한 점은 같은 i를 두 번 수정하는 부작용들이 서로 순서 없이 평가되므로 이 식의 동작이 undefined behavior라는 사실입니다.

undefined behavior는 여러 후보 출력 중 하나를 골라 예측하라는 뜻이 아닙니다. 출력값을 계산하지 말고 식이 표준의 요구 조건을 벗어났다고 판정합니다. C11의 여기서 쓰는 근거는 2011 WG14 committee draft N1570이며 최신 최종 ISO 문서라고 부르지 않습니다.

unspecified 순서와 undefined 동작은 다른 결론입니다

C 코드에서 ‘순서를 정하지 않았다’는 말만으로 모든 경우를 undefined behavior라고 부르면 안 됩니다. C11은 함수 호출 인자들이 어떤 순서로 평가되는지 정하지 않습니다. 이것은 서로 독립된 인자에 대해 표준이 한쪽 순서를 보장하지 않는다는 뜻입니다. 반면 같은 스칼라 객체를 두 번 수정하고 그 수정 사이에 필요한 순서가 성립하지 않으면 더 강한 결론인 undefined behavior가 적용될 수 있습니다. 예를 들어 f(i++, i++)는 i를 두 번 증가시키는 식을 하나의 함수 호출 인자 목록에 둡니다. C11 N1570 §6.5p2–3은 같은 객체의 순서 없는 수정과 부분식의 평가 관계를 규정합니다. §6.5.2.2p10은 인자와 함수 지정자 평가를 마친 뒤 실제 호출 전에 sequence point가 있음을 설명합니다. 이는 함수 본문 실행 전의 경계이지 인자끼리 왼쪽부터 평가한다는 뜻이 아닙니다. 같은 절 p12의 예도 인자 안 함수 호출들의 순서를 고정하지 않습니다. 따라서 이 예제에 ‘왼쪽 증가가 먼저’라는 표를 그리거나 특정 숫자 두 개가 전달된다고 단정하면 안 됩니다.

정의되지 않은 함수 호출은 실행하지 않고, 별도 문장으로 순서를 명시한 대안을 제시합니다.
int i = 4;
int first = i++;
int second = i++;
consume(first, second);
/* first is 4, second is 5, and i is 6. */

안전한 대안에서는 문장 경계가 증가 순서를 고정합니다

대안에서는 먼저 i를 4로 둡니다. 첫 번째 전체 문장에서 i++는 현재 값 4를 first에 전달하고, 문장이 끝날 때까지 객체 i를 5로 갱신합니다. 다음 문장은 갱신된 5를 second에 저장하고 i를 6으로 만듭니다. 그 뒤 consume(first, second)는 이미 별도 변수에 저장된 값을 받습니다. 인자 평가 순서가 바뀌더라도 first와 second를 읽는 것만으로 같은 객체를 두 번 수정하지 않으므로 이 예제의 순서 문제는 사라집니다. 여기서 각 증가를 별도 문장으로 나누는 이유는 ‘컴파일러가 보통 이렇게 한다’는 경험칙 때문이 아닙니다. C의 full expression 경계 사이에 sequence point가 있어 첫 번째 증가가 다음 문장 시작 전에 완료되기 때문입니다. 코드를 단계별로 나누면 표준이 보장하는 순서를 풀이에도 그대로 옮길 수 있습니다.

같은 표기 안의 수정과 순차 문장의 수정은 판정이 다릅니다
코드같은 변수 수정 횟수표준상 결론답안 방식
f(i++, i++)두 번, 인자 순서는 미지정undefined behavior출력값을 쓰지 않고 규칙 위반으로 판정
first = i++; second = i++;두 문장에서 차례로 한 번씩각 문장 경계가 증가를 정돈초기값부터 순서대로 표 계산
consume(first, second)인자에서 i를 수정하지 않음이 사례의 부수 효과 충돌 없음저장된 값을 전달

표의 첫 줄에서 답은 ‘인자 1이 4이고 인자 2가 5다’도 아니고 ‘둘 다 4다’도 아닙니다. 이러한 숫자는 실행한 컴파일러에서 관찰한 결과일 수 있지만 언어 규칙상 답이 되지 않습니다. undefined behavior가 있는 프로그램에 표준이 특정 출력 결과를 부여하지 않기 때문입니다.

식마다 같은 객체를 읽고 쓰는 횟수를 표시합니다

  1. 식에서 수정되는 변수나 메모리 위치를 동그라미 칩니다.
  2. 각 수정과 그 객체 값을 읽는 계산을 표시합니다.
  3. 전체 식 안에서 이들 사이에 sequencing 관계가 있는지 C 표준 규칙을 확인합니다.
  4. 순서가 정해지지 않았다는 사실과 unsequenced 접근으로 인한 undefined behavior 판정을 구분합니다.
  5. undefined behavior면 실행 출력이나 한 가지 연산 순서를 만들어 답하지 않습니다.
  6. 안전한 코드가 필요하면 상태 갱신을 별도 full expression으로 분리합니다.

i++는 현재 값을 식의 결과로 내면서 객체 i를 나중에 증가시킵니다. 따라서 변수 추적 문제에서는 ‘결과로 쓰이는 옛 값’과 ‘저장된 뒤의 값’을 서로 다른 칸에 적어야 합니다. 그러나 f(i++, i++)에서는 단순히 옛 값 두 개를 상상해서 인자로 나누는 것이 먼저가 아닙니다. 같은 객체에 대한 수정이 서로 순서 없이 놓였는지를 확인해야 하며, 이 예제는 그 검사에서 undefined behavior로 끝납니다.

undefined behavior를 실행 결과의 불확실성과 혼동하지 않습니다

‘컴파일러마다 다르게 출력할 수 있다’라는 설명은 결과를 아예 단정할 수 없다는 점을 전달할 수 있지만, 시험 답안에서 임의의 가능한 출력 목록을 쓰게 만들 위험도 있습니다. 더 정확한 답은 C11 규칙에 따라 undefined behavior이며, 표준이 출력 결과를 보장하지 않는다는 것입니다. 컴파일이 성공했다는 사실도 동작이 정의되었다는 증거가 아닙니다. 반대 사례도 구분해 보세요. f(left++, right++)에서 left와 right가 서로 다른 독립 스칼라 객체라면 이 동일 객체 수정 충돌은 없습니다. 그렇다고 함수 인자 평가 순서가 왼쪽에서 오른쪽으로 고정되는 것은 아닙니다. 이 독립 객체 예에서는 각 변수의 이전 값이 전달되며, 어느 증가가 먼저인지로 같은 객체 수정 충돌이 생기지는 않습니다. 서로 의존하는 상태 갱신의 순서를 보장해야 하는 다른 코드라면 별도 문장으로 나누어야 합니다. 질문이 순서 보장인지 undefined behavior인지 먼저 읽고 답을 좁히세요.

정답을 가리고 확인해 보세요

이 글을 위해 만든 학습 예시이며 공식 기출문제나 판매 카드의 인용이 아닙니다.

C11에서 f(i++, i++)의 인자 값을 특정 순서로 계산할 수 있나요? 표준상 판정을 말하고, 안전한 순차 대안을 쓰세요.

정답과 해설 확인특정 인자 순서나 출력값을 계산할 수 없습니다. 같은 스칼라 i의 두 수정이 unsequenced라 동작이 undefined입니다. int first = i++; int second = i++; f(first, second);처럼 수정과 값 저장을 별도 문장으로 나눕니다.

N1570 §6.5p2–3으로 동일 객체의 unsequenced 수정을 판정합니다. §6.5.2.2p10의 실제 호출 전 sequence point는 인자끼리의 평가 순서를 정하지 않습니다. N1570은 2011 committee draft이며 최신 최종 ISO 표준은 아닙니다.