[C++] 오른값 참조, move()

seunghyun·2023년 5월 22일
0

참고하면 좋을 사이트 1
참고하면 좋을 사이트 2
이미지 출처

🌱 개념

복사/이동의 차이

'원본 데이터를 유지해야 하는지'에 있다.

  • 복사생성자 & 복사연산자의 경우, 원본을 날리면 안되고 복사만 해야하는데
  • 이동생성자 & 이동대입연산자의 경우, 원본은 더 이상 필요없다는 명확한 가정이 있으므로, 경우에 따라 얕은 복사를 통해 소유권을 이전할 수 있다.

이동은 복사와는 조금 다른 것이, 원본은 정말 사용하지 않겠다는 의미이다.
경우에 따라 복사 비용을 엄청! 절감할 수 있다 ⭐️

  • 우리가 배웠던 자료구조 중에서 STL Vector 에서 사용되고 있다.
  • Unique Pointer 처럼 복사는 막지만 이동은 허용. 소유권 자체를 넘길 때.

왼값, 오른값

  • l value : 단일식을 넘어서 계속 지속되는 개체

  • r value : l value 가 아닌 나머지

    • 단일식을 넘어서 계속 지속되지 않는 대표적인 예시가 바로 임시 객체이다.
    • 참고로 임시 객체는 스택에 잠시 몸을 담는다~

오른값 참조

  • 오른값만 참조할 수 있는 참조 타입
  • 오른값 참조라고 해서 꼭 오른값인 것은 아니다!

오른값 참조 코드 예시

void TestKnight_Copy(Knight knight) { } // 복사
void TestKnight_LValueRef(Knight& knight) { } // 왼값 참조 (비const)
void TestKnight_ConstLValueRef(const Knight& knight) { } // 왼값 참조 const
void TestKnight_RValueRef(Knight&& knight) { } // 오른값 참조 // 소유권 자체를 넘긴다

int main()
{
	Knight k1;
	
	TestKnight_Copy(k1);
	
	TestKnight_LValueRef(k1);
	// TestKnight_LValueRef(Knight()); // error
	
	TestKnight_ConstLValueRef(Knight()); 
	
	TestKnight_RValueRef(Knight());
	// 왼값 참조처럼 원본을 고칠 수 있다는, 굉장히 큰 권한을 얻게 된 것이다
	// 근데 이제 원본이 더 이상 필요가 없어졌다. 소유권도 가질 수 있게 된다 ⭐️
	TestKnight_RValueRef(static_cast<Knight&&>(k1)); 

	
	return 0;
}

🙋🏻‍♀️ 여기서 질문!

𝑸. C++ 컴파일러는 클래스에 대해 어떤 함수들을 암시적으로 생성할 수 있는가?
𝐀. 생성자, 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자 를 생성한다.

  • 생성자

    • 부모 클래스의 생성자를 호출한다는 특성이 있다
    • 클래스에 파라미터가 없는 생성자가 정의되어 있지 않을 때 자동 생성된다
  • 소멸자

  • 복사 생성자

    • 멤버를 얕은 복사하는 것으로 동작한다
    • 생성과 동시에 복사를 할 때 호출된다
Knight(const Knight& knight)
{

}
  • 복사 대입 연산자
    • 생성은 이전에 되었었고, 복사만 할 때 호출된다
    • 일어날 수 있는 문제점 : 얕은 복사
void operator=(const Knight& knight)
{
	_hp = knight._hp;
	
	// 깊은 복사로, 얕은복사 문제 해결! 그러나 복사 비용이 엄청 클 수 있다.
	if(knight._pet)
		_pet = new Pet(*knight._pet); 
}
  • 이동 생성자
Knight(Knight&& knight) noexcept
{

}
  • 이동 대입 연산자
