본문으로 건너뛰기

안동민 개발노트

본문 시작

연산자 우선순위

여러 연산자가 섞인 표현식의 구문 묶임을 정하고, 실제 평가 순서와 부분식별 타입 변환을 별도로 확인합니다.

하나의 표현식에 여러 연산자가 함께 쓰이면 먼저 어떤 피연산자가 어느 연산자에 속하는지 정해야 합니다.

이 구문 묶임을 정하는 규칙이 **연산자 우선순위(Operator Precedence)**와 **결합 방향(Associativity)**입니다. 그러나 두 규칙은 런타임의 피연산자 평가 순서를 일반적으로 정하지 않습니다. 평가와 부수 효과의 순서는 각 연산자에 정의된 sequencing 규칙을 따로 확인해야 합니다.

C++ 복합 표현식을 구문 묶임, 연산자별 의미, 실제 평가 순서로 나누고 네 예에서 우선순위와 결합 방향이 sequencing과 다름을 비교한 다이어그램

GROUPING IS NOT EVALUATION ORDER

우선순위와 결합 방향은 구문 트리를 만들고, sequencing은 각 연산자의 별도 계약이 정합니다.

  1. 묶임을 확정

    명시적 괄호, 우선순위, 같은 단계의 결합 방향으로 어느 피연산자가 어느 연산자에 속하는지 적습니다.

  2. 연산자 계약 적용

    각 노드에서 허용 타입, 변환, 전제 조건, 결과 타입·값 범주와 부수 효과를 확인합니다.

  3. 평가 순서를 별도 확인

    &&, ||, ?:, 대입, 콤마처럼 순서를 정한 경우만 믿고, 보장이 없으면 부수 효과를 분리합니다.

5 + 3 * 2

묶임 5 + (3 * 2)

결과 안쪽 곱의 값 6을 사용해 11을 만듭니다.

10 - 5 + 2

묶임 (10 - 5) + 2

경계 왼쪽 결합은 일반적인 피연산자 실행 방향이 아닙니다.

a = b = 10

묶임 a = (b = 10)

sequencing 기본 제공 대입은 오른쪽 피연산자를 왼쪽보다 먼저 평가합니다.

f() + g()

묶임 두 호출 결과가 +의 피연산자입니다.

sequencing 어느 호출이 먼저인지 우선순위로 정할 수 없습니다.

괄호는 묶임을 명확하게 하지만 보통 새로운 sequencing을 만들지 않습니다. 순서가 중요한 부수 효과는 중간 변수와 별도 문장으로 표현합니다.


연산자 우선순위란?

우선순위가 높은 연산자는 주변 피연산자를 더 강하게 묶습니다.

연산자 우선순위 예시
int result = 5 + 3 * 2;

곱셈 연산자 *가 덧셈 연산자 +보다 높은 단계에 있으므로 이 식은 다음처럼 해석됩니다.

int result = 5 + (3 * 2); // 11

3 * 2의 값이 바깥쪽 +의 오른쪽 피연산자가 되어 결과는 11입니다. 반면 (5 + 3) * 2처럼 괄호로 묶음을 바꾸면 결과는 16입니다.

여기서 “곱셈이 먼저 묶인다”와 “모든 하위 표현식이 높은 우선순위 순서로 실행된다”는 다른 주장입니다. 특히 f() + g()에서 +의 두 피연산자 호출 순서는 우선순위로 정할 수 없습니다.


결합 방향이란?

결합 방향은 같은 우선순위 단계의 연산자가 연속될 때 구문이 어느 쪽으로 묶이는지 정합니다.

왼쪽 결합
int result = 10 - 5 + 2;
// (10 - 5) + 2 -> 7

이항 +-는 같은 단계에서 왼쪽으로 묶이므로 결과는 7입니다. 이는 일반적인 피연산자 평가 방향을 “항상 왼쪽부터”로 정한다는 뜻이 아닙니다.

