산술 연산자

산술 연산자 (Arithmetic operators)

산술 연산자는 특정 산술 연산의 결과를 돌려줘요. 덧셈, 뺄셈, 곱셈, 나눗셈, 나머지, 비트 연산까지 한자리에서 정리하는 주제예요.

출처: cppreference - Arithmetic operators

본문

연산자 이름 문법 클래스 안 프로토타입 예 (T) 클래스 밖 프로토타입 예 (T)
단항 플러스 (Unary plus) +a T T::operator+() const; T operator+(const T& a);
단항 마이너스 (Unary minus) -a T T::operator-() const; T operator-(const T& a);
덧셈 (Addition) a + b T T::operator+(const T2& b) const; T operator+(const T& a, const T2& b);
뺄셈 (Subtraction) a - b T T::operator-(const T2& b) const; T operator-(const T& a, const T2& b);
곱셈 (Multiplication) a * b T T::operator*(const T2& b) const; T operator*(const T& a, const T2& b);
나눗셈 (Division) a / b T T::operator/(const T2& b) const; T operator/(const T& a, const T2& b);
나머지 (Remainder) a % b T T::operator%(const T2& b) const; T operator%(const T& a, const T2& b);
비트 NOT (Bitwise NOT) ~a T T::operator~() const; T operator~(const T& a);
비트 AND (Bitwise AND) a & b T T::operator&(const T2& b) const; T operator&(const T& a, const T2& b);
비트 OR (Bitwise OR) a | b T T::operator|(const T2& b) const; T operator|(const T& a, const T2& b);
비트 XOR (Bitwise XOR) a ^ b T T::operator^(const T2& b) const; T operator^(const T& a, const T2& b);
비트 좌측 시프트 a << b T T::operator<<(const T2& b) const; T operator<<(const T& a, const T2& b);
비트 우측 시프트 a >> b T T::operator>>(const T2& b) const; T operator>>(const T& a, const T2& b);

참고:

  • 이 표의 모든 연산자는 오버로드할 수 있어요.
  • 모든 내장 연산자는 값을 돌려주고, 대부분의 사용자 정의 오버로드도 내장처럼 쓰일 수 있도록 값을 돌려줘요. 다만 사용자 정의 연산자 오버로드에서는 반환 타입으로 어떤 타입이든(void 포함) 쓸 수 있어요. 특히 스트림 삽입·추출 오버로드인 operator<<operator>>T&를 돌려줘요.
  • T2T를 포함해 어떤 타입이든 될 수 있어요.

일반 설명 (General explanation)

모든 내장 산술 연산자는 특정 산술 연산의 결과를 계산해 그 결과를 돌려줘요. 인자(피연산자)는 수정되지 않아요.

변환 (Conversions)

내장 산술 연산자에 전달된 피연산자가 정수 타입이나 unscoped 열거형 타입이면, 다른 어떤 동작보다 먼저 (적용 가능하다면 lvalue-to-rvalue 변환 후에) 피연산자가 **정수 승격(integral promotion)**을 거쳐요. 피연산자가 배열이나 함수 타입이면 배열-포인터, 함수-포인터 변환이 적용돼요.

이항 연산자(시프트 제외)의 경우, 승격된 피연산자 타입이 다르면 **일반 산술 변환(usual arithmetic conversions)**이 적용돼요.

오버플로 (Overflows)

부호 없는 정수 산술은 항상 mod 2^n으로 수행돼요. 여기서 n은 그 정수의 비트 수예요. 예를 들어 unsigned int에서 UINT_MAX에 1을 더하면 0이 되고, 0에서 1을 빼면 UINT_MAX가 돼요.

부호 있는 정수 산술 연산이 오버플로되면(결과가 결과 타입에 맞지 않으면) 동작이 정의되지 않아요. 이런 연산의 가능한 징후는 다음과 같아요:

  • 표현 규칙(보통 2의 보수)에 따라 감긴다(wraps around)
  • 일부 플랫폼이나 컴파일러 옵션에 의해 트랩(trap)된다 (예: GCC·Clang의 -ftrapv)
  • 최솟값이나 최댓값으로 포화된다 (많은 DSP에서)
  • 컴파일러에 의해 완전히 최적화되어 사라진다

부동소수점 환경 (Floating-point environment)

