이동 생성자
이동 생성자 (Move constructor)
객체를 함수에 전달하거나 함수에서 반환할 때, 매번 복사하지 않고 "이동"만 하고 싶을 때가 있어요. 이때 작동하는 게 이동 생성자(move constructor)예요. 복사와 이동이 어떻게 다른지 살펴볼게요.
출처: cppreference
본문
이동 생성자는 같은 클래스 타입의 인자로 호출할 수 있고 인자의 내용을 복사하면서 인자를 변경할 수도 있는(수정할 수 있는) 생성자예요.
문법
class-name( parameter-list ); (1)
class-name( parameter-list ) function-body (2)
class-name( single-parameter-list ) = default; (3)
class-name( parameter-list ) = delete; (4)
class-name :: class-name( parameter-list ) function-body (5)
class-name :: class-name( single-parameter-list ) = default; (6)
class-name— 이동 생성자를 선언하는 클래스.parameter-list— 다음 모든 조건을 만족하는 비어 있지 않은 매개변수 목록. 클래스 타입을T라고 할 때 첫 매개변수가T&&,const T&&,volatile T&&,const volatile T&&중 하나의 타입이고, 다른 매개변수가 없거나 그 매개변수들이 모두 기본 인자를 가짐.single-parameter-list— 정확히 하나의 매개변수만 있고T&&,const T&&,volatile T&&,const volatile T&&중 하나의 타입이며 기본 인자가 없는 목록.function-body— 이동 생성자의 함수 본문.
설명
올바른 예시와 잘못된 예시를 먼저 비교해 볼게요.
struct X
{
X(X&& other); // 이동 생성자
// X(X other); // 오류: 잘못된 매개변수 타입
};
union Y
{
Y(Y&& other, int num = 1); // 여러 매개변수를 가진 이동 생성자
// Y(Y&& other, int num); // 오류: `num`에 기본 인자가 없음
};
이동 생성자는 보통 같은 타입의 rvalue(오른값)로 객체를 초기화할 때 호출돼요. (C++17 이전에는 xvalue 또는 prvalue, C++17부터는 xvalue) 대표적인 경우로는:
- 초기화:
T a = std::move(b);또는T a(std::move(b));— 여기서b는T타입이에요. - 함수 인자 전달:
f(std::move(a));— 여기서a는T타입이고f는void f(T t);예요. - 함수 반환: 이동 생성자를 가진
T타입의a에 대해,T f()같은 함수 안에서return a;를 하는 경우.
초기화자가 prvalue일 때, 이동 생성자 호출은 종종 최적화로 생략되거나(C++17 이전까지), 아예 이루어지지 않아요(C++17부터). 자세한 내용은 복사 생략(copy elision)을 참고하세요.
이동 생성자는 보통 인자가 가진 자원(동적으로 할당된 객체에 대한 포인터, 파일 디스크립터, TCP 소켓, 스레드 핸들 등)을 복사본을 만드는 대신 전달하고, 인자를 유효하지만 값이 불특정한 상태로 남겨요. 이동 생성자는 인자의 수명을 바꾸지 않기 때문에, 나중에 인자에 대해 소멸자가 호출되는 게 일반적이에요. 예를 들어 std::string이나 std::vector에서 이동하면 인자가 비어 있는 상태로 남을 수 있어요. std::unique_ptr 같은 일부 타입에서는 이동된 상태(moved-from state)가 완전히 지정돼요.
암시적으로 선언된 이동 생성자
클래스 타입에 대해 사용자 정의 이동 생성자가 제공되지 않고, 다음이 모두 참이면:
- 사용자 선언 복사 생성자가 없고,
- 사용자 선언 복사 대입 연산자가 없고,
- 사용자 선언 이동 대입 연산자가 없고,
- 사용자 선언 소멸자가 없다면,
컴파일러는 시그니처 T::T(T&&)를 가진 이동 생성자를 그 클래스의 비-explicit 인라인 public 멤버로 선언해요.
클래스는 여러 이동 생성자를 가질 수 있어요. 예를 들어 T::T(const T&&)와 T::T(T&&) 둘 다요. 사용자 정의 이동 생성자가 있더라도, default 키워드로 암시적으로 선언된 이동 생성자의 생성을 강제할 수 있어요.
암시적으로 선언된(또는 첫 선언에서 default된) 이동 생성자는 동적 예외 사양(C++17 이전까지) / noexcept 사양(C++17부터)에서 설명하는 예외 사양을 가져요.
암시적으로 정의된 이동 생성자
암시적으로 선언된 이동 생성자가 삭제되지도 않고 자명(trivial)하지도 않으면, ODR-use되거나 상수 평가에 필요할 때 컴파일러가 그것을 정의해요(함수 본문을 생성하고 컴파일). 공용체 타입의 경우, 암시적으로 정의된 이동 생성자는 객체 표현(object representation)을 복사해요(std::memmove와 같이). 공용체가 아닌 클래스 타입의 경우, 이동 생성자는 객체의 직접 기본 하위객체와 멤버 하위객체에 대해, 초기화 순서대로 xvalue 인자를 사용한 직접 초기화로 전체 멤버별 이동(full member-wise move)을 수행해요. 참조 타입의 각 비정적 데이터 멤버에 대해, 이동 생성자는 소스 참조가 바인딩된 것과 같은 객체나 함수에 참조를 바인딩해요.
이것이 constexpr 생성자(C++23 이전까지) / constexpr 함수(C++23부터)의 요구사항을 만족하면, 생성된 이동 생성자는 constexpr이에요.
삭제된 이동 생성자
클래스 T의 암시적으로 선언되거나 명시적으로 default된 이동 생성자는, T가 클래스 타입 M(또는 그 다차원 배열)의 잠재적으로 구성된 하위객체를 가져서 다음 중 하나라도 만족하면 삭제된 것으로 정의돼요:
M이 복사 생성자에서 삭제되었거나 접근할 수 없는 소멸자를 가질 때, 또는M의 이동 생성자를 찾기 위한 오버로드 해석이- 유용한 후보를 만들지 못하거나, 하위객체가 variant 멤버인 경우 자명하지 않은 함수를 선택할 때.
그런 생성자는 오버로드 해석에서 무시돼요(그렇지 않으면 rvalue로부터의 복사 초기화를 막을 테니까요).
자명한 이동 생성자 (Trivial move constructor)
클래스 T의 이동 생성자는 다음이 모두 참이면 자명(trivial)해요:
- 사용자가 제공한 것이 아니고(즉 암시적으로 정의되거나 default된 것),
T에 가상 멤버 함수가 없고,T에 가상 기본 클래스가 없고,T의 각 직접 기본 클래스에 대해 선택된 이동 생성자가 자명하고,T의 각 클래스 타입(또는 클래스 타입 배열) 비정적 멤버에 대해 선택된 이동 생성자가 자명할 때.
자명한 이동 생성자는 자명한 복사 생성자와 같은 동작을 수행하는 생성자예요. 즉 std::memmove처럼 객체 표현의 복사본을 만드는 것이에요. C 언어와 호환되는 모든 데이터 타입은 자명하게 이동 가능(trivially movable)해요.
적격 이동 생성자 (Eligible move constructor)
이동 생성자가 삭제되지 않으면 적격(eligible)이에요. (C++20 이전까지) 이동 생성자는 다음 조건이 모두 만족되면 적격이에요: 삭제되지 않았고, 연관 제약(있다면)이 만족되고, 연관 제약이 만족되는 다른 이동 생성자 중 더 제한적인 것이 없을 때. (C++20부터)
적격 이동 생성자의 자명성은 클래스가 implicit-lifetime 타입인지, 그리고 trivially copyable 타입인지 결정해요.
참고 사항 (Notes)
강한 예외 보장(strong exception guarantee)을 가능하게 하려면, 사용자 정의 이동 생성자는 예외를 던지지 않아야 해요. 예를 들어 std::vector는 요소를 재배치할 때 이동과 복사 중 무엇을 선택할지 std::move_if_noexcept에 의존해요.
복사 및 이동 생성자가 모두 제공되고 다른 생성자가 viable하지 않으면, 오버로드 해석은 인자가 같은 타입의 rvalue일 때 이동 생성자를 선택해요(std::move의 결과 같은 xvalue 또는 C++17 이전까지는 이름 없는 임시 같은 prvalue). 인자가 lvalue(이름 있는 객체 또는 lvalue 참조를 반환하는 함수/연산자)일 때는 복사 생성자를 선택해요. 복사 생성자만 제공되면 모든 인자 범주가 그것을 선택해요(const 참조를 받는 한, rvalue는 const 참조에 바인딩될 수 있으므로). 덕분에 이동이 불가능할 때 복사가 이동의 대체(fallback)가 돼요.
예제
#include <iomanip>
#include <iostream>
#include <string>
#include <utility>
struct A
{
std::string s;
int k;
A() : s("test"), k(-1) {}
A(const A& o) : s(o.s), k(o.k) { std::cout << "move failed!\n"; }
A(A&& o) noexcept :
s(std::move(o.s)), // 클래스 타입 멤버의 명시적 이동
k(std::exchange(o.k, 0)) // 비클래스 타입 멤버의 명시적 이동
{}
};
A f(A a)
{
return a;
}
struct B : A
{
std::string s2;
int n;
// 암시적 이동 생성자 B::(B&&)
// A의 이동 생성자 호출
// s2의 이동 생성자 호출
// 그리고 n의 비트별 복사 수행
};
struct C : B
{
~C() {} // 소멸자가 암시적 이동 생성자 C::(C&&)를 막는다
};
struct D : B
{
D() {}
~D() {} // 소멸자가 암시적 이동 생성자 D::(D&&)를 막는다
D(D&&) = default; // 어쨌든 이동 생성자를 강제한다
};
int main()
{
std::cout << "Trying to move A\n";
A a1 = f(A()); // 값으로 반환하면 함수 매개변수로부터 대상 객체를 이동 생성
//
std::cout << "Before move, a1.s = " << std::quoted(a1.s)
<< " a1.k = " << a1.k << '\n';
A a2 = std::move(a1); // xvalue에서 이동 생성
std::cout << "After move, a1.s = " << std::quoted(a1.s)
<< " a1.k = " << a1.k << '\n';
std::cout << "\nTrying to move B\n";
B b1;
std::cout << "Before move, b1.s = " << std::quoted(b1.s) << "\n";
B b2 = std::move(b1); // 암시적 이동 생성자 호출
std::cout << "After move, b1.s = " << std::quoted(b1.s) << "\n";
std::cout << "\nTrying to move C\n";
C c1;
C c2 = std::move(c1); // 복사 생성자 호출
std::cout << "\nTrying to move D\n";
D d1;
D d2 = std::move(d1);
}
출력:
Trying to move A
Before move, a1.s = "test" a1.k = -1
After move, a1.s = "" a1.k = 0
Trying to move B
Before move, b1.s = "test"
After move, b1.s = ""
Trying to move C
move failed!
Trying to move D
A는 이동 생성자에 noexcept를 붙이고 각 멤버를 명시적으로 이동해서, 이동 후 a1.s는 비워지고 a1.k는 0이 되는 걸 볼 수 있어요. C처럼 소멸자가 있으면 암시적 이동 생성자가 막혀서 "move failed!(" 복사 생성자가 대신 호출되고, D는 = default로 강제해서 이동을 쓰는 걸 확인할 수 있어요.
결함 보고 (Defect reports)
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1353 | C++11 | defaulted 이동 생성자가 삭제된 것으로 정의되는 조건이 다차원 배열 타입을 고려하지 않음 | 이 타입들도 고려함 |
| CWG 1402 | C++11 | 자명하지 않은 복사 생성자를 호출할 defaulted 이동 생성자는 삭제된 것으로 정의됨; 삭제된 defaulted 이동 생성자는 오버로드 해석에 계속 참여함 | 그런 복사 생성자 호출 허용; 오버로드 해석에서 무시됨 |
| CWG 1491 | C++11 | rvalue 참조 타입의 비정적 데이터 멤버를 가진 클래스의 defaulted 이동 생성자는 삭제된 것으로 정의됨 | 이 경우 삭제되지 않음 |
| CWG 2094 | C++11 | volatile 하위객체가 defaulted 이동 생성자를 자명하지 않게 만들었음 (CWG 이슈 496) | 자명성에 영향 없음 |
| CWG 2595 | C++20 | 더 제한적이지만 연관 제약을 만족하지 않는 다른 이동 생성자가 있으면 이동 생성자가 적격이 아니었음 | 이 경우 적격일 수 있음 |