예전에 했던 StringBad.cpp를 보자.
#include "StringBad.h"
using namespace std;
int StringBad::num_strings = 0;
StringBad::StringBad()
{
len = 4;
str = new char[4];
strcpy_s(str, 4, "C++");
num_strings++;
cout << num_strings << ": \"" << str << "\" 디폴트 객체 생성\n";
}
StringBad::StringBad(const StringBad& s)
{
len = s.len;
str = new char[len + 1];
strcpy_s(str, len + 1, s.str);
num_strings++;
cout << num_strings << ": \"" << str << "\" 복사 객체 생성\n";
}
StringBad& StringBad::operator=(const StringBad& st)
{
if (this == &st)
return *this;
delete[] str;
len = st.len;
str = new char[len + 1];
strcpy_s(str, len + 1, st.str);
cout << num_strings << ": \"" << str << "\" 대입 연산자\n";
return *this;
}
StringBad StringBad::Rvalue(const StringBad& st)
{
StringBad temp;
temp.len = st.len + st.len;
temp.str = new char[temp.len + 1];
strcpy_s(temp.str, temp.len + 1, st.str);
cout << "StringBad::RValue. 길이 두 배 해줌." << endl;
return temp;
}
StringBad::StringBad(const char* s)
{
len = std::strlen(s);
str = new char[len + 1];
strcpy_s(str, len + 1, s);
num_strings++;
cout << num_strings << ": \"" << str << "\" 객체 생성\n";
}
StringBad::~StringBad()
{
cout << "\"" << str << "\" 객체 파괴, ";
--num_strings;
cout << "남은 객체 수 : " << num_strings << "\n";
delete[] str;
}
std::ostream& operator<<(std::ostream& os, const StringBad& st)
{
os << st.str;
return os;
}
덧붙여 이런 코드를 적으면 어떨까?
StringBad a("a");
cout << "--------------------------" << endl;
StringBad b(a);
cout << "--------------------------" << endl;
StringBad c(a.Rvalue(b));
cout << "--------------------------" << endl;
실행 결과.
여기서 StringBad c(a.Rvalue(b));를 잘 보자. Rvalue() 메서드는 다음과 같았다.
StringBad StringBad::Rvalue(const StringBad& st)
{
StringBad temp; // 임시 객체(디폴트) 생성.
temp.len = st.len + st.len;
temp.str = new char[temp.len + 1];
strcpy_s(temp.str, temp.len + 1, st.str);
cout << "StringBad::RValue. 길이 두 배 해줌." << endl;
return temp;
}
객체 c의 매개변수로 만든 임시 객체를 만들어주었는데, 이것을 삭제하고 다시 복사 생성을 해주었다. 이는 굉장히 비효율적이다. 객체의 크기가 크면 클수록 더 비효율적이다. 이런 문제를 해결할 방법이 있을까? 예를 들어 임시 객체는 어짜피 사라질 거니, 값들의 주소만 살짝 바꾸고 원래 주소는 없애는 거다. 이게 바로 move_semantics의 원리이다. (주소를 move 한다고 받아들이면 될듯.)
다음 이동 생성자를 추가해주었다.
StringBad::StringBad(StringBad&& s)
:len(s.len)
{
str = s.str; // 주소 가로채기.
s.str = nullptr; // 이전 객체가 아무것도 반환 못 하게 함.
s.len = 0;
cout << num_strings << ": \"" << str << "\" 이동 생성자\n";
}
이 함수는 어떤 일을 하는가? 위에서 말한대로 매개변수 객체의 주소만 가로채고, 매개변수의 값들은 '무'로 돌려버린다. 이렇게 주소를 뺏는 것을 pilfering(필퍼링, 좀도둑질)이라 부른다. 이동 '생성자'라 파괴자가 호출이 된다. 만약 s.str = nullptr; 구문을 쓰지 않았더라면, 같은 주소를 두 번 파괴해서 난감했을 것이다. 그리고 특별히 이 클래스 파괴자는 str도 호출하고 있다. nullptr를 호출하는 것도 난감하다. 그래서 nullptr일 때는 리턴하도록 수정했다.
StringBad::~StringBad()
{
if (str)
cout << "\"" << str << "\" 객체 파괴, ";
else
cout << "\"없는\" 객체 파괴, ";
--num_strings;
cout << "남은 객체 수 : " << num_strings << "\n";
delete[] str;
}
이동 생성자는 매개변수에서 &&를 사용했다. 이는 복사 생성자의 lvalue와 구분하기 위함이고, rvalue 참조라는 뜻이다.
복사 대입도 이동 생성자랑 비슷하게 작성하면 된다.
StringBad& StringBad::operator=(StringBad&& st)
{
if (this == &st)
return *this;
delete[] str;
len = st.len;
str = st.str; // 주소 가로채기.
strcpy_s(str, len + 1, st.str);
st.len = 0;
st.str = nullptr;
cout << num_strings << ": \"" << str << "\" 이동 대입 연산자\n";
return *this;
}
만약 이동 생성자와 이동 대입 연산자를 lvalues로 사용하려면 어떻게 해야 하는가? static_cast<> 연산자를 이용해서 객체를 rvalue형으로 바꾸면 가능하다. 또한 <utility> 라이브러리의 std::move() 함수를 이용해도 된다.
StringBad a("a");
cout << "--------------------------" << endl;
StringBad d = std::move(a);
cout << "--------------------------" << endl;