본문 바로가기

C++

[TIL 33장] R-value 레퍼런스( feat. move() ), 이동 생성자

오늘의 TIL 목차 (22. 09. 23)

 

  • R-value 레퍼런스
  • 이동 생성자

r-value 레퍼런스


[ r-value 레퍼런스 ]

: 우항에만 위치할 수 있는, 사용되는 단일 식을 넘어 지속되지 않는 일시적인 값을 의미

[ L-value와 R-value ]
// int iA, iB;

iA = 10; // iA는 L-value, 10은 R-value
=> 10은 임시 객체에 있다가 라인을 벗어나면 소멸됨, 재사용 불가능 (R-value)

iB = iA; // iB, iA는 L-value
=> iA와 iB는 언제든 재사용 가능, 왼쪽에 위치할 수 있음 (L-value)

-

[ R-value reference ]
int& 는 L-value를 참조,
int&&는 R-value를 참조 ( 임시 공간에 할당되어 있음 )

int&& r = 10; // r은 r-value인 10을 참조하는 l-value
int& l = r; // r은 l-value이므로 l-value 레퍼런스인 l이 r을 참조 가능
r += 100; // r은 110, l은 r을 참조함으로 l도 110

■ 기능 / 주의

  • r-value 레퍼런스는 일시적인 값이므로 주소 &를 참조할 수 없다. ( l-value는 참조 가능 )
  • r-value의 종류로는 주소가 없는 개체, 리터럴(문자열 리터럴 제외), 람다식 등이 있다.
    - 문자열 상수 자체는 주소이므로 r-value와 l-value 둘 다 참조 가능하다.
  • r-value와 이동 생성자를 이용하면 복사를 막을 수 있다. ( 이동 생성자 매개변수는 r-value 레퍼런스 )
  • r-value 레퍼런스는 r-value만 참조 가능, 참조하는 해당 변수는 r-value가 아니다.
    - string&& s= "dainn"; 에서 s는 r-value만 참조 가능하지만 s 자체는 l-value이다.
  • C++ 11 이후에 나온 문법
  • 레퍼런스( l-value, r-value )는 따로 메모리 공간없이 가르키는 주소로 바로 치환하는 형태이다.
  • auto&& a = "한글"; 시 auto는 const char형이기 때문에 이도 r-value이다.
  • const의 경우 r-value 레퍼런스로 참조 불가, 똑같은 const형의 l-value 레퍼런스로 참조해야 한다.
    - const int iA = 100;의 경우 iA는 재사용이 가능한 l-value이기 때문
  • 함수 오버로딩 시 매개변수 l-value타입과 r-value타입은 가능하지만 일반인 경우와 레퍼런스인 경우는 불가능

▶ 함수 오버로딩 주의

void setName(string _name) // 1번
	{
		string sName = move(_name); // move시 r-value로 만들어 임시 객체화 시켜버리기 때문에 _name은 다음 라인으로 넘어가면 소멸
									// cout << _name << endl; // move로 인해 소멸됨
		cout << sName << endl;
	}

void setName(string& _name) // 2번
{
	string sName = move(_name); // move시 r-value로 만들어 임시 객체화 시켜버리기 때문에 _name은 다음 라인으로 넘어가면 소멸
	// cout << _name << endl; // move로 인해 소멸됨
	cout << sName << endl;
}

void setName(string&& _name) // 3번
{
	// move를 이용해 r-value로 받아줘야 copy가 안 일어난대, 왜지
	// 그냥 _name을 대입해주면 복사 발생, move로 넘겨주면 0인 대신 _name은 임시 객체가 되어 소멸됨
	string sName = _name; 
	cout << sName << endl;
}

=> 2번 3번은 함수 오버로딩 가능
=> 1번 & 2번 또는 1번 & 3번은 1번 (일반변수)는 l-value와 r-value 다 받을 수 있기 때문에 불가능

▶ 레퍼런스는 메모리 공간 없음

int&& r = 10;
int& l = r;

cout << sizeof(short&) << "\t" << sizeof(short*) << endl; // 2	2
cout << sizeof(short&&) << "\t" << sizeof(short*) << endl; // 2  2

=> 레퍼런스의 크기는 자료형의 크기이다.

■ 예시

