복사 대입 연산자

복사 대입 연산자 (Copy assignment operator)

한 객체의 내용을 다른 객체에 "복사해서 담아" 주고 싶을 때 쓰는 게 복사 대입 연산자예요. 같은 클래스 타입의 인자로 호출될 수 있고, 이름이 operator=인 비템플릿 비정적 멤버 함수가 바로 그것이에요. 인자의 내용을 복사하되 인자 자체는 바꾸지 않죠.

출처: cppreference

본문

복사 대입 연산자는 같은 클래스 타입의 인자로 호출될 수 있고 인자의 내용을 복사하되 인자를 변경하지 않는, 이름이 operator=인 비템플릿 비정적 멤버 함수예요.

구문 (Syntax)

형식적인 복사 대입 연산자 구문에 대해서는 함수 선언을 참고해요. 아래 구문 목록은 유효한 복사 대입 연산자 구문의 일부만 보여줘요.

구문 번호
return-type operator=(parameter-list); (1)
return-type operator=(parameter-list) function-body (2)
return-type operator=(parameter-list-no-default) = default; (3) (since C++11)
return-type operator=(parameter-list) = delete; (4) (since C++11)
return-type class-name::operator=(parameter-list) function-body (5)
return-type class-name::operator=(parameter-list-no-default) = default; (6) (since C++11)
항목 - 설명
class-name - 복사 대입 연산자를 선언하는 클래스로, 아래 설명에서 클래스 타입을 T로 나타냄
parameter-list - T, T&, const T&, volatile T&, const volatile T& 타입 중 하나인 단 하나의 매개변수 목록
parameter-list-no-default - 위 타입 중 하나이고 기본 인자를 갖지 않는 단 하나의 매개변수 목록
function-body - 복사 대입 연산자의 함수 본문
return-type - 임의 타입이지만, 대입을 연쇄할 수 있도록 T&가 선호됨

설명 (Explanation)

  1. 클래스 정의 안에서의 복사 대입 연산자 선언. 2-4) 클래스 정의 안에서의 복사 대입 연산자 정의.
  2. 복사 대입 연산자가 명시적으로 defaulted됨.
  3. 복사 대입 연산자가 deleted됨. 5,6) 클래스 정의 밖에서의 복사 대입 연산자 정의 (클래스는 (1)의 선언을 포함해야 함).
  4. 복사 대입 연산자가 명시적으로 defaulted됨.
struct X
{
    X& operator=(X& other);     // copy assignment operator
    X operator=(X other);       // pass-by-value is allowed
//  X operator=(const X other); // Error: incorrect parameter type
};

union Y
{
    // copy assignment operators can have syntaxes not listed above,
    // as long as they follow the general function declaration syntax
    // and do not violate the restrictions listed above
    auto operator=(Y& other) -> Y&;       // OK: trailing return type
    Y& operator=(this Y& self, Y& other); // OK: explicit object parameter
//  Y& operator=(Y&, int num = 1);        // Error: has other non-object parameters
};

복사 대입 연산자는 오버로드 해석이 선택할 때마다, 예를 들어 객체가 대입 표현식의 왼쪽에 나타날 때 호출돼요.

암시적으로 선언된 복사 대입 연산자 (Implicitly-declared copy assignment operator)

클래스 타입에 사용자 정의 복사 대입 연산자가 없으면, 컴파일러는 항상 클래스의 inline public 멤버로 하나를 선언해요. 이 암시적으로 선언된 복사 대입 연산자는 다음이 모두 참이면 T& T::operator=(const T&) 형태를 가져요:

  • T의 각 직접 기반 BB, const B&, const volatile B& 타입 매개변수를 갖는 복사 대입 연산자를 가짐
  • T의 클래스 타입 또는 클래스 타입 배열인 각 비정적 데이터 멤버 MM, const M&, const volatile M& 타입 매개변수를 갖는 복사 대입 연산자를 가짐

그 외에는 암시적으로 선언된 복사 대입 연산자가 T& T::operator=(T&)로 선언돼요.

이 규칙 때문에 암시적으로 선언된 복사 대입 연산자는 volatile lvalue 인자에 바인딩할 수 없어요.

클래스는 여러 복사 대입 연산자를 가질 수 있어요, 예를 들어 T& T::operator=(T&)T& T::operator=(T)를 모두 가질 수 있죠. 사용자 정의 복사 대입 연산자가 있어도 default 키워드로 암시적으로 선언된 복사 대입 연산자의 생성을 강제할 수 있어요.(since C++11)