대입 연산자는 오른쪽으로 묶입니다.

오른쪽 결합
int a = 0;
int b = 0;
a = b = 10;
// a = (b = 10)

기본 제공 대입에서는 오른쪽 피연산자가 왼쪽 피연산자보다 먼저 평가되고, 두 피연산자의 값 계산 뒤 저장이 일어납니다. 따라서 안쪽 b = 10의 결과가 바깥쪽 대입에 사용되어 두 객체가 모두 10이 됩니다.


주요 우선순위 단계

C++ 표준은 별도의 숫자 표 대신 표현식 문법으로 묶임을 정의합니다. 다음 표는 이 절에서 자주 쓰는 기본 제공 연산자를 높은 단계부터 요약한 읽기 도구입니다.

상대 단계주요 형태기본 묶임읽을 때 확인할 점
높음a(), a[], a.m, p->m, x++왼쪽호출·첨자·멤버 접근·후위 증감
++x, --x, +x, -x, !x, ~x, *p, &x, sizeof x오른쪽전위·단항 표현식
p->*m, a.*m왼쪽멤버 포인터
*, /, %왼쪽usual arithmetic conversions, /·%의 0 제수 금지
+, -왼쪽산술 또는 포인터 연산 계약
<<, >>왼쪽정수 승격, 결과 타입, 유효한 이동 수
<=>왼쪽세 방향 비교
<, <=, >, >=왼쪽연쇄 비교는 수학식과 다르게 묶임
==, !=왼쪽관계 연산보다 낮은 단계
&왼쪽비트 AND
^왼쪽비트 XOR
|왼쪽비트 OR
&&왼쪽왼쪽 우선 평가와 false 단락
||왼쪽왼쪽 우선 평가와 true 단락
?:, =, +=, -=, *=, /=오른쪽조건 분기와 대입에는 별도 sequencing 규칙이 있음
낮음,왼쪽기본 제공 콤마 연산자는 왼쪽을 먼저 평가하고 버림

함수 인수, 초기화 목록, 템플릿 인수 등에 쓰인 쉼표는 구분자이며 기본 제공 콤마 연산자가 아닙니다. 오버로드된 연산자는 구문상의 우선순위와 묶임은 유지하지만, 피연산자 타입과 의미에는 함수 호출·오버로드 해석 규칙이 관여합니다.


괄호는 묶음을 명시한다

괄호로 만든 parenthesized expression은 기본 우선순위가 만들 묶음을 바꿉니다. 괄호 자체를 “모든 연산자보다 높은 연산자”로 보기보다는, 원하는 부분식을 문법적으로 하나로 묶는 장치로 이해하는 편이 정확합니다.

괄호로 의도 고정
int a = 10;
int b = 5;
int c = 2;

int by_precedence = a + b * c; // a + (b * c) -> 20
int by_intent = (a + b) * c;   // (a + b) * c -> 30

bool enabled = true;
bool ready = false;
bool valid = true;

bool default_group = enabled || ready && valid;
// enabled || (ready && valid)

bool explicit_group = (enabled || ready) && valid;

괄호는 묶임을 분명하게 만들지만, 보통 연산자의 피연산자 사이에 없던 sequencing까지 추가하지는 않습니다. 예를 들어 (f()) + (g())의 괄호는 두 호출의 평가 순서를 고정하지 않습니다. 부수 효과의 순서가 중요하다면 별도 문장과 중간 변수로 표현합니다.


묶인 부분식마다 타입을 결정한다

산술 변환은 단순히 “더 큰 타입으로 바뀐다”는 규칙이 아닙니다. 이 예에서 산술 타입을 받는 기본 제공 *, /, +는 각 노드의 피연산자에 usual arithmetic conversions를 적용하고 그 공통 타입으로 연산합니다. 필요한 정수 승격도 이 변환 과정에 포함됩니다. 시프트처럼 다른 변환·결과 타입 규칙을 가진 연산자는 해당 계약을 따로 확인해야 합니다.