#pragma STDC FENV_ACCESS가 지원되고 ON으로 설정되어 있다면, 모든 부동소수점 산술 연산자는 static initializer의 일부가 아닌 한 현재 부동소수점 반올림 방향을 따르고 math_errhandling에 지정된 대로 부동소수점 산술 오류를 보고해요. (static initializer에 포함되면 부동소수점 예외는 발생하지 않고 반올림 모드는 nearest예요.)

부동소수점 수축 (Floating-point contraction)

#pragma STDC FP_CONTRACT가 지원되고 OFF로 설정되어 있지 않다면, 모든 부동소수점 산술은 중간 결과가 무한한 범위와 정밀도를 가진 것처럼 수행될 수 있어요. 즉 반올림 오류와 부동소수점 예외를 생략하는 최적화가 허용돼요. 예를 들어 C++는 (x*y)+z를 단일의 fused multiply-add CPU 명령어로 구현하거나, a=x*x*x*x;tmp=x*x; a=tmp*tmp;로 최적화하는 것을 허용해요.

수축과는 무관하게, 부동소수점 산술의 중간 결과는 타입이 나타내는 값과 다른 범위와 정밀도를 가질 수 있어요. FLT_EVAL_METHOD를 참고해요.

형식적으로 말하면 C++ 표준은 부동소수점 연산의 정확도에 어떤 보장도 하지 않아요.

단항 산술 연산자 (Unary arithmetic operators)

단항 산술 연산자 표현식은 다음 형태를 가져요:

형태 설명
+expression (1) 단항 플러스 (승격)
-expression (2) 단항 마이너스 (부정)

단항 +-는 모든 이항 산술 연산자보다 우선순위가 높아요. 그래서 표현식은 최상위 이항 산술 연산자를 포함할 수 없어요. 이 연산자들은 오른쪽에서 왼쪽으로 결합해요:

+a - b; // equivalent to (+a) - b, NOT +(a - b)
-c + d; // equivalent to (-c) + d, NOT -(c + d)

+-e; // equivalent to +(-e), the unary + is a no-op if "e" is a built-in type
     // because any possible promotion is performed during negation already

내장 단항 산술 연산자

  1. 내장 단항 플러스 연산자: expression은 산술, unscoped 열거, 포인터 타입의 prvalue여야 해요. expression이 정수나 unscoped 열거 타입이면 정수 승격이 수행돼요. 결과 타입은 expression의 (승격된) 타입이에요. 내장 승격의 결과는 expression의 값이에요. 피연산자가 승격된 정수 타입이나 포인터 타입의 prvalue면 내장 단항 연산은 no-op이에요. 그 외에는 정수 승격이나 lvalue-to-rvalue, 배열-포인터, 함수-포인터, 사용자 정의 변환에 의해 피연산자의 타입이나 값 범주가 바뀌어요. 예를 들어 charint로 변환되고, 일반 캡처 없는 람다 표현식은 단항 플러스 표현식에서 함수 포인터로 변환돼요(C++11 이후).

  2. 내장 단항 마이너스 연산자: expression은 산술이나 unscoped 열거 타입의 prvalue여야 해요. 정수 승격이 수행돼요. 결과 타입은 승격된 타입이에요. 내장 부정의 결과는 승격된 표현식의 음수예요. 부호 없는 a에 대해 -a의 값은 2^N - a이에요. 여기서 N은 승격 후의 비트 수예요.