void Print(string&& _r) // 매개변수로 "다인"을 받았을 때
{
	// 원래는 l-value 레퍼런스가 r-value 참조 불가능, 리터럴 문자열이라 가능한 것
	string& l = move(_r); // 리터럴 문자열은 move로 r-value를 만들어도 l-value 레퍼런스로 참조 가능
	cout << _r << "\t" << l << endl; // 다인 다인 , l은 r을 참조했기 때문
	_r = "안녕"; // _r은 일반 l-value인 상태
    // move(_r) = "안녕"; 이었다면 당연히 _r은 r-value로 형 변환되어 불가능하게 됨
	cout << _r << "\t" << l << endl; // 안녕 안녕 , l과 _r은 같기 때문에 _r 값이 바뀌면 l도 바뀜
}

=> 리터럴 문자열일 때만 r-value에 구속받지 않고 참조 가능 ( move를 해도 l-value 레퍼런스가 참조 가능 )

void Render(int&& _i) // 상수 10을 받았을 때
{
	int&& lr = move(_i); // 해당 라인에서만 _i가 r-value로 형 변환
    // int&& lr = _i;의 경우 _i는 l-value이므로 참조 불가
    _i += 1;
	lr += 100; // lr은 l-value이므로 값 변경 가능
	cout << _i << "\t" << lr << endl; // 1 	100
}

=> 일시적 형 변환으로 _i를 참조 받은 lr과 _i는 다른 존재

void main(void)
{
	Print("다인");
    
	auto&& csiwoo= "이시우"; // auto는 const char*형
    Print(csiwoo); // 매개변수는 string&&을 받는데 묵시적 형변환이 일어나는 듯함 아마?
    
    const string& ls = cSiwoo; // cSiwoo는 문자열 상수이기 때문에 l-value 레퍼런스로 참조 시 const형으로 참조 가능
    string&& rs = cSiwoo; // 또는 cSiwoo는 문자열 상수이므로 r-value 레퍼런스로 참조 가능

	Render(0);

}
출력 결과 :
다인	다인 // Print("다인"); 결과
안녕	안녕
이시우	이시우 // Print(cSiwoo); 결과
안녕	안녕
1	100 // Render(0); 결과

 

[ move() ]

: r-value로 형변환 시켜주는 함수로 소유권 이동에 사용된다. ( 복사를 하지 않고 값을 넘기기 위함 )

class CDainn
{
public:

	void setName(string _name) // 1번, call by value
	{
		string sName = move(_name); // move시 r-value로 만들어 임시 객체화 시켜버리기 때문에 _name은 다음 라인으로 넘어가면 소멸
									// cout << _name << endl; // move로 인해 소멸됨
		cout << sName << endl;
	}

	// 1번과 2번 함수 오버로딩 불가능
	//void setName(string& _name) // 2번, call by reference
	//{
	//	string sName = move(_name); // move시 r-value로 만들어 임시 객체화 시켜버리기 때문에 _name은 다음 라인으로 넘어가면 소멸
	//	// cout << _name << endl; // move로 인해 소멸됨
	//	cout << sName << endl;
	//}

};
void main(void)
{
	CDainn cd;
	string s = "kitty";

	cd.setName(s); // 1번, s는 "kitty" 값을 담고 있는 일반 문자열 변수
    
	cout << "s값 : " << s << endl;
    => 1번 실행 시 값에 의한 복사이므로 _name을 move해도 원본은 살아있음
    => 2번 실행 시 레퍼런스로 원본 참조이므로 move시 r-value(임시 객체)로 변환되어 소멸되기 때문에 원본 값 사라짐
}
출력 결과 :
// 1번
kitty
s값 : kitty
// 2번
kitty
s값 : // 소멸됨

※ r-value는 임시 객체에 있다가 해당 라인을 벗어나면 소멸되는 재사용 불가 코드이므로, move시 해당 변수/객체는 소유권 사라짐( l-value, r-value 레퍼런스로 받은 경우 둘 다 ), move한 l_value 변수 자체는 살아있기 때문에 안의 값은 소유권이 없지만 사용은 가능
- 만약 move한 _name에 _name = "hello";을 한다면 _name은 hello
로 출력됨

=> 아니 근데 string형이랑 객체는 move하면 사라지는데 int형은 move해도 원본값 안 사라짐;

 

이동 생성자


[ 이동 생성자 ]

: 복사 생성자와 달리 메모리 재할당을 하지 않고 얕은 복사로 다른 개체 멤버들의 소유권을 가져오는 생성자