void operator=(Knight&& knight) noexcept
{
	_hp = knight._hp;
	_pet = knight._pet;
	knight._pet = nullptr;
}
int main()
{
	Knight k1;
	Knight k2;

	// 아래 두 코드는 완전히 같은 의미이다
	k2 = static_cast<Knight&&>(k1); 
	k2 = std::move(k1);
}

🎁 활용 예시 : 값 전달, 참조 전달, 왼값 참조, 오른값 참조

/* 값 복사. 원본에 영향을 미치지 않는다. */
void TestKnight_Copy(Knight knight)
{
	Knight._hp = 100;
}

/* 
참조 방식. 왼값 참조라고 생각해도 좋다. 
원본 주소값을 넘겨주는 방식으로, 
내부적으로 코드를 살펴보면 포인터를 넘겨주는 것과 차이가 없다.
원본에 영향을 미칠 수 있다.
*/
void TestKnight_LValueRef(Knight& knight)
{
	Knight._hp = 100;
}

/* const 로 받으므로 원본 변경이 불가능하다 */
void TestKnight_ConstLValueRef(const Knight& knight)
{
	Knight._hp = 100;
}

/* 
참조의 참조가 아니라, 오른값 참조라는 것 
원본에 영향을 미칠 수 있는데
더 이상 활용하지 않을 예정이라서 마음대로 하라는 뜻
*/
void TestKnight_RValueRef(Knight&& knight)
{

}

int main()
{
	Knight k1;

	// 컴파일러 오류 : 비 const 참조에 대한 초기값은 lvalue여야 한다.
    // 논리적으로도 앞뒤가 맞지 않다. 임시 객체를 수정한다니 (???)
    TestKnight_LValueRef(Knight()); 
    
    // 실행 가능
    TestKnight_ConstLValueRef(Knight());
    
    // 컴파일러 오류 : rvalue 참조를 lvalue 에 바인딩할 수 없다.
    TestKnight_RValueRef(k1);
      
    // 실행 가능
    TestKnight_RValueRef(Knight());

    TestKnight_RValueRef(static_cast<Knight&&>(k1));
}

🎁 활용 예시 : 정리해보자~

class Pet
{

};

class Knight
{
public:
	Knight()
	{
		cout << "Knight()" << endl;
	}

	// 복사 생성자
	Knight(const Knight& knight)
	{
		cout << "const Knight()" << endl;
	}

	// 이동 생성자
	Knight(Knight&& knight)
	{
	
	}
	
	~Knight()
	{
		if(_pet)
			delete _pet;
	}

	// 복사 대입 연산자
	// 복사는 "복사 생성자" 또는 "복사 대입 연산자" 에서 이뤄진다
	void operator=(const Knight& knight)
	{
		cout << "operator=(const Knight&)" << endl;
		_hp = knight._hp;
		// 깊은 복사
		// 단점 : 코스트가 비싸질 수 있다, 원본을 참고하고 돌려준다 (원본 훼손 불가 ⭐️)
		if(knight._pet)
			_pet = new Pet(*Knight._pet);
	}

	// 이동 대입 연산자
	// 장점: 원본 훼손 가능 ⭐️, 얕은 복사로도 쉽게 사용 가능
	void operator=(Knight&& knight) noexcept
	{
		cout << "operator=(Knight&&)" << endl;
		_hp = knight._hp;
		
		//if(knight._pet)
		//	_pet = new Pet(*knight._pet);
		_pet = knight._pet; // ⭐️
		knight._pet = nullptr;
	}
	
public:
	int _hp = 100;
	Pet* _pet;
};


int main()
{
	Knight k2;
	k2._pet = new Pet();
	k2._hp = 1000;
	Knight k3;
	k3 = std::move(k2); // 오른값 참조로 캐스팅 // k3 = static_cast<Knight&&>(k2);

	std::unique_ptr<Knight> uptr = std::make_unique<Knight>();
	std::unique_ptr<Knight> uptr2 = std::move(uptr);
}

0개의 댓글