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
를 사용하여 넘겨주어야 한다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀
린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기