할당 연산자

할당 연산자 (Assignment operators)

할당 연산자는 객체의 값을 바꿔요. 단순 할당(=)부터 복합 할당(+=, *= 등)까지, 값이 어떻게 갱신되는지를 정리하는 주제예요.

출처: cppreference - Assignment operators

본문

연산자 이름 문법 오버로드 가능 클래스 안 프로토타입 예 (T) 클래스 밖 프로토타입 예 (T)
단순 할당 a = b T& T::operator=(const T2& b); N/A
덧셈 할당 a += b T& T::operator+=(const T2& b); T& operator+=(T& a, const T2& b);
뺄셈 할당 a -= b T& T::operator-=(const T2& b); T& operator-=(T& a, const T2& b);
곱셈 할당 a *= b T& T::operator*=(const T2& b); T& operator*=(T& a, const T2& b);
나눗셈 할당 a /= b T& T::operator/=(const T2& b); T& operator/=(T& a, const T2& b);
나머지 할당 a %= b T& T::operator%=(const T2& b); T& operator%=(T& a, const T2& b);
비트 AND 할당 a &= b T& T::operator&=(const T2& b); T& operator&=(T& a, const T2& b);
비트 OR 할당 a |= b T& T::operator|=(const T2& b); T& operator|=(T& a, const T2& b);
비트 XOR 할당 a ^= b T& T::operator^=(const T2& b); T& operator^=(T& a, const T2& b);
비트 좌측 시프트 할당 a <<= b T& T::operator<<=(const T2& b); T& operator<<=(T& a, const T2& b);
비트 우측 시프트 할당 a >>= b T& T::operator>>=(const T2& b); T& operator>>=(T& a, const T2& b);

참고:

  • 모든 내장 할당 연산자는 *this를 돌려줘요. 대부분의 사용자 정의 오버로드도 내장처럼 쓰일 수 있도록 *this를 돌려줘요. 다만 사용자 정의 오버로드에서는 반환 타입으로 어떤 타입이든(void 포함) 쓸 수 있어요.
  • T2T를 포함해 어떤 타입이든 될 수 있어요.

정의 (Definitions)

**복사 할당(copy assignment)**은 객체 a의 내용을 b의 내용의 복사본으로 대체해요 (b는 수정되지 않아요). 클래스 타입에 대해서는 특수 멤버 함수로 수행되는데, 복사 할당 연산자 페이지에서 설명해요.

**이동 할당(move assignment)**은 가능하면 복사를 피하면서 객체 a의 내용을 b의 내용으로 대체해요 (b는 수정될 수 있어요). 클래스 타입에 대해서는 특수 멤버 함수로 수행되는데, 이동 할당 연산자 페이지에서 설명해요. (C++11 이후)

비클래스 타입에 대해서는 복사 할당과 이동 할당을 구분할 수 없고, 이를 **직접 할당(direct assignment)**이라고 불러요.

**복합 할당(compound assignment)**은 객체 a의 내용을 a의 이전 값과 b의 값 사이의 이항 연산 결과로 대체해요.

할당 연산자 문법

할당 표현식은 다음 형태를 가져요:

형태 설명
target-expr = new-value (1) 단순 할당 표현식
target-expr op = new-value (2) 복합 할당 표현식
  • target-expr — 할당될 표현식 (할당 표현식보다 우선순위가 높아야 해요)
  • op*=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |= 중 하나
  • new-value — 대상에 할당할 표현식(until C++11)/이니셜라이저 절(initializer clause, since C++11). new-value는 콤마 표현식이 될 수 없어요 (우선순위가 더 낮기 때문이에요).

new-value가 표현식이 아니면, 할당 표현식은 오버로드된 복합 할당 연산자와 절대 일치하지 않아요. (C++11 이후)

내장 단순 할당 연산자

내장 단순 할당에서 target-expr은 수정 가능한(modifiable) lvalue여야 해요.

target-expr이 가리키는 객체는 그 값을 new-value의 결과로 대체해 수정돼요. 가리키는 객체가 정수 타입 T이고 new-value의 결과가 해당하는 부호 있는/부호 없는 정수 타입이면, 객체의 값은 new-value의 결과와 같은 값 표현을 가진 타입 T의 값으로 대체돼요.

내장 단순 할당의 결과는 target-expr을 가리키는, target-expr 타입의 lvalue예요. target-expr이 비트필드면 결과도 비트필드예요.

표현식으로부터의 할당 (Assignment from an expression)

new-value가 표현식이면 target-expr의 cv 비한정 타입으로 암시적으로 변환돼요. target-expr이 표현식의 값을 표현할 수 없는 비트필드라면, 비트필드의 결과 값은 구현 정의예요.

target-exprnew-value가 겹치는 객체를 가리키면 동작이 정의되지 않아요. (겹침이 정확하고 타입이 같은 경우는 예외예요.)

target-expr의 타입이 volatile로 한정되어 있으면, (가능하면 괄호로 감싼) 할당 표현식이 무시된 값 표현식(discarded-value expression)이나 평가되지 않는 피연산자가 아닌 한, 그 할당은 폐기 예정(deprecated)이에요. (C++20 이후)

비표현식 이니셜라이저 절로부터의 할당

new-value가 표현식이 아니어도 되는 경우는 다음뿐이에요:

  • target-expr이 스칼라 타입 T이고 new-value가 비어 있거나 요소가 하나뿐. 이 경우 발명된 변수 tT t = new-value로 선언·초기화한다고 할 때, x = new-value의 의미는 x = t예요.
  • target-expr이 클래스 타입. 이 경우 new-value가 오버로드 결정으로 선택된 할당 연산자 함수에 인자로 전달돼요.
#include <complex>

