Perfect Forwarding 소개:
- Perfect Forwarding은 템플릿에서 잘 사용되는 개념 입니다.
- 예시:
void function_name(int& lvalue) {
std::cout << "this fuction accepts lvalue: " << lvalue << std::endl;
}
void function_name(int&& rvalue) {
std::cout << "this function accepts rvalue: " << rvalue << std::endl;
}
- 위와 같은 경우에 템플릿으로 임의의 Type 을 받았을 때, function_name 을 호출하고 싶은데, 받은 Type이 lvalue 인지 rvalue 인지에 따라 다른 함수를 호출하고 싶으면 어떻게 할까?
template <class Type>
void template_fuction(Type&& parameter) { // universal reference: 유니버셜 참조
function_name(std::forward<Type>(parameter); // std::forward: C++ 표준 라이브러리 utility 함수
}
- 위 코드가 perfect forwarding 이다. universal reference를 통해 lvalue 와 rvalue 가 모두 인자로 올 수 있다는 것을 나타낸다. lvalue가 오면 lvalue를 function_name 인자로 넣어주고, rvalue가 오면 rvalue를 function_name 인자로 넣어주는 것이 perfect forwarding (속성이 유지됨) 이다. 이때, std::forward 를 사용해 넣는 parameter 인자가 rvalue면 rvalue를 쓰겠다고 알려줘야 한다. 아니면, lvalue로 가정해 rvalue가 lvalue로 변환되어 호출된다.
universal reference가 뭐고 std::forward 는 꼭 같이 써야 하는가:
- universal reference는 템플릿 작성 시 매개 변수 옆에 &&을 붙이는 경우 동작하는 개념입니다.
- 템플릿이 아닌 비템플릿 코드에서 매개 변수 옆에 &&을 붙이면 rvalue 매개 변수를 뜻합니다.
- universal reference는 매개변수로 lvalue, rvalue 가 모두 올 수 있다는 것을 뜻합니다.
- std::forward 는 설계 될 때 템플릿에서 universal reference와 같이 사용되도록 설계 되었습니다.
- 단, template 을 안쓰고 명시적으로 type을 rvalue로 정의하는 경우 universal reference 없이 std::forward 를 사용 할 수 있습니다.
void template_function(int&& parameter) { // 직접 매개변수 정의
function_name(std::forward<int&&>(parameter));
}
- 단, 이때는 std::move와 똑같이 동작하므로, std::forward 를 사용해야 할 의무는 없습니다.
C+11 에서 move semantics 도입한 배경:
- 복사가 아닌 이동을 위해서:
- 복사는 비싸다는 것을 보여주는 예시:
/* 테스트 준비 */
class LargeObject {
private:
std::vector<int> data;
public:
LargeObject(size_t size) : data(size, 0) {
// size_t 는 플랫폼 종속적인 양의 정수를 나타낼 때 씀
}
LargeObject(const LargeObject& other) : data(other.data) {
// copy constructor
}
LargeObject(LargeObject&& other) noexcept : data(std::move(other.data)) { // member 변수로 쓰는 std::vector는 move constructor가 noexcept 이므로 noexcept 가능
// move constructor
}
LargeObject& =operator(const LargeObject& other) {
// copy assignment operator
if (&other != this) { // address를 비교. other != *this 는 value를 비교한다
// avoid self-assignment
data = other.data;
// vector 는 별도 delete 과정이 필요없다 (내부적으로 처리)
}
return *this;
}
LargeObject& =operator(LargeObject&& other) noexcept { // 내부적으로 쓰이는 member 변수의 vector의 move assignment operator가 noexcept 이므로 noexcept 가능
// move assignment operator
if (&other != this) { // other != *this 와는 다르다. other != *this 는 value 비교를 뜻한다. 물론, 이는 !=operator 의 정의에 의해 동작한다.
// avoid self-assignment
data = std::move(other.data); // std::move의 반환값은 xvalue (rvalue) 이다.
// rvalue를 기존에 생성된 vector에 대입하고 있으므로 vector의 move assignment operator 가 호출된다
// vector의 move assignment operator가 other.data를 정리하므로 other.data.clear() 는 필요 없다.
}
}
};
/* 시간 측정 테스트 */
template <class Func>
void measure_time_taken(const std::string& operation_name, Func&& func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto finish = std::chrono::high_resolution_clock::now();
auto duration = finish - start;
auto duration_cast = std::chrono::duration_cast<std::chrono::milliseconds>(duration);
std::cout << operation_name << " took " << duration_cast.count() << " ms" << std::endl;
}
size_t size = 10'000'000; // '는 C++14부터 도입된 자릿수 구분 기호
LargeObject object(size);
measure_time_taken("copy constructor", [&](){
LargeObject copy_object(object);
});
measure_time_taken("move constructor", [&](){
LargeObject move_object(std::move(object));
});
LargeObject another_object(size);
measure_time_taken("copy assignment operator", [&](){
copy_object = another_object;
});
measure_time_taken("move assignment operator", [&](){
move_object = std::move(another_object);
});
rvalue 소개:
- rvalue 자체는 C++98/03 부터 존재했던 개념
- rvalue 는 C++11 이후 두 종류로 나뉨
- pure rvalue (prvalue): 임시 객체 또는 리터럴 값
- 리터럴 값: 5, "Hello World" 등 바로 값이 assembly code에 써짐
- 임시 객체: 임의의 메모리 주소에 값을 적고, 그 메모리 주소를 참조하는 객체
- xvalue (expiring value): 이동 대상 객체
- pure rvalue (prvalue): 임시 객체 또는 리터럴 값
/* prvalue */
// 리터럴
// 5, "Hello world"
int x = 5;
/* 임시 객체 */
// Foo()
Foo x{Foo()};
// uniform initialization 사용 의도적으로 Foo() 라는 임시 객체를 생성함.
// C++17 이후에서는 무조건 copy elision 발생.
/* xvalue */
// std::move() 가 반환하는 값
std::string str = "Hello, world!";
std::string moved_str = std::move(str);
다음 글:
- C++ 면접 질문) "uniform initialization 사용을 권장하는 이유를 혹시 아시나요?"
- 자료구조 면접 질문) "array와 linked list 의 차이점을 말해주세요."
'C++' 카테고리의 다른 글
자료구조 면접 질문) "array와 linked list 의 차이점을 말해주세요." (0) | 2025.01.11 |
---|---|
C++ 면접 질문) "C++ 템플릿이 뭔가요? 관련해서 사용한 경험 있으시면 말씀해주세요." (0) | 2025.01.09 |
C++ 면접 질문) "람다 함수 형식을 설명해주세요." (0) | 2025.01.08 |
C++ 면접 질문) "boost asio를 사용하여 어떻게 event-driven architecture를 구현하셨나요?" (0) | 2025.01.08 |
C++ 면접 질문) "어떤 때 reference 쓰고 어떤 때 value를 써야 할까요?" (0) | 2025.01.07 |