암시적으로 선언된 (또는 첫 번째 선언에서 defaulted된) 복사 대입 연산자는 dynamic exception specification(until C++17)noexcept specification(since C++17)에 설명된 예외 명세를 가져요.

복사 대입 연산자는 모든 클래스에 항상 선언되기 때문에, 기반 클래스의 대입 연산자는 항상 가려져요(hidden). using-declaration으로 기반 클래스에서 대입 연산자를 가져오고 그 인자 타입이 파생 클래스의 암시적 대입 연산자의 인자 타입과 같을 수 있다면, 그 using-declaration도 암시적 선언에 가려져요.

암시적으로 정의된 복사 대입 연산자 (Implicitly-defined copy assignment operator)

암시적으로 선언된 복사 대입 연산자가 deleted도 아니고 trivial도 아니면, odr-used되거나 상수 평가(since C++14)에 필요할 때 컴파일러가 정의해요(즉 함수 본문이 생성되고 컴파일됨). 유니온 타입의 경우 암시적으로 정의된 복사 대입은 객체 표현(object representation)을 복사해요(std::memmove처럼). 비유니온 클래스 타입의 경우, 이 연산자는 객체의 직접 기반과 비정적 데이터 멤버를 초기화 순서대로 멤버별 복사 대입(member-wise copy assignment)을 수행해요. 스칼라에는 내장 대입을, 배열에는 멤버별 복사 대입을, 클래스 타입에는 (비가상으로 호출되는) 복사 대입 연산자를 사용해요.

참고 (since)
클래스 T의 암시적으로 정의된 복사 대입 연산자는, T가 literal 타입이고 각 직접 기반 클래스 부분 객체를 복사하도록 선택된 대입 연산자가 constexpr 함수이며, T의 클래스 타입(또는 그 배열)인 각 비정적 데이터 멤버를 복사하도록 선택된 대입 연산자가 constexpr 함수이면 constexpr이다. (since C++14)(until C++23)
클래스 T의 암시적으로 정의된 복사 대입 연산자는 constexpr이다. (since C++23)
참고 (since)
T에 사용자 선언 소멸자 또는 사용자 선언 복사 생성자가 있으면 암시적으로 정의된 복사 대입 연산자의 생성은 deprecated된다. (since C++11)

삭제된 복사 대입 연산자 (Deleted copy assignment operator)

클래스 T에 대한 암시적으로 선언되거나 명시적으로 defaulted된(since C++11) 복사 대입 연산자는 다음 조건 중 하나라도 만족하면 undefined(until C++11)defined as deleted(since C++11)돼요:

  • T가 const 한정된 비클래스 타입(또는 그 다차원 배열)의 비정적 데이터 멤버를 갖는 경우
  • T가 참조 타입의 비정적 데이터 멤버를 갖는 경우
  • T가 클래스 타입 M(또는 그 다차원 배열)의 잠재적으로 구성되는 부분 객체(subobject)를 갖는데, M의 복사 대입 연산자를 찾기 위한 오버로드 해석이
    • 사용 가능한 후보를 만들지 못하거나,
    • 부분 객체가 variant 멤버인 경우 비trivial 함수를 선택하는 경우
참고 (since)
T가 이동 생성자 또는 이동 대입 연산자를 선언하면 클래스 T의 암시적으로 선언된 복사 대입 연산자는 deleted로 정의된다. (since C++11)

trivial 복사 대입 연산자 (Trivial copy assignment operator)

클래스 T의 복사 대입 연산자는 다음이 모두 참이면 trivial이에요:

  • 사용자 제공(user-provided)이 아님 (즉 암시적으로 정의되거나 defaulted됨)
  • T에 가상 멤버 함수가 없음
  • T에 가상 기반 클래스가 없음
  • T의 각 직접 기반에 대해 선택된 복사 대입 연산자가 trivial임
  • T의 각 비정적 클래스 타입(또는 클래스 타입 배열) 멤버에 대해 선택된 복사 대입 연산자가 trivial임

trivial 복사 대입 연산자는 std::memmove처럼 객체 표현의 복사본을 만들어요. C 언어와 호환되는 모든 데이터 타입(POD 타입)은 trivially copy-assignable이에요.