std::complex<double> z;
z = {1, 2};  // meaning z.operator=({1, 2})
z += {1, 2}; // meaning z.operator+=({1, 2})

int a, b;
a = b = {1}; // meaning a = b = 1;
a = {1} = b; // syntax error

(C++11 이후)

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

T*& operator=(T*&, T*);
T*volatile & operator=(T*volatile &, T*);

모든 열거형이나 멤버 포인터 타입 T(선택적으로 volatile 한정)에 대해 다음 시그니처가 참여해요:

T& operator=(T&, T);

모든 쌍 A1, A2에 대해 (A1은 산술 타입(선택적으로 volatile 한정), A2는 승격된 산술 타입) 다음 시그니처가 참여해요:

A1& operator=(A1&, A2);

내장 복합 할당 연산자

모든 내장 복합 할당 표현식 target-expr op= new-value의 동작은, target-expr한 번만 평가된다는 점을 제외하면 target-expr = target-expr op new-value 표현식의 동작과 정확히 같아요.

내장 단순 할당 연산자의 target-expr·new-value에 대한 요구사항도 적용돼요. 추가로:

  • +=-=에서는 target-expr의 타입이 산술 타입이거나 (가능하면 cv 한정된) 완전히 정의된 객체 타입에 대한 포인터여야 해요.
  • 다른 모든 복합 할당 연산자에서는 target-expr의 타입이 산술 타입이어야 해요.

사용자 정의 연산자와의 오버로드 결정에서, 모든 쌍 A1, A2에 대해 (A1은 산술 타입(선택적으로 volatile 한정), A2는 승격된 산술 타입) 다음 시그니처가 참여해요:

A1& operator*=(A1&, A2);
A1& operator/=(A1&, A2);
A1& operator+=(A1&, A2);
A1& operator-=(A1&, A2);

모든 쌍 I1, I2에 대해 (I1은 정수 타입(선택적으로 volatile 한정), I2는 승격된 정수 타입) 다음 시그니처가 참여해요:

I1& operator%=(I1&, I2);
I1& operator<<=(I1&, I2);
I1& operator>>=(I1&, I2);
I1& operator&=(I1&, I2);
I1& operator^=(I1&, I2);
I1& operator|=(I1&, I2);

모든 (선택적으로 cv 한정된) 객체 타입 T에 대해 다음 시그니처가 참여해요:

T*& operator+=(T*&, std::ptrdiff_t);
T*& operator-=(T*&, std::ptrdiff_t);
T*volatile & operator+=(T*volatile &, std::ptrdiff_t);
T*volatile & operator-=(T*volatile &, std::ptrdiff_t);

예제

할당 연산자가 다양한 변환과 함께 어떻게 동작하는지 볼게요.

#include <iostream>

int main()
{
    int n = 0;        // not an assignment

    n = 1;            // direct assignment
    std::cout << n << ' ';

    n = {};           // zero-initialization, then assignment
    std::cout << n << ' ';

    n = 'a';          // integral promotion, then assignment
    std::cout << n << ' ';

    n = {'b'};        // explicit cast, then assignment
    std::cout << n << ' ';

    n = 1.0;          // floating-point conversion, then assignment
    std::cout << n << ' ';

//  n = {1.0};        // compiler error (narrowing conversion)

    int& r = n;       // not an assignment
    r = 2;            // assignment through reference
    std::cout << n << ' ';

    int* p;
    p = &n;           // direct assignment
    p = nullptr;      // null-pointer conversion, then assignment
    std::cout << p << ' ';

    struct { int a; std::string s; } obj;
    obj = {1, "abc"}; // assignment from a braced-init-list
    std::cout << obj.a << ':' << obj.s << '\n';
}

가능한 출력:

1 0 97 98 1 2 (nil) 1:abc

여기서 주목할 점은 n = {}는 0으로 초기화 후 할당이라 0이 출력되고, n = 'a'는 정수 승격으로 97, n = {'b'}는 명시적 캐스트로 98이 나와요. n = {1.0}은 수렴 변환(narrowing conversion)이라 컴파일 오류예요. 마지막은 중괄호 이니셜라이저 목록으로 구조체에 할당하는 모습이에요.

결함 보고 (Defect reports)

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

DR 적용 대상 발행 당시 동작 올바른 동작
CWG 1527 C++11 클래스 타입 객체에 대한 할당에서, 오른쪽 피연산자가 사용자 정의 할당 연산자로 할당이 정의된 경우에만 이니셜라이저 목록일 수 있었음 사용자 정의 할당 제약 제거
CWG 1538 C++11 E1={E2}E1=T(E2)(TE1의 타입)와 같았고, 이는 C 스타일 캐스트를 도입했음 E1=T{E2}와 같음
CWG 2654 C++20 volatile 한정 타입의 복합 할당 연산자가 일관성 없이 폐기 예정이었음 어느 것도 폐기 예정이 아님
CWG 2768 C++11 스칼라 값에 대한 비표현식 이니셜라이저 절로부터의 할당이 직접 목록 초기화를 수행함 대신 복사 목록 초기화 수행
CWG 2901 C++98 int lvalue를 통해 unsigned int 객체에 할당되는 값이 불명확했음 명확히 함
P2327R1 C++20 일부 플랫폼에서 유용한데도 volatile 타입의 비트 복합 할당 연산자가 폐기 예정이었음 폐기 예정이 아님

더 알아보기 (Learn more)

  • 연산자 우선순위 — 각 연산자의 우선순위와 결합성을 정리한 표예요.
  • 연산자 오버로딩 — 사용자 정의 타입에서 연산자를 재정의하는 방법이에요.
  • 복사 할당 연산자, 이동 할당 연산자 — 클래스 타입에서의 특수 할당 멤버 함수를 다루는 주제예요.