완벽한 전달
Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 84번째.
인자를 받아 다른 함수로 전달하는 템플릿 함수를 작성할 때 한 가지 문제가 발생합니다. 원래 인자가 lvalue였는지 rvalue였는지 어떻게 유지할 수 있을까요? 완벽한 전달(Perfect forwarding)은 인자를 받은 그대로 전달하여 값 범주(value category)를 유지함으로써 이 문제를 해결합니다.
핵심 요소는 전달 참조(forwarding references)(템플릿 컨텍스트에서 T&&로 작성됨)와 std::forward입니다. 전달 참조는 lvalue와 rvalue 모두에 바인딩될 수 있으며, std::forward는 인수를 원래의 타입으로 조건부 캐스팅합니다:
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int n = 10;
wrapper(n); // process(int&) 호출 - lvalue 보존됨
wrapper(20); // process(int&&) 호출 - rvalue 보존됨
}std::forward가 없으면, rvalue가 전달되었더라도 이름이 있는 매개변수는 함수 내부에서 항상 lvalue가 됩니다. 이는 생성자나 다른 함수로 전달할 때 이동 의미론(move semantics)이 올바르게 작동하는 것을 방해합니다.
완벽한 전달(Perfect forwarding)은 팩토리 함수, 래퍼 클래스, 그리고 이동 또는 복사에 관한 호출자의 의도를 유지하면서 객체를 생성하거나 함수를 호출해야 하는 모든 제네릭 코드를 작성하는 데 필수적입니다.
챌린지
쉬움전달된 인수의 값 카테고리(value category)를 보존하면서 객체를 생성하는 완벽한 전달(perfect forwarding)을 보여주는 팩토리 함수를 만들어 보겠습니다. 인수를 클래스 생성자로 전달하여 lvalue는 복사되고 rvalue는 이동되도록 보장하는 제네릭 make_object 함수를 작성하게 됩니다.
코드는 다음 세 개의 파일로 구성됩니다:
Widget.h: 데이터를 어떻게 전달받는지 추적하는Widget클래스를 정의합니다.Widget은std::string타입의 name과int타입의 value를 저장해야 합니다. 두 개의 생성자를 제공하세요:const std::string&(lvalue 참조)와int를 받는 생성자 —Widget constructed (copy): [name]를 출력합니다.std::string&&(rvalue 참조)와int를 받는 생성자 —Widget constructed (move): [name]를 출력하고 문자열을 이동(move)시킵니다.
또한
[name]: [value]를 출력하는display()메서드를 추가하세요.Factory.h: 완벽한 전달 팩토리 함수를 작성합니다.name에 대해서는 전달 참조(forwarding reference)를 받고, value에 대해서는 일반
int를 받는 템플릿 함수make_object를 작성하세요.std::forward를 사용하여 name 인수의 원래 값 카테고리를 유지하면서Widget생성자에 전달합니다.이 함수는 생성된
Widget을 값으로 반환해야 합니다.main.cpp: 두 개의 입력을 읽습니다:- 이름 (문자열)
- 값 (정수)
두 개의 위젯을 생성하여 완벽한 전달을 시연합니다:
- 첫째, 이름을 지역 변수에 저장하고 이를 lvalue로서
make_object에 전달합니다. - 둘째, 임시 문자열(동일한 이름 뒤에
"_temp"를 연결한 것)을 rvalue로서make_object에 직접 전달합니다.
각 위젯을 생성한 후, 해당 위젯의
display()메서드를 호출하세요.
예를 들어, 입력값이 Gadget과 42인 경우:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42입력값이 Device와 100인 경우:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100핵심은 make_object 함수가 lvalue를 받았는지 rvalue를 받았는지 알지 못하더라도, std::forward를 사용함으로써 Widget 생성자를 호출할 때 해당 정보를 보존한다는 점입니다. 이를 통해 생성자 오버로딩 해결(overload resolution)이 적절한 버전을 선택하게 되어, 이름이 있는 변수는 안전하게 복사하고 임시 객체는 효율적으로 이동할 수 있게 됩니다.
치트 시트
완벽한 전달(Perfect forwarding)은 템플릿 함수가 인수를 다른 함수로 전달할 때 원래의 값 범주(lvalue 또는 rvalue)를 유지할 수 있게 해줍니다.
이 메커니즘은 std::forward와 결합된 전달 참조(템플릿 컨텍스트에서의 T&&)를 사용합니다:
#include <utility>
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}전달 참조는 lvalue와 rvalue 모두에 바인딩될 수 있습니다. std::forward<T>는 인수를 원래의 타입으로 조건부 캐스팅합니다.
std::forward가 없으면, rvalue가 전달되었더라도 함수 내부의 이름 있는 매개변수는 항상 lvalue로 취급됩니다. 이는 이동 의미론(move semantics)이 올바르게 작동하는 것을 방해합니다.
값 범주 보존을 보여주는 예제:
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int n = 10;
wrapper(n); // Calls process(int&) - lvalue preserved
wrapper(20); // Calls process(int&&) - rvalue preserved완벽한 전달은 팩토리 함수, 래퍼 클래스, 그리고 이동 대 복사에 관한 호출자의 의도를 유지하면서 객체를 생성하거나 함수를 호출하는 제네릭 코드에 필수적입니다.
직접 해보기
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// 입력 읽기
string name;
int value;
cin >> name >> value;
// TODO: name을 lvalue로 전달하여 첫 번째 widget 생성
// name을 지역 변수에 저장하고 make_object에 전달
// TODO: 첫 번째 widget의 display() 호출
// TODO: 임시 문자열을 rvalue로 전달하여 두 번째 widget 생성
// (name + "_temp")를 make_object에 직접 전달
// TODO: 두 번째 widget의 display() 호출
return 0;
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.