다시 말해 결과는 피연산자의 2의 보수(two's complement)예요 (피연산자와 결과를 둘 다 부호 없음으로 간주).

오버로드 (단항)

사용자 정의 연산자와의 오버로드 결정에서, cv 비한정 승격된 산술 타입 A마다 그리고 타입 T마다 다음 함수 시그니처가 오버로드 결정에 참여해요:

A operator+(A)
T* operator+(T*)
A operator-(A)
#include <iostream>

int main()
{
    char c = 0x6a;
    int n1 = 1;
    unsigned char n2 = 1;
    unsigned int n3 = 1;
    std::cout << "char: " << c << " int: " << +c << "\n"
                 "-1, where 1 is signed: " << -n1 << "\n"
                 "-1, where 1 is unsigned char: " << -n2 << "\n"
                 "-1, where 1 is unsigned int: " << -n3 << '\n';
    char a[3];
    std::cout << "size of array: " << sizeof a << "\n"
                 "size of pointer: " << sizeof +a << '\n';
}

가능한 출력:

char: j int: 106
-1, where 1 is signed: -1
-1, where 1 is unsigned char: -1
-1, where 1 is unsigned int: 4294967295
size of array: 3
size of pointer: 8

여기서 예시가 드러내는 차이가 재미있어요. 부호 없는 unsigned int에 단항 마이너스를 적용하면 mod 2^32 연산이라 4294967295가 나와요. 또 +a(단항 플러스)를 배열에 적용하면 배열-포인터 변환이 일어나서 sizeof가 8(포인터)이 돼요.

가산 연산자 (Additive operators)

가산 연산자 표현식은 다음 형태를 가져요:

형태 설명
lhs + rhs (1) 이항 플러스 (덧셈)
lhs - rhs (2) 이항 마이너스 (뺄셈)

이항 +, -*, /, %를 제외한 모든 다른 이항 산술 연산자보다 우선순위가 높아요. 왼쪽에서 오른쪽으로 결합해요:

a + b * c;  // equivalent to a + (b * c),  NOT (a + b) * c
d / e - f;  // equivalent to (d / e) - f,  NOT d / (e - f)
g + h >> i; // equivalent to (g + h) >> i, NOT g + (h >> i)

j - k + l - m; // equivalent to ((j - k) + l) - m

내장 가산 연산자

내장 이항 플러스·마이너스 연산자에서 lhsrhs 모두 prvalue여야 하고, 다음 중 하나를 만족해야 해요:

  • 두 피연산자 모두 산술 또는 unscoped 열거 타입. 이 경우 두 피연산자에 일반 산술 변환이 수행돼요.
  • 정확히 한 피연산자가 정수 또는 unscoped 열거 타입. 이 경우 그 피연산자에 정수 승격이 적용돼요.

이 절의 나머지 설명에서 "피연산자"(lhs, rhs)는 변환되거나 승격된 피연산자를 가리켜요.

  1. 내장 덧셈: 다음 중 하나를 만족해야 해요.
  • 두 피연산자 모두 산술 타입. 이 경우 결과는 피연산자들의 합이에요.
  • 한 피연산자가 완전히 정의된 객체 타입에 대한 포인터이고 다른 피연산자가 정수 타입. 이 경우 정수 값을 포인터에 더해요 (포인터 산술 참고).
  1. 내장 뺄셈: 다음 중 하나를 만족해야 해요.
  • 두 피연산자 모두 산술 타입. 이 경우 결과는 lhs에서 rhs를 뺀 차이예요.
  • lhs가 완전히 정의된 객체 타입에 대한 포인터이고 rhs가 정수 타입. 이 경우 포인터에서 정수 값을 빼요 (포인터 산술 참고).
  • 두 피연산자 모두 같은 완전히 정의된 객체 타입의 cv 한정 또는 비한정 버전에 대한 포인터. 이 경우 lhs에서 rhs를 뺴요 (포인터 산술 참고).

두 피연산자 모두 부동소수점 타입이고 그 타입이 IEEE 부동소수점 산술을 지원한다면 (std::numeric_limits::is_iec559 참고):

  • 한 피연산자가 NaN이면 결과는 NaN이에요.
  • 무한대에서 무한대를 빼면 NaN이고, FE_INVALID가 발생해요.
  • 무한대에 음의 무한대를 더하면 NaN이고, FE_INVALID가 발생해요.

포인터 산술 (Pointer arithmetic)

정수 타입의 표현식 J를 포인터 타입의 표현식 P에 더하거나 빼면 결과는 P의 타입이에요.

  • P가 널 포인터 값으로 평가되고 J가 0으로 평가되면 결과는 널 포인터 값이에요.
  • 그 외에 Pn개의 요소를 가진 배열 객체 xi번째 요소를 가리키고 J의 값이 j라면, P는 다음과 같이 더하거나 빼져요:
    • P+JJ+Pi+j[0, n) 안에 있으면 x(i+j)번째 요소를 가리키고, i+jn이면 x의 마지막 요소 뒤를 가리키는 포인터가 돼요.
    • P-Ji-j[0, n) 안에 있으면 x(i-j)번째 요소를 가리키고, i-jn이면 마지막 요소 뒤 포인터예요.
    • 그 외의 j 값은 정의되지 않은 동작이에요.
  • 그 외에 P가 완전 객체, 기반 클래스 부분객체, 멤버 부분객체 y를 가리키고 J의 값이 j라면:
    • P+JJ+Pj가 0이면 y를 가리키고, j가 1이면 y의 끝 뒤 포인터예요.
    • P-Jj가 0이면 y를 가리키고, j가 -1이면 y의 끝 뒤 포인터예요.
    • 그 외의 j 값은 정의되지 않은 동작이에요.
  • 그 외에 P가 객체 z의 끝 뒤 포인터이고 J의 값이 j라면:
    • zn개 요소의 배열이면:
      • P+JJ+Pn+j[0, n) 안에 있으면 z(n+j)번째 요소를, j가 0이면 z의 마지막 요소 뒤 포인터가 돼요.
      • P-Jn-j[0, n) 안에 있으면 z(n-j)번째 요소를, j가 0이면 마지막 요소 뒤 포인터예요.
      • 그 외의 j 값은 정의되지 않은 동작이에요.
    • 그 외에는:
      • P+JJ+Pj가 -1이면 z를, j가 0이면 z의 끝 뒤 포인터를 가리켜요.
      • P-Jj가 1이면 z를, j가 0이면 z의 끝 뒤 포인터를 가리켜요.
      • 그 외의 j 값은 정의되지 않은 동작이에요.
  • 그 외에는 동작이 정의되지 않아요.

두 포인터 표현식 PQ를 뺄 때 결과 타입은 std::ptrdiff_t예요.

  • PQ가 모두 널 포인터 값으로 평가되면 결과는 0이에요.
  • 그 외에 PQ가 각각 같은 배열 객체 xi번째, j번째 배열 요소를 가리키면 P - Q의 값은 i - j예요.
    • i - jstd::ptrdiff_t로 표현할 수 없으면 동작이 정의되지 않아요.
  • 그 외에 PQ가 같은 완전 객체·기반 클래스 부분객체·멤버 부분객체를 가리키면 결과는 0이에요.
  • 그 외에는 동작이 정의되지 않아요.

이 포인터 산술 연산자들 덕분에 포인터가 LegacyRandomAccessIterator 요구사항을 충족할 수 있어요.

덧셈·뺄셈에서 PQ가 "(가능하면 cv 한정된) T에 대한 포인터" 타입이고 T와 배열 요소 타입이 유사(similar)하지 않으면 동작이 정의되지 않아요:

int arr[5] = {1, 2, 3, 4, 5};
unsigned int *p = reinterpret_cast<unsigned int*>(arr + 1);
unsigned int k = *p; // OK, the value of "k" is 2
unsigned int *q = p + 1; // undefined behavior: "p" points to int, not unsigned int

오버로드 (가산)

사용자 정의 연산자와의 오버로드 결정에서, 승격된 산술 타입의 모든 쌍 L, R과 모든 객체 타입 T에 대해 다음 함수 시그니처가 참여해요 (여기서 LRLR의 일반 산술 변환 결과예요):

LR operator+(L, R)
LR operator-(L, R)
T* operator+(T*, std::ptrdiff_t)
T* operator+(std::ptrdiff_t, T*)
T* operator-(T*, std::ptrdiff_t)
std::ptrdiff_t operator-(T*, T*)
#include <iostream>

int main()
{
    char c = 2;
    unsigned int un = 2;
    int n = -10;
    std::cout << " 2 + (-10), where 2 is a char    = " << c + n << "\n"
                 " 2 + (-10), where 2 is unsigned  = " << un + n << "\n"
                 " -10 - 2.12  = " << n - 2.12 << '\n';

    char a[4] = {'a', 'b', 'c', 'd'};
    char* p = &a[1];
    std::cout << "Pointer addition examples: " << *p << *(p + 2)
              << *(2 + p) << *(p - 1) << '\n';
    char* p2 = &a[4];
    std::cout << "Pointer difference: " << p2 - p << '\n';
}

출력:

 2 + (-10), where 2 is a char    = -8
 2 + (-10), where 2 is unsigned  = 4294967288
 -10 - 2.12  = -12.12
Pointer addition examples: bdda
Pointer difference: 3

charc는 정수 승격으로 int가 돼서 -8이 나오지만, unsignedun은 mod 2^32 연산이라 4294967288이 나와요.

곱셈 연산자 (Multiplicative operators)

곱셈 연산자 표현식은 다음 형태를 가져요:

형태 설명
lhs * rhs (1) 곱셈
lhs / rhs (2) 나눗셈
lhs % rhs (3) 나머지

곱셈 연산자는 다른 모든 이항 산술 연산자보다 우선순위가 높아요. 왼쪽에서 오른쪽으로 결합해요:

a + b * c;  // equivalent to a + (b * c),  NOT (a + b) * c
d / e - f;  // equivalent to (d / e) - f,  NOT d / (e - f)
g % h >> i; // equivalent to (g % h) >> i, NOT g % (h >> i)

j * k / l % m; // equivalent to ((j * k) / l) % m

내장 곱셈 연산자

내장 곱셈·나눗셈 연산자에서 두 피연산자 모두 산술 또는 unscoped 열거 타입이어야 해요. 내장 나머지 연산자는 두 피연산자 모두 정수 또는 unscoped 열거 타입이어야 해요. 두 피연산자에 일반 산술 변환이 수행돼요.

이 절의 나머지 설명에서 "피연산자"(lhs, rhs)는 변환된 피연산자를 가리켜요.

  1. 내장 곱셈의 결과는 피연산자들의 곱이에요. 두 피연산자 모두 부동소수점 타입이고 그 타입이 IEEE 부동소수점 산술을 지원한다면:
  • NaN과 어떤 수의 곱은 NaN이에요.
  • 무한대와 0의 곱은 NaN이고 FE_INVALID가 발생해요.
  1. 내장 나눗셈의 결과는 lhsrhs로 나눈 값이에요. rhs가 0이면 동작이 정의되지 않아요. 두 피연산자 모두 정수 타입이면 결과는 대수 몫(algebraic quotient)이에요 (정수 나눗셈을 수행): 몫은 0을 향해 잘려요(소수 부분은 버려짐). 두 피연산자 모두 부동소수점 타입이고 타입이 IEEE 부동소수점 산술을 지원한다면:
  • 한 피연산자가 NaN이면 결과는 NaN이에요.
  • 0이 아닌 수를 ±0.0으로 나누면 올바른 부호의 무한대가 되고 FE_DIVBYZERO가 발생해요.
  • 0.0을 0.0으로 나누면 NaN이고 FE_INVALID가 발생해요.
  1. 내장 나머지의 결과는 lhsrhs로 나눈 정수 나눗셈의 나머지예요. rhs가 0이면 동작이 정의되지 않아요. a / b가 결과 타입으로 표현 가능하면 (a / b) * b + a % b == a예요. a / b가 결과 타입으로 표현 불가능하면 a / ba % b 둘 다 동작이 정의되지 않아요. (즉 2의 보수 시스템에서 INT_MIN % -1은 정의되지 않아요.)

참고: CWG issue 614가 해결되기 전(N2757)에는 이항 %의 피연산자 하나 이상이 음수면 나머지의 부호가 구현 정의였어요. 정수 나눗셈의 반올림 방향에 달려 있기 때문이에요. 그 경우 std::div 함수가 잘 정의된 동작을 제공했어요.

참고: 부동소수점 나머지는 std::remainderstd::fmod를 참고해요.

오버로드 (곱셈)

사용자 정의 연산자와의 오버로드 결정에서, 승격된 산술 타입의 모든 쌍 LA, RA와 승격된 정수 타입의 모든 쌍 LI, RI에 대해 다음 시그니처가 참여해요 (여기서 LRxLxRx의 일반 산술 변환 결과예요):

LRA operator*(LA, RA)
LRA operator/(LA, RA)
LRI operator%(LI, RI)
#include <iostream>

int main()
{
    char c = 2;
    unsigned int un = 2;
    int  n = -10;
    std::cout << "2 * (-10), where 2 is a char    = " << c * n << "\n"
                 "2 * (-10), where 2 is unsigned  = " << un * n << "\n"
                 "-10 / 2.12  = " << n / 2.12 << "\n"
                 "-10 / 21  = " << n / 21 << "\n"
                 "-10 % 21  = " << n % 21 << '\n';
}

출력:

2 * (-10), where 2 is a char    = -20
2 * (-10), where 2 is unsigned  = 4294967276
-10 / 2.12  = -4.71698
-10 / 21  = 0
-10 % 21  = -10

-10 / 21은 정수 나눗셈이라 0을 향해 잘려 0이 되고, -10 % 21-10이에요. -10을 21로 나눈 나머지의 부호는 원래 피연산자 부호를 따라요.

비트 논리 연산자 (Bitwise logic operators)

비트 논리 연산자 표현식은 다음 형태를 가져요:

형태 설명
~rhs (1) 비트 NOT
lhs & rhs (2) 비트 AND
lhs | rhs (3) 비트 OR
lhs ^ rhs (4) 비트 XOR

비트 NOT 연산자는 모든 이항 산술 연산자보다 우선순위가 높아요. 오른쪽에서 왼쪽으로 결합해요:

~a - b; // equivalent to (~a) - b, NOT ~(a - b)
~c * d; // equivalent to (~c) * d, NOT ~(c * d)

~-e; // equivalent to ~(-e)

~ 다음에 타입 이름이나 decltype 지정자(C++11 이후)가 오면 문법에 모호함이 있어요. operator~가 될 수도 있고 소멸자 식별자가 시작될 수도 있거든요. 이 모호함은 ~operator~로 처리해서 해결해요. ~operator~를 형성하는 것이 문법적으로 유효하지 않은 곳에서만 소멸자 식별자를 시작할 수 있어요.

다른 모든 비트 논리 연산자는 다른 모든 이항 산술 연산자보다 우선순위가 낮아요. 비트 AND는 비트 XOR보다, 비트 XOR은 비트 OR보다 우선순위가 높아요. 왼쪽에서 오른쪽으로 결합해요:

a & b * c;  // equivalent to a & (b * c),  NOT (a & b) * c
d / e ^ f;  // equivalent to (d / e) ^ f,  NOT d / (e ^ f)
g << h | i; // equivalent to (g << h) | i, NOT g << (h | i)

j & k & l; // equivalent to (j & k) & l
m | n ^ o  // equivalent to m | (n ^ o)

내장 비트 논리 연산자

내장 비트 NOT 연산자에서 rhs는 정수 또는 unscoped 열거 타입의 prvalue여야 하고, rhs에 정수 승격이 수행돼요. 다른 내장 비트 논리 연산자에서는 두 피연산자 모두 정수 또는 unscoped 열거 타입이어야 하고 일반 산술 변환이 수행돼요.

이 절의 나머지 설명에서 "피연산자"(lhs, rhs)는 변환되거나 승격된 피연산자를 가리켜요.

  1. 피연산자를 x, 내장 비트 NOT 연산 결과를 r이라고 할 때, x의 2진 표현 계수 x_i에 대해 r의 2진 표현 계수 r_ix_i가 0이면 1, 그 외에는 0이에요.

다시 말해 결과는 피연산자의 1의 보수(one's complement)예요 (피연산자와 결과를 둘 다 부호 없음으로 간주). 결과 r의 타입은 피연산자 x의 타입이에요.

2-4) 피연산자를 각각 x, y, 이항 비트 논리 연산의 결과를 r이라고 할 때, xy의 2진 표현 계수 쌍 x_i, y_i에 대해 r의 계수 r_i는:

  • (2, AND) x_iy_i가 모두 1이면 1, 그 외에는 0.
  • (3, OR) x_iy_i 중 적어도 하나가 1이면 1, 그 외에는 0.
  • (4, XOR) x_iy_i 중 정확히 하나만 1이면 1, 그 외에는 0.

결과 r의 타입은 피연산자 x, y의 타입이에요.

오버로드 (비트 논리)

사용자 정의 연산자와의 오버로드 결정에서, 승격된 정수 타입의 모든 쌍 L, R에 대해 다음 시그니처가 참여해요 (여기서 LRLR의 일반 산술 변환 결과예요):

R operator~(R)
LR operator&(L, R)
LR operator^(L, R)
LR operator|(L, R)
#include <bitset>
#include <cstdint>
#include <iomanip>
#include <iostream>

int main()
{
    std::uint16_t mask = 0x00f0;
    std::uint32_t x0 = 0x12345678;
    std::uint32_t x1 = x0 | mask;
    std::uint32_t x2 = x0 & ~mask;
    std::uint32_t x3 = x0 & mask;
    std::uint32_t x4 = x0 ^ mask;
    std::uint32_t x5 = ~x0;
    using bin16 = std::bitset<16>;
    using bin32 = std::bitset<32>;
    std::cout << std::hex << std::showbase
              << "Mask: " << mask << std::setw(49) << bin16(mask) << "\n"
                 "Value: " << x0 << std::setw(42) << bin32(x0) << "\n"
                 "Setting bits: " << x1 << std::setw(35) << bin32(x1) << "\n"
                 "Clearing bits: " << x2 << std::setw(34) << bin32(x2) << "\n"
                 "Selecting bits: " << x3 << std::setw(39) << bin32(x3) << "\n"
                 "XOR-ing bits: " << x4 << std::setw(35) << bin32(x4) << "\n"
                 "Inverting bits: " << x5 << std::setw(33) << bin32(x5) << '\n';
}

출력:

Mask: 0xf0                                 0000000011110000
Value: 0x12345678          00010010001101000101011001111000
Setting bits: 0x123456f8   00010010001101000101011011111000
Clearing bits: 0x12345608  00010010001101000101011000001000
Selecting bits: 0x70       00000000000000000000000001110000
XOR-ing bits: 0x12345688   00010010001101000101011010001000
Inverting bits: 0xedcba987 11101101110010111010100110000111

이 예제는 비트 마스크 조작의 전형적인 네 가지 패턴을 보여줘요. |는 비트를 켜고(Setting), & ~는 비트를 끄고(Clearing), &는 비트를 걸러내고(Selecting), ^는 비트를 뒤집어요(XOR-ing).

비트 시프트 연산자 (Bitwise shift operators)

비트 시프트 연산자 표현식은 다음 형태를 가져요:

형태 설명
lhs << rhs (1) 비트 좌측 시프트
lhs >> rhs (2) 비트 우측 시프트

비트 시프트 연산자는 비트 논리 연산자보다 우선순위가 높지만, 가산·곱셈 연산자보다는 낮아요. 왼쪽에서 오른쪽으로 결합해요:

a >> b * c;  // equivalent to a >> (b * c),  NOT (a >> b) * c
d << e & f;  // equivalent to (d << e) & f,  NOT d << (e & f)

g << h >> i; // equivalent to (g << h) >> i, NOT g << (h >> i)

내장 비트 시프트 연산자

내장 비트 시프트 연산자에서 두 피연산자 모두 정수 또는 unscoped 열거 타입의 prvalue여야 해요. 두 피연산자에 정수 승격이 수행돼요.

이 절의 나머지 설명에서 "피연산자"(a, b, lhs, rhs)는 변환되거나 승격된 피연산자를 가리켜요.

rhs의 값이 음수이거나 lhs의 비트 수보다 작지 않으면 동작이 정의되지 않아요.

부호 없는 a에 대해 a << b의 값은 a * 2^b를 mod 2^N으로 줄인 값이에요. N은 반환 타입의 비트 수예요 (즉 비트 좌측 시프트가 수행되고 대상 타입에서 밀려나온 비트는 버려져요).

부호 있고 음이 아닌 a에서 a * 2^b가 반환 타입의 부호 없는 버전으로 표현 가능하면, 그 값을 부호 있는 값으로 변환한 것이 a << b의 값이에요 (덕분에 1 << 31INT_MIN을 만드는 게 합법적이에요). 그렇지 않으면 동작이 정의되지 않아요.

음수 a의 경우 a << b의 동작은 정의되지 않아요.

부호 없는 a, 그리고 부호 있고 음이 아닌 a에 대해 a >> b의 값은 a / 2^b의 정수 부분이에요.

음수 aa >> b 값은 구현 정의예요. (대부분의 구현에서 산술 우측 시프트를 수행해서 결과가 계속 음수로 남아요.) (until C++20)

a << b의 값은 a * 2^b와 mod 2^N으로 합동인 유일한 값이에요. N은 반환 타입의 비트 수예요 (즉 비트 좌측 시프트가 수행되고 밀려나온 비트는 버려져요). a >> b의 값은 음의 무한대 쪽으로 반올림된 a / 2^b예요 (다시 말해 부호 있는 a에서 우측 시프트는 산술 우측 시프트예요). (since C++20)

결과의 타입은 lhs의 타입이에요.

오버로드 (시프트)

사용자 정의 연산자와의 오버로드 결정에서, 승격된 정수 타입의 모든 쌍 L, R에 대해 다음 시그니처가 참여해요:

L operator<<(L, R)
L operator>>(L, R)
#include <iostream>

enum { ONE = 1, TWO = 2 };

int main()
{
    std::cout << std::hex << std::showbase;
    char c = 0x10;
    unsigned long long ull = 0x123;
    std::cout << "0x123 << 1 = " << (ull << 1) << "\n"
                 "0x123 << 63 = " << (ull << 63) << "\n" // overflow in unsigned
                 "0x10 << 10 = " << (c << 10) << '\n';   // char is promoted to int
    long long ll = -1000;
    std::cout << std::dec << "-1000 >> 1 = " << (ll >> ONE) << '\n';
}

출력:

0x123 << 1 = 0x246
0x123 << 63 = 0x8000000000000000
0x10 << 10 = 0x4000
-1000 >> 1 = -500

0x123 << 63은 부호 없는 타입이라 오버플로가 허용되고 mod 2^64로 감겨 0x8000...이 돼요. charc는 정수 승격으로 int가 돼서 0x10 << 100x4000이 돼요. 음수 -1000 >> 1은 구현마다 다를 수 있지만 대부분 산술 시프트라 -500이 돼요. (C++20부터는 -500이 보장돼요.)

표준 라이브러리

많은 표준 라이브러리 타입에 대해 산술 연산자가 오버로드되어 있어요.

  • 단항 산술 연산자: std::chrono::durationoperator+/operator-, std::complex의 단항 연산자, std::valarray의 요소별 단항 연산자.
  • 가산 연산자: 시간점(time point) 연산, duration 산술, year_month_day의 월·년 더하기/빼기, 문자열 연결(operator+), std::reverse_iterator/std::move_iterator의 증감, std::complex 산술, std::valarray 이항 연산자.
  • 곱셈 연산자: duration 산술, std::complex 산술, std::valarray 이항 연산자.
  • 비트 논리 연산자: std::bitset&/|/^/~, valarray의 비트 연산자.
  • 비트 시프트 연산자: std::valarray, std::bitset의 시프트.
  • 스트림 삽입·추출 연산자: 표준 라이브러리 전반에서 비트 시프트 연산자(<<, >>)는 흔히 I/O 스트림(std::ios_base& 또는 그 파생 클래스)을 왼쪽 피연산자이자 반환 타입으로 오버로드해요. 이런 연산자를 스트림 삽입(insertion), 스트림 추출(extraction) 연산자라고 불러요. 예: std::basic_istreamoperator>>, std::basic_ostreamoperator<<, std::complex/std::bitset/문자열의 직렬화, 난수 엔진·분포의 스트림 입출력.

결함 보고 (Defect reports)

다음은 이전에 발행된 C++ 표준에 소급 적용된, 동작을 바꾸는 결함 보고예요.

DR 적용 대상 발행 당시 동작 올바른 동작
CWG 614 C++98 정수 나눗셈의 대수 몫이 구현 정의 방향으로 반올림됨 몫은 0을 향해 잘림 (소수 부분은 버려짐)
CWG 1450 C++98 a / b의 결과가 결과 타입으로 표현 불가능하면 불특정이었음 이 경우 a / ba % b 둘 다 정의되지 않음
CWG 1457 C++98 양의 부호 있는 값의 맨 왼쪽 1비트를 부호 비트로 시프트하는 동작이 정의되지 않았음 잘 정의되게 함
CWG 1504 C++98 배열 요소의 기반 클래스 부분객체에 대한 포인터를 포인터 산술에 쓸 수 있었음 이 경우 동작은 정의되지 않음
CWG 1515 C++98 unsigned로 선언된 정수만 mod 2^n 산술 법칙을 따라야 했음 모든 부호 없는 정수에 적용됨
CWG 1642 C++98 산술 연산자가 피연산자를 lvalue로 허용했음 일부 피연산자는 rvalue여야 함
CWG 1865 C++98 CWG 1504 해결로 배열 요소 포인터에 대한 포인터 산술이, 가리키는 타입과 배열 요소 타입의 cv 한정이 비최상위에서 다르면 정의되지 않게 됨 잘 정의되게 함
CWG 1971 C++98 ~ 모호성 해결 규칙이 ~X(0) 같은 경우에 적용되는지 불명확했음 그런 경우에 적용됨
CWG 2419 C++98 비배열 객체에 대한 포인터는 &로 얻은 경우에만 크기 1 배열의 첫 요소 포인터로 취급됐음 비배열 객체에 대한 모든 포인터에 적용됨
CWG 2626 C++98 내장 operator~의 결과가 적절한 정의 없이 단순히 '1의 보수'였음 결과를 2진 표현 기준으로 서술함
CWG 2724 C++20 산술 우측 시프트의 반올림 방향이 불명확했음 명확히 함
CWG 2853 C++98 객체의 끝 뒤 포인터를 정수와 더하거나 뺄 수 없었음 할 수 있음

더 알아보기 (Learn more)

  • 연산자 우선순위 — 각 연산자의 우선순위와 결합성을 한눈에 정리한 표예요.
  • 연산자 오버로딩 — 사용자 정의 타입에서 연산자를 재정의하는 방법이에요.
  • std::div, std::fmod, std::remainder — 정수·부동소수점 나머지를 더 안전하게 다루는 라이브러리 함수예요.