[ 이동 생성자 호출 조건 ]
1. 임시 객체를 전달할 때
2. move()를 통해 r-value를 전달할 때

[ 이동 생성자는 얕은 복사를 이용 ]
class CDainn {	
public:
	CDainn(CDainn&& _rhs) // 이동 생성자 형태
	{
	// 각 멤버 1대1 단순 대입
    	m_ptr = _rhs.m_ptr; // 얕은 복사
	_rhs.m_ptr = nullptr; // 이동할 객체가 얕은 복사로 원본의 주소를 가리키면, 원본은 접근 권한을 없앰
    
private:
    m_ptr;
}

void main(void) {
CDainn cd1;
cd2 = cd1; // 복사 생성자 ( 대입 연산자 후 복사 생성자 )
cd2 = move(cd1): // 이동 생성자
}

※ move()는 R-value 레퍼런스로 캐스팅 해주는 함수 ( 이동 생성자는 매개변수로 R-value 레퍼런스 받음 )

 

■ 기능 / 주의

  • 복사 생성자를 명시적으로 생성하면 이동 생성자는 암묵적으로 생성도지 않는다. (원래는 자동 생성됨)
  • 이동 생성자는 복사 생성자의 추가적인 메모리 할당과 복사로 인한 시간을 보완하기 위해 나온 문법이다.
  • 포인터를 일부러 얕은 복사시켜 복사할 메모리의 주소를 가리키게 한 뒤, 원본 포인터의 접근 권한을 없앤다.
  • 포인터로 가리키는 메모리를 일일이 복사하지 않기 때문에 복사 생성자에 비해 시간이 훨씬 단축된다.
  • 직접 정의한 복사 생성자나 복사 대입 연산자가 있으면 암시적 생성이 안된다. 

얕은 복사로 복사본이 원본 메모리를 가리키게 한 후, 원본 포인터의 접근 권한을 nullptr로 끊는다.

 

■ 예시

class CPlayer
{
public:
	CPlayer() : m_pArray(nullptr), m_iSize(0) {} // 기본 생성자

	CPlayer(int _iSize)  : m_iSize(_iSize)			// 1번, 매개변수 받는 생성자
	{
		m_pArray = new int[_iSize];

		for (int i = 0; _iSize > i; ++i)
		{
			m_pArray[i] = i+1;
		}
	}

	CPlayer(const CPlayer& rhs) : m_pArray(new int[rhs.m_iSize]), m_iSize(rhs.m_iSize) // 2번, 복사생성자 (깊은 복사)
	{
		memcpy(m_pArray, rhs.m_pArray, sizeof(int) * rhs.m_iSize);
	}

	CPlayer(CPlayer&& rhs) // 3번, 이동 생성자 (얕은 복사 후 원본 포인터 nullptr)
	{
		m_pArray = rhs.m_pArray;
		m_iSize = rhs.m_iSize;

		rhs.m_pArray = nullptr;		// 소유권 이전
	}


	~CPlayer() // 소멸자, 동적할당 해제
	{
		if (m_pArray)
		{
			delete[]m_pArray;
			m_pArray = nullptr;
		}
	}

public:
	CPlayer*		Clone(void)
	{
		return new CPlayer(*this); // *this는 자기 자신 CPlayer 값 자체, 자기 자신 복사본 객체를 할당해서 해당 주소를 넘기겠다는 뜻
	}

	void Print()
	{
		for (size_t i = 0; m_iSize > i; ++i)
		{
			cout << m_pArray[i] << "\t";
		}
	}

public:
	int*		m_pArray;
	int			m_iSize;
};

void main(void)
{
	CPlayer c1(10); // 매개변수 생성자 호출
	CPlayer c2 = c1; // 복사 생성자
	CPlayer c3 = std::move(c2); // 이동 생성자
	c1.Print(); // 1 ~ 9까지 출력
	cout << endl;
	//c2.m_pArray[0] = 100; // 이동 생성자로 c3이 주소를 갖고 가고 c2는 nullptr이 돼서 접근 불가
	//c2.Print(); // c2의 m_pArray는 nullptr이라 힙 메모리에 접근 불가
	c3.Print(); // c2가 갖고 있던 모든 멤버들 복사, 포인터 주소 얕은 복사로 대입 받음
}