C++ Chapter 19.7 : 완벽한 전달과 std::forward
카테고리: Cpp
태그: Cpp Programming
인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀  
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!
chapter 19. 모던 C++ 필수 요소들
완벽한 전달과 std::forward
🔔 std::forward 개념과 문법 설명
#include <utility>
문맥에 따라
&L-value Reference,&&R-value Reference 을 구분하지 못하는 경우std::forward를 통해 타입을 분명하게 해준다. 완벽한 전달
문제점 : &와 && 구분이 어려운 경우
#include <iostream>	
#include <vector>
#include <utility>
using namespace std;
struct myStruct
{};
void func(struct myStruct& s) { cout << "Pass by L-ref\n"; }  // L-value Reference 오버로딩
void func(struct myStruct&& s) { cout << "Pass by R-ref\n"; }  // R-value Reference 오버로딩
template<typename T>   // 여러가지 타입을 T로 받을 수 있다.
void func_wrapper(T t)
{
    func(t);
}
int main()
{
	myStruct s;
	func(s);			// 적당한 것을 IDE가 잘 찾아서 연결해준다.
	func(myStruct());
    cout << endl;
	func_wrapper(s);
	func_wrapper(myStruct());
}
💎출력💎
Pass by L-ref
Pass by R-ref
Pass by L-ref
Pass by L-ref
- void func(struct myStruct&s)- 👉 구조체를 L-value Reference로 받는 func 함수 오버로딩
 
- void func(struct myStruct&&s)- 👉 구조체를 R-value Reference로 받는 func 함수 오버로딩
 
- template<typename T> void func_wrapper(T t){func(t);}
    - 👉 템플릿 함수로, func함수를 실행한다.
- 인수에 어떤 타입이 들어올지 알 수 없다.
        - 어떤 타입의 t가 들어오냐에 따라func(struct myStruct& s)을 실행할지func(struct myStruct&& s)을 실행할지 결정하게 됨.
 
- 어떤 타입의 
 
- 👉 템플릿 함수로, 
- main 함수
    - 아래와 같은 경우엔 컴파일러가 L-value Reference 인지 R-value Reference인지 구분이 가능하다.
        func(s); // L-value Reference를 받는 func(struct myStruct& s)가 실행된다. func(myStruct()); // R-value Reference를 받는 func(struct myStruct&& s)가 실행된다.
- 그러나 아래와 같이 ✨템플릿✨을 사용하는 경우엔 컴파일러가 구분하지 못해 제대로 func함수를 오버로딩 하지 못한다.func_wrapper(s); func_wrapper(myStruct());- 둘 다 L-value Reference로 처리해버린다
            - 대입되는 타입이 T 템플릿화가 되면서 R-value, L-value 정보가 날아가버리기 때문이다. 따라서 둘다 그냥 L-value Reference 인 것 처럼 실행이 된다.
                - Move Semantics 를 사용하기 위해 어떻게든 R-value로 넘기고 싶다면 이런 부분들이 문제가 됨.
 
 
- 대입되는 타입이 T 템플릿화가 되면서 R-value, L-value 정보가 날아가버리기 때문이다. 따라서 둘다 그냥 L-value Reference 인 것 처럼 실행이 된다.
                
 
- 둘 다 L-value Reference로 처리해버린다
            
 
- 아래와 같은 경우엔 컴파일러가 L-value Reference 인지 R-value Reference인지 구분이 가능하다.
        
해결책 👉 std::forward
std::forward가 하는 일 👉 L-value 로 들어온거면 L-value 로 리턴해주고, R-value 로 들어온거면 R-value 로 리턴.
template<typename T> 
void func_wrapper(T && t)
{
	func(std::forward<T>(t));
}
...
func_wrapper(s);
func_wrapper(myStruct());
void func_wrapper(T
&&t){ func(std::forward<T>(t)); }
- 인수 t를 넘길 때 1️⃣ 템플릿의 파라미터를T && t로 받고, 2️⃣std::forward<T>(t)을 사용하여 타입을 분명하게 해준다. 1️⃣,2️⃣ 가 둘 다 만족되야 한다.- t에 L-value가 들어오면 템플릿 변수- t를 L-value로 리턴한다.- 예를 들어 L-value 타입의 string 를 전달했다면 (func_wrapper(str) 같은) T는string &로 추론이 된다.- 즉, T && t👉string &+&+t가 되는 것이다.
- 그리고 std::forward가t를func에게 L-value Reference를 전달하게 된다.
 
- 즉, 
 
- 예를 들어 L-value 타입의 string 를 전달했다면 (func_wrapper(str) 같은) 
- t에 R-value가 들어오면 템플릿 변수- t를 R-value로 리턴한다.- 예를 들어 R-value 타입의 string 를 전달했다면 (func_wrapper(“Hello”) 같은) T는string로 추론이 된다.- 즉, T && t👉string+&&+t가 되는 것이다.
- 그리고 std::forward가t를func에게 R-value Reference를 전달하게 된다.
 
