기존 복사 생성자를 확인해보자면 아래와 같은 방식으로 작동을 했었다.
//testClass가 정의되어 있다고 가정.
int main(){
testClass test1(1);
testClass test2(test1); <- 복사 생성자 호출됨.
}그런데 만약, 아래와 같은 경우가 발생하면 어떻게 될까?
testClass test3(testClass(1));(즉, 복사 생성(일반 생성) 을 호출하게 된다면?) ⭐ 복사 생성자가 아닌 일반 생성자가 호출되게 된다.
이유는 복사 생략이 발생했기 때문인데, 요즘은 컴파일러가 똑똑해져서 임시로 만들어진 testClass(1) 객체 자체를 test3로 만들고 복사를 생략하게 된다.
그러나 모든 상황에서 이 복사 생략이 일어나는 것은 아니다.
string을 직접 구현했을 때 + 연산자를 오버로딩 했다고 생각을 해보자.
Mystring Mystring::operator +(const Mystring& rhs){
Mystring str; // ⭐임시 객체 생성
...두 string의 길이를 더하는 과정
...새로 만든 임시 객체에 할당
return str;
} 그리고 이런 오버로딩은 다음과 같이 사용하게 된다.
Mystring str3 = str1 + str2; // ⭐객체 복사 생성⭐가 붙은 두번의 생성자가 호출이 되는 것이다. 객체 하나를 만들기 위해서 2개의 객체가 만들어지는 것인데 만약 이 객체들의 크기가 컸다면 굉장한 비효율이 발생했을 것이다. (여기서도 복사 생성 과정에서 임시 객체가 할당한 문자열을 str3에서 다시 할당하여 복사 받는다고 가정한다.)
이러한 비효율성을 극복하기 위해서 나온 것이 바로 이동생성자1
만약 Mystring str3 = str1 + str2; 에서 str1 + str2;이 생성한
임시 객체의 문자열 포인터를 str3이 그냥 받기만 한다면 어떨까?
그러니까 new char[length] 와 같이 새로 할당 받는 과정을 건너뛰고, 주소의 소유권을 넘겨주는 형태로 진행하는 것이다.
그럼 물론 효율적이게 변할 것이다. 이러한 과정을 밟기 위해서는 하나의 해결과제가 남아있다.
바로 임시 객체 소멸 과정에서 발생하는 메모리 반환을 막아야 하는 것이다. (그 메모리는 이제 str3이 소유권을 가지게 될 것이니까.)
이를 막기 위해서는 복사 생성자가 아닌 새로운 생성자가 필요하다.
- str3이 받게 될 인자는 +오퍼레이터 함수의 반환값인 R-value라는 점.
- 매개인자의 값을 직접 수정하기 위해서는 레퍼런스로 받아야 한다는 점.(혹은 포인터)
1번의 문제가 2번의 진행을 막고 있다. (원래 R-value는 레퍼런스 및 포인터 선언이 불가하다.) 이를 진행하기 위해서 필요한 것이 ‘우측값 레퍼런스’이다. R_Value_Reference
우측값 레퍼런스의 연산자는 &&
int && r_val = 3;
const int a =3;
int&& b = a;위와 같은 문법이 가능하게 되는 것이다. 우측값 레퍼런스의 특징은 임시 객체가 바로 소멸되지 않도록 붙들고 있다는 것이다. (즉 바로 소멸하지 않기에 소멸자 호출이 지연됨)
우측값 레퍼런스를 통한 이동 생성자의 구현은 다음과 같이 이루어진다.
MyString::MyString(MyString&& str){
...멤버 함수 복사받는 과정 (길이 / 문자열 포인터)
str.cData = nullptr; (매개 인자의 포인터 널포인터화)
}그런데, 이동 생성자를 사용할 때에는 주의사항이 있다. 만약 vector 에 이동생성자를 넣는다고 해보자. 일반적으로 복사 생성자의 방식으로 진행을 하는 과정에 오류가 발생했다면, 원본은 남아있기 때문에 괜찮다.
그렇지만, 이동 생성자의 경우에는 이동 후에 원본 메모리의 소유권을 잃어버리기에 벡터로 옮겨지지도 않고, 원본이 가지고 있지도 않은 메모리가 붕 떠버리게 되는 것이다.
그렇기에 모든 이동 생성자는 noexcept 라는 키워드를 붙여서예외 처리를 지원하지 않도록 해야 한다. (STL 벡터는 noexcept가 되어 있지 않다면 이동 생성자를 호출하지 않는다.)
move 문법
만약 내가 swap함수를 직접 템플릿으로 구현했다고 생각을 해보자. 아래와 같이 구현을 진행했을 것이다.
template<typename T>
void swap(T &a, T &b){
T tmp(a);
a=b;
b=tmp;
}이건 tmp 임시 객체를 생성하고 tmp에 a를 복사하고, a에 b를 복사하고, b에 a를 복사하는 3번의 복사가 이루어지는 과정이다. 마찬가지로 객체의 크기가 크다면 비효율적일 수밖에 없다.
만약, 데이터가 가리키는 포인터만 간단하게 교환해준다면, 8바이트끼리의 교환으로 훨씬 간편하게 진행될 수 있을 텐데..
문제는 T &a, T &b 가 L-value라는 점이다. 우측값이 아니기에 이동 생성자가 호출되지 않는 것이 문제. 이때 나타나는 것이 move, move_sementic 이다.
#include <utility> 헤더가 필요.
T tmp(std::move(a)); 와 같이 사용하는 문법인데, 이는 좌측값을 우측값처럼 취급되게 해주는 기능이다.
주의 : 좌측값을 우측값으로 타입 변환 시켜줄 뿐 이동생성자를 호출하는 기능은 아니다. (이름 때문에 헷갈리지 말 것.)
a=std::move(b); 를 사용할 때에 주의점은 operator=(T && s) 와 같이 이동대입 연산자를 미리 오버로딩 해두는 것이 좋다는 것.
확인필요 perfect_forwarding 보편적 레퍼언스와 forward부분은 이해가 잘 안됨. 템플릿을 사용할 때, 컴파일러가 판단하에 우측값 레퍼런스와 좌측값 레퍼런스를 구분하지 않는데, forward는 동적캐스팅처럼 그 상황에 맞게 타입 변환을 해준다는 것 같은데 아직 이해하지 못함.
Footnotes
-
이동생성자를 알기 전에 우측값과 좌측값에 대한 이해가 부족하다면 LValue & RValue로 ↩