부분식별 타입 변환
double d_val = 10.5;
int i_val = 4;

double result_div = d_val / i_val;
// double / double -> 2.625

double result_complex = 2 * d_val + i_val / 3;
// (2 * d_val) + (i_val / 3)
// 21.0(double) + 1(int) -> 22.0(double)

2 * d_val에서는 2double 쪽 산술 변환을 거쳐 21.0을 만듭니다. 별개의 오른쪽 부분식 i_val / 3은 두 피연산자가 int이므로 소수부를 버린 정수 몫 1을 만듭니다. 바깥쪽 +가 그 1double로 변환해 최종 값 22.0을 만듭니다.

두 곱셈·나눗셈 부분식은 모두 바깥쪽 덧셈의 피연산자이지만, +가 왼쪽 부분식을 오른쪽 부분식보다 먼저 평가하도록 보장하지는 않습니다. 이 예에는 부수 효과가 없으므로 어느 쪽을 먼저 평가해도 같은 값이 나옵니다.

오른쪽 나눗셈 자체를 실수 나눗셈으로 바꾸려면 변환을 그 부분식 안에 둡니다.

double precise = 2 * d_val + static_cast<double>(i_val) / 3;
// 21.0 + 1.333... -> 22.333...
2 곱하기 double 값과 int 나누기 3의 두 부분식이 각각 double 21과 int 1을 만든 뒤 바깥 덧셈에서 공통 double 타입으로 변환되어 22가 되는 타입 흐름

TYPE FLOW FOLLOWS THE PARSE TREE

2 * d_val + i_val / 3은 두 부분식에서 각각 변환·연산한 뒤 바깥쪽 +에서 다시 공통 타입을 정합니다.

복합 산술 표현식의 부분식별 타입 변환 트리 왼쪽 곱셈은 정수 2를 double로 변환해 double 21을 만들고 오른쪽 나눗셈은 int끼리 계산해 int 1을 만든다. 두 결과가 바깥 덧셈에서 double 22로 합쳐진다. 왼쪽 부분식 2 * d_val · int → double 21.0 · double 오른쪽 부분식 i_val / 3 · int / int 소수부를 버린 1 · int 바깥 덧셈 21.0 + 1.0 22.0 · double
  1. 구문 묶임

    (2 * d_val) + (i_val / 3)으로 두 자식 부분식을 나눕니다.

  2. 왼쪽 곱셈

    2double로 변환되어 21.0을 만듭니다.

  3. 오른쪽 나눗셈

    int / int의 결과는 소수부를 버린 1입니다.

  4. 바깥 덧셈

    1double로 변환되어 최종 22.0을 만듭니다.

현재 변환 위치

i_val / 3이 먼저 int 결과 1을 만들고, 바깥 덧셈에서 그 결과를 double로 변환합니다.

나눗셈 전에 변환

static_cast<double>(i_val) / 3은 나눗셈 자체를 실수 연산으로 만들어 약 1.333을 남깁니다.

두 자식 부분식의 값은 바깥 덧셈 결과보다 먼저 필요하지만, 기본 제공 +는 왼쪽 자식을 오른쪽 자식보다 먼저 평가한다고 보장하지 않습니다.

복합 표현식은 다음 순서로 검토하면 안전합니다.

묶임: 명시적 괄호와 연산자 문법으로 부분식 구조를 적습니다.

연산자 계약: 각 부분식의 허용 타입, 전제 조건, 변환, 결과 타입과 값 범주를 확인합니다.

sequencing: 부수 효과가 있다면 실제 평가 순서가 보장되는지 별도로 확인합니다.

의도 표현: 한눈에 확신하기 어렵다면 괄호, 중간 변수, 별도 문장으로 다시 씁니다.

우선순위 표는 구문을 읽는 도구입니다. 정확한 코드는 묶임, 연산자별 의미, 실제 평가 순서를 서로 바꾸어 말하지 않습니다.