- 즉, 
 
- 예를 들어 R-value 타입의 string 를 전달했다면 (func_wrapper(“Hello”) 같은) 
 
🔔 Move Semantics 와 연관지어 생각해보기
#include <iostream>	
#include <vector>
#include <utility>
using namespace std;
class CustomVector
{
public:
	unsigned n_data = 0;  // 동적 배열의 사이즈가 될 멤버 변수
	int *ptr = nullptr;  // 동적 배열 포인터가 될 멤버 변수 (할당은 init 함수에서)
	CustomVector(const unsigned & _n_data, const int & _init = 0)
	{
		cout << "Constructor" << endl;
		
		init(_n_data, _init);
	}
	CustomVector(CustomVector & l_input)  // L-value 만 받을 수 있다.
	{
		cout << "Copy construtor" << endl;
		init(l_input.n_data);
        // 🎉깊은 복사 
		for (unsigned i = 0; i < n_data; ++i)  
			ptr[i] = l_input.ptr[i];
	}
	CustomVector(CustomVector && r_input) // R-value 만 받을 수 있다.
	{
		cout << "Move construtor" << endl;
        // 🎉얕은 복사 
        // 소유권 이전
		n_data = r_input.n_data;
		ptr = r_input.ptr;
        // 소유권 박탈
		r_input.n_data = 0;
		r_input.ptr = nullptr;
	}
	
	~CustomVector()
	{
		delete[] ptr;
	}
	void init(const unsigned & _n_data, const int & _init = 0)
	{
		n_data = _n_data;
		ptr = new int[n_data];
		for (unsigned i = 0; i < n_data; ++i)
			ptr[i] = _init; 
	}
};
void doSomething(CustomVector & vec)
{
	cout << "Pass by L-reference" << endl;
	CustomVector new_vec(vec);
}
void doSomething(CustomVector && vec)
{
	cout << "Pass by R-reference" << endl;
	CustomVector new_vec(std::move(vec));  // R-value로 vec을 받았더라도 std::move로서 넘겨주어야 한다. 
}
template<typename T>
void doSomethingTemplate_O(T && vec)  // 올바르게 R-value, L-value를 구분해서 컴파일 한다.
{
	doSomething(std::forward<T>(vec));
}
template<typename T>
void doSomethingTemplate_X(T vec)  // R-value, L-value를 구분해서 컴파일 하지 못하고 그냥 다 L-value로 처리해버린다.
{
	doSomething(vec);
}
- 복사 생성자 👉 CustomVector(CustomVector &l_input)- L-value 만 받을 수 있다.
- 깊은 복사
        - 그저 동적 배열의 원소 내용들만 쫙 복사해준다.
            - 복사 대상이 된 객체와 복사로 생성된 객체는 별개의 존재. 그저 내용물만 복사 받았을 뿐.
 
- 소유권 이전이 이루어지지 않음. 복사 대상이 된 객체의 소유권도 여전히 보장이 됨.
 
- 그저 동적 배열의 원소 내용들만 쫙 복사해준다.
            
 
- 이동 생성자 👉
    - R-value 만 받을 수 있다. CustomVector(CustomVector &&r_input)
- 얕은 복사
        - 아예 배열 자체의 소유권을 넘겨준다. 포인터만 복사하면 됨!
- 소유권 이전이 이루어 진다.
            - 복사 대상이 된 객체의 소유권은 박탈 됨. A 에서 B 로 얕은 복사가 이루어졌다면 A 와 해당 메모리의 연결이 끊어지며 동일한 메모리에 대한 소유는 이제 B 가 가지게 된다.
 
 
 