적격 복사 대입 연산자 (Eligible copy assignment operator)

정의 (since)
복사 대입 연산자는 사용자 선언이거나 암시적으로 선언되고 정의 가능한 경우에 적격(eligible)이다. (until C++11)
복사 대입 연산자는 deleted가 아니면 적격이다. (since C++11)(until C++20)
복사 대입 연산자는 다음 조건이 모두 충족되면 적격이다: deleted가 아님. 연관 제약(있다면)이 만족됨. 연관 제약이 만족되는 복사 대입 연산자 중 더 제한적(constrained)인 것이 없음. (since C++20)

적격 복사 대입 연산자의 trivial 여부는 클래스가 trivially copyable 타입인지 결정해요.

참고 (Notes)

복사 대입과 이동 대입 연산자를 모두 제공하면, 오버로드 해석은 인자가 rvalue(이름 없는 임시 같은 prvalue 또는 std::move의 결과 같은 xvalue)일 때 이동 대입을, 인자가 lvalue(이름 있는 객체 또는 lvalue 참조를 반환하는 함수/연산자)일 때 복사 대입을 선택해요. 복사 대입만 제공하면 모든 인자 범주가 그것을 선택해요(값 또는 const 참조로 인자를 받는 한, rvalue는 const 참조에 바인딩될 수 있으므로), 그래서 이동이 불가능할 때 복사 대입이 이동 대입의 폴백(fallback)이 돼요.

상속 격자(inheritance lattice)에서 둘 이상의 경로로 접근 가능한 가상 기반 클래스 부분 객체가 암시적으로 정의된 복사 대입 연산자에 의해 둘 이상 할당되는지 여부는 불특정이에요(이동 대입에도 동일하게 적용).

사용자 정의 복사 대입 연산자의 기대 동작에 대한 추가 세부사항은 대입 연산자 오버로딩을 참고해요.

예제 (Example)

#include <algorithm>
#include <iostream>
#include <memory>
#include <string>

struct A
{
    int n;
    std::string s1;
    
    A() = default;
    A(A const&) = default;
    
    // user-defined copy assignment (copy-and-swap idiom)
    A& operator=(A other)
    {
        std::cout << "copy assignment of A\n";
        std::swap(n, other.n);
        std::swap(s1, other.s1);
        return *this;
    }
};

struct B : A
{
    std::string s2;
    // implicitly-defined copy assignment
};

struct C
{
    std::unique_ptr<int[]> data;
    std::size_t size;
    
    // user-defined copy assignment (non copy-and-swap idiom)
    // note: copy-and-swap would always reallocate resources
    C& operator=(const C& other)
    {
        if (this != &other) // not a self-assignment
        {
            if (size != other.size) // resource cannot be reused
            {
                data.reset(new int[other.size]);
                size = other.size;
            }
            std::copy(&other.data[0], &other.data[0] + size, &data[0]);
        }
        return *this;
    }
};

int main()
{
    A a1, a2;
    std::cout << "a1 = a2 calls ";
    a1 = a2; // user-defined copy assignment

    B b1, b2;
    b2.s1 = "foo";
    b2.s2 = "bar";
    std::cout << "b1 = b2 calls ";
    b1 = b2; // implicitly-defined copy assignment
    
    std::cout << "b1.s1 = " << b1.s1 << "; b1.s2 = " << b1.s2 << '\n';
}

출력 (Output):

a1 = a2 calls copy assignment of A
b1 = b2 calls copy assignment of A
b1.s1 = foo; b1.s2 = bar

A는 사용자 정의 복사 대입(copy-and-swap idiom)을 쓰고, 이를 상속하는 B는 암시적으로 정의된 복사 대입을 써요. 그래서 b1 = b2에서도 기반 클래스인 A의 복사 대입이 먼저 호출되고 그다음 B의 멤버가 복사되죠.

더 알아보기 (Learn more)

  • 변환 생성자 — 암시적 변환을 일으키는 생성자.
  • 복사 생성자 — 같은 타입 객체로부터 초기화하는 생성자.
  • 복사 생략 — 복사·이동 생성이 생략될 수 있는 상황.
  • 기본 생성자, 소멸자, explicit.
  • 복사 초기화·직접 초기화·목록 초기화 등 각 초기화 방식.
  • 이동 대입, 이동 생성자.