C++ Chapter 19.7 : 완벽한 전달과 std::forward

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 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로 넘기고 싶다면 이런 부분들이 문제가 됨.


해결책 👉 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) 같은) Tstring &로 추론이 된다.
        • 즉, T && t 👉 string & + & + t 가 되는 것이다.
        • 그리고 std::forwardtfunc에게 L-value Reference를 전달하게 된다.
    • t 에 R-value가 들어오면 템플릿 변수 t 를 R-value로 리턴한다.
      • 예를 들어 R-value 타입의 string 를 전달했다면 (func_wrapper(“Hello”) 같은) Tstring로 추론이 된다.
        • 즉, T && t 👉string + && + t 가 되는 것이다.
        • 그리고 std::forwardtfunc에게 R-value Reference를 전달하게 된다.


🔔 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 가 가지게 된다.
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” 출력
  • tempmy_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_vecptr 동적 배열 소유권이 박탈되었기 때문에 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를 사용하여 넘겨주어야 한다.



🌜 개인 공부 기록용 블로그입니다. 오류나 틀
린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기