- R-value 만 받을 수 있다. CustomVector(CustomVector 
int main()
{
    CustomVector my_vec(10, 1024);
	CustomVector temp(my_vec);  // my_vec 은 L-value
	cout << my_vec.n_data << endl;
}
💎출력💎
Constructor
Copy constructor
10
- my_vec를 생성하면서 “Constructor” 출력
- temp를- my_vec을 통해 복사 생성하면서 “Copy constructor” 출력
- my_vec의 동적 배열 멤버- ptr은 깊은 복사를 통해- temp의 새로운 동적 배열 공간에 내용물을 다 복사해 주었기 때문에- my_vec의 멤버로 동적 배열의 사이즈를 나타내는- n_data는 문제 없이 잘 보존이 되어있는 것을 “10” 출력을 통해 할 수 있다.- my_vec.ptr동적 배열과- temp.ptr동적 배열은 내용물만 같을 뿐 별개의 존재이다. 새 집을 얻고 그 공간에 이전 집과 똑같은 가구들을 새로 사서 배치한 것과 같다. 띠리사- my_vec은 보존된다.
int main()
{
    CustomVector my_vec(10, 1024);
	CustomVector temp(std::move(my_vec));  // my_vec 은 R-value
	cout << my_vec.n_data << endl;
}
💎출력💎
Constructor
Move constructor
0
- my_vec를 생성하면서 “Constructor” 출력
- temp가 R-value로서 넘긴- my_vec을 통해 이동 생성하면서 “Move constructor” 출력
- my_vec의 동적 배열 멤버- ptr은 얕은 복사를 통해- temp에게 아예 동일한 그- ptr배열을 통째로 넘겨주었다. 즉, 포인터만 복사함. 따라서- my_vec의- ptr동적 배열 소유권이 박탈되었기 때문에- my_vec.n_data는 “0”이 출력을 되는 것을 볼 수 있다.- my_vec.ptr동적 배열과- temp.ptr동적 배열은 동일한 존재이다. 이제 동일한- ptr동적 배열의 주인은- my_vec이 아니라- temp가 된다. 집은 그대로인데 열쇠(포인터)를 건네주어 주인만 바뀐 격이다. 가구들을 똑같은 것으로 새로 살 필요가 없다.
int main()
{
    CustomVector my_vec(10, 1024);
	doSomething(my_vec);
    doSomething(CustomVector(10, 8));
	my_vec;
}
💎출력💎
Constructor
Pass by L-reference
Copy constructor
Constructor
Pass by R-reference
Move constructor
- my_vec를 생성하면서 “Constructor” 출력
- doSomething(my_vec)👉 void doSomething(CustomVector & vec) 호출- “Pass by L-reference” 출력
- L-value Referece인 my_vec으로new_vec을 복사 생성하면서 “Copy constructor” 출력
 
- doSomething(CustomVector(10, 8))👉 void doSomething(CustomVector && vec) 호출- 인수로 넘긴 CustomVector(10, 8)로 “Constructor” 출력
- “Pass by R-reference” 출력
- R-value Referece인 my_vec으로new_vec을 이동 생성하면서 “Move constructor” 출력
 
- 인수로 넘긴 
- 아래에 my_vec;은my_vec을 또 사용하겠다는 의지로 교수님께서 추가하신 것이다.my_vec을 사용하여 객체를 만들 대,my_vec을 또 사용하겠다는 의지가 있다면 소유권이 박탈되지 않게끔 깊은 복사를 사용하는 L-value로서 넘겨야 하고CustomVector(10, 8)처럼 더는 사용할 일이 없는 R-value라면 이동 생성자를 호출하는 것을 권장한다.- my_vec도 다시는 사용되지 않을 것이라면- my_vec를 사용하여- temp를 만들 때,- my_vec을 R-value로서 넘겨 이동 생성자를 호출해줄 수도 있지만 이런 경우 프로그래머가- my_vec은 다시는 사용되지 않을 객체라는 것을 계속 기억을 하고 있어야 하므로 불편하다.
 
int main()
{
    CustomVector my_vec(10, 1024);
	doSomethingTemplate_O(my_vec);
    doSomethingTemplate_O(CustomVector(10, 8));
	my_vec;
}
💎출력💎
Constructor
Pass by L-reference
Copy constructor
Constructor
Pass by R-reference
Move constructor
- doSomethingTemplate_O 함수는 T && t로 인수를 받고,std::forward를 통해 올바르게 R-value, L-value를 구분해서 컴파일 하는 함수다.
- 출력 결과를 보면 R-value, L-value를 구분한 것을 볼 수 있다.
int main()
{
    CustomVector my_vec(10, 1024);
	doSomethingTemplate_X(my_vec);
    doSomethingTemplate_X(CustomVector(10, 8));
	my_vec;
}
💎출력💎
Constructor
Pass by L-reference
Copy constructor
Constructor
Pass by L-reference
Copy constructor
- doSomethingTemplate_X 함수는 T t로 인수를 받고, R-value, L-value를 구분하지 못하고 그냥 다 L-value로 처리해버리는 함수다.
- 출력 결과를 보면 전부 L-value로서 다룬 것을 볼 수 있다.
void doSomething(CustomVector && vec)
{
	cout << "Pass by R-reference" << endl;
	CustomVector new_vec(std::move(vec));  // R-value로 vec을 받았더라도 std::move로서 넘겨주어야 한다. 
}
소소하게 주의할 점이라면, 매개 변수 vec가 && R-value Reference라 하더라도 vec는 사실 변수이기 때문에 CustomVector new_vec(vec) 로 넘겨주면 L-value로서 Copy contructor가 호출된다. 따라서 vec이 R-value Reference 변수인 경우에도 이동 생성자를 호출하고 싶다면 꼭 CustomVector new_vec(std::move(vec)) 이렇게 std::move를 사용하여 넘겨주어야 한다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀
린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
 
      
    
댓글 남기기