복사 대입 연산자
복사 대입 연산자 (Copy assignment operator)
a = b; 같은 코드가 실제로 어떤 함수를 호출하는지 생각해 본 적 있나요? 클래스 타입 객체라면 operator=라는 멤버 함수가 동작해요. 그중에서도 인자를 같은 클래스 타입으로 받아 '복사'하는 특별한 멤버 함수를 복사 대입 연산자라고 불러요. 이 페이지에서 그 함수가 언제 선언되고, 어떻게 생성되고, 언제 삭제되며, 언제 trivial한지 규칙 기준으로 정리해 볼게요.
출처: cppreference
본문
복사 대입 연산자는 이름이 operator=인 비템플릿·비정적 멤버 함수로, 같은 클래스 타입의 인자로 호출할 수 있고, 인자를 변경하지 않고 그 내용을 복사해요.
문법
정식 복사 대입 연산자 문법은 함수 선언 문서를 참고해요. 아래 문법 목록은 유효한 복사 대입 연산자 문법의 일부만 보여주는 거예요.
return-type operator=(parameter-list ); (1)
return-type operator=(parameter-list ) function-body (2)
return-type operator=(parameter-list-no-default ) = default; (3) (C++11부터)
return-type operator=(parameter-list ) = delete; (4) (C++11부터)
return-type class-name ::operator=(parameter-list ) function-body (5)
return-type class-name ::operator=(parameter-list-no-default ) = default; (6) (C++11부터)
설명
- 클래스 정의 안에서 복사 대입 연산자 선언. 2-4) 클래스 정의 안에서 복사 대입 연산자 정의.
- 복사 대입 연산자가 명시적으로 default 처리됨.
- 복사 대입 연산자가 삭제(delete)됨. 5,6) 클래스 정의 바깥에서 복사 대입 연산자 정의(클래스에 선언(1)이 있어야 함).
- 복사 대입 연산자가 명시적으로 default 처리됨.
struct X
{
X& operator=(X& other); // 복사 대입 연산자
X operator=(X other); // 값 전달은 허용
// X operator=(const X other); // Error: 잘못된 매개변수 타입
};
union Y
{
// 복사 대입 연산자는 위에 나열되지 않은 문법도 가질 수 있어요.
// 일반 함수 선언 문법을 따르고 위 제약을 어기지만 않으면 되죠
auto operator=(Y& other) -> Y&; // OK: 후행 반환 타입
Y& operator=(this Y& self, Y& other); // OK: 명시적 객체 매개변수
// Y& operator=(Y&, int num = 1); // Error: 다른 비객체 매개변수가 있음
};
복사 대입 연산자는 오버로드 결정에 의해 선택될 때마다 호출돼요. 예를 들어 객체가 대입 표현식의 왼쪽에 나타날 때예요.
암시적으로 선언된 복사 대입 연산자
클래스 타입에 사용자 정의 복사 대입 연산자가 제공되지 않으면, 컴파일러는 항상 클래스의 inline public 멤버로 하나를 선언해요. 이 암시적으로 선언된 복사 대입 연산자는 다음이 모두 참일 때 T& T::operator=(const T&) 형태예요.
T의 각 직접 기반B가, 매개변수가B또는const B&또는const volatile B&인 복사 대입 연산자를 가짐.T의 클래스 타입이거나 클래스 타입 배열인 각 비정적 데이터 멤버M이, 매개변수가M또는const M&또는const volatile M&인 복사 대입 연산자를 가짐.
그렇지 않으면 암시적으로 선언된 복사 대입 연산자는 T& T::operator=(T&)로 선언돼요.
이 규칙 때문에 암시적으로 선언된 복사 대입 연산자는 volatile lvalue 인자에 바인딩할 수 없어요.
클래스는 복사 대입 연산자를 여러 개 가질 수 있어요. 예를 들어 T& T::operator=(T&)와 T& T::operator=(T) 둘 다요. 사용자 정의 복사 대입 연산자가 있으면, 사용자는 default 키워드로 암시적으로 선언된 복사 대입 연산자의 생성을 강제할 수 있어요.(C++11부터)
암시적으로 선언된(또는 첫 선언에서 default 처리된) 복사 대입 연산자는, 동적 예외 명세(dynamic exception specification)(C++17 이전) / noexcept 명세(C++17부터)에 설명된 예외 명세를 가져요.
복사 대입 연산자는 어떤 클래스에도 항상 선언되므로, 기반 클래스의 대입 연산자는 항상 숨겨져요. using 선언으로 기반 클래스의 대입 연산자를 끌어올리고 그 인자 타입이 파생 클래스의 암시적 대입 연산자의 인자 타입과 같을 수 있다면, 그 using 선언도 암시적 선언에 숨겨져요.
암시적으로 정의된 복사 대입 연산자
암시적으로 선언된 복사 대입 연산자가 삭제되지도 trivial하지도 않다면, odr 사용되거나 상수 평가에 필요할 때 컴파일러가 정의해요(함수 본문이 생성·컴파일됨).(C++14부터) 공용체 타입의 경우 암시적으로 정의된 복사 대입은 객체 표현(std::memmove로)을 복사해요. 비공용체 클래스 타입의 경우, 그 연산자는 객체의 직접 기반들과 비정적 데이터 멤버들을 초기화 순서로 멤버별 복사 대입(member-wise copy assignment)해요.
- 스칼라는 내장 대입 사용
- 배열은 멤버별 복사 대입
- 클래스 타입은 복사 대입 연산자 사용(비가상 호출)
삭제된 복사 대입 연산자
클래스 T의 암시적으로 선언된 또는 명시적으로 default 처리된(C++11부터) 복사 대입 연산자는, 다음 조건 중 하나를 만족하면 (C++11 이전에는 undefined / C++11부터는) 삭제된 것으로 정의돼요.
T가 const 한정된 비클래스 타입(또는 그 다차원 배열)의 비정적 데이터 멤버를 가짐.T가 참조 타입의 비정적 데이터 멤버를 가짐.T가,M의 복사 대입 연산자를 찾는 오버로드 결정이- 사용 가능한 후보(candidate)를 만들어내지 못하는, 또는
- 하위 객체가 variant 멤버인 경우 비trivial 함수를 선택하는,
그런 클래스 타입
M(또는 그 다차원 배열)의 잠재적으로 구성된 하위 객체를 가짐.
trivial 복사 대입 연산자
클래스 T의 복사 대입 연산자는 다음이 모두 참이면 trivial이에요.
- 사용자 제공(user-provided)이 아님(즉, 암시적으로 정의되거나 default 처리됨).
T에 가상 멤버 함수가 없음.T에 가상 기반 클래스가 없음.T의 각 직접 기반에 대해 선택된 복사 대입 연산자가 trivial.T의 각 비정적 클래스 타입(또는 클래스 타입 배열) 멤버에 대해 선택된 복사 대입 연산자가 trivial.
trivial 복사 대입 연산자는 std::memmove로 한 것처럼 객체 표현을 복사해요. C 언어와 호환되는 모든 데이터 타입(POD 타입)은 trivial하게 복사 대입 가능해요.
유효한 복사 대입 연산자
복사 대입 연산자의 trivial 여부가 클래스가 trivial하게 복사 가능한 타입(trivially copyable type)인지 결정해요.
주의
복사·이동 대입 연산자가 둘 다 제공되면, 오버로드 결정은 인자가 rvalue(이름 없는 임시 같은 prvalue, std::move 결과 같은 xvalue)면 이동 대입을, 인자가 lvalue(이름 있는 객체나 lvalue 참조를 반환하는 함수·연산자)면 복사 대입을 선택해요. 복사 대입만 제공되면 모든 인자 범주가 그것을 선택해요(값 전달이거나 const 참조로 인자를 받기만 하면, rvalue도 const 참조에 바인딩될 수 있으니까요). 그래서 복사 대입은 이동이 불가능할 때 이동 대입의 폴백이 돼요.
상속 격자 구조에서 여러 경로로 접근 가능한 가상 기반 클래스 하위 객체가 암시적으로 정의된 복사 대입 연산자로 두 번 이상 대입되는지 여부는 지정되지 않아요(이동 대입에도 동일하게 적용돼요).
사용자 정의 복사 대입 연산자의 기대 동작에 대한 추가 세부사항은 대입 연산자 오버로딩 문서를 참고해요.
예시
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
struct A
{
int n;
std::string s1;
A() = default;
A(A const&) = default;
// 사용자 정의 복사 대입 (복사-및-교환 copy-and-swap 관용구)
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;
// 암시적으로 정의된 복사 대입
};
struct C
{
std::unique_ptr<int[]> data;
std::size_t size;
// 사용자 정의 복사 대입 (copy-and-swap이 아닌 관용구)
// 참고: copy-and-swap은 항상 자원을 재할당하게 됨
C& operator=(const C& other)
{
if (this != &other) // 자기 대입이 아님
{
if (size != other.size) // 자원을 재사용할 수 없음
{
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; // 사용자 정의 복사 대입
B b1, b2;
b2.s1 = "foo";
b2.s2 = "bar";
std::cout << "b1 = b2 calls ";
b1 = b2; // 암시적으로 정의된 복사 대입
std::cout << "b1.s1 = " << b1.s1 << "; b1.s2 = " << b1.s2 << '\n';
}
출력:
a1 = a2 calls copy assignment of A
b1 = b2 calls copy assignment of A
b1.s1 = foo; b1.s2 = bar
B b1 = b2에서 암시적으로 정의된 복사 대입이 기반 클래스 A의 사용자 정의 복사 대입까지 호출하는 것(copy assignment of A가 두 번 출력)을 확인할 수 있어요.
더 알아보기
- 복사 생성자, 이동 대입, 이동 생성자와 함께 '특별 멤버 함수' 흐름을 보면 좋아요.
- 복사 생략(copy elision)과 기본 생성자·소멸자의 규칙도 이어서 보면 자연스러워요.
- cppreference의 복사 대입 연산자 원문에서 결함 보고 기록을 더 볼 수 있어요.