16. 새로운 C++(1) - Move Semantics과 Rvalue 참조(이동 대입)

WanJu Kim·2022년 12월 21일
0

C++

목록 보기
74/81

예전에 했던 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;
profile
Question, Think, Select

0개의 댓글