완벽한 전달( perfect forwarding )이 무엇인가?
함수로 전달된 인수가 함수 내에서 다시 다른 함수로 전달되는 일은 너무 자주 발생되는 상황입니다.
아래와 같이, 사용자를 추상화한 Person 객체를 매개변수로 받아서, 현재 시간을 기록하고, 로그인하는 함수를 다시 호출하는 함수를 그 예로 들 수 있습니다.
bool logging_and_login( const Persion& p ){
logging_current_time();
return login( p ); // 함수로 전달된 인자를 다시 다른 함수로 전달
}
이러한 인수의 함수로의 전달 중에,
완벽한 전달이란 함수에 전달된 인수가 자신의 속성을 그대로 간직한 채, 다른 함수의 인수로 다시 전달되는 것을 말합니다.
이 완벽한 전달을 구현하면, 왼값( l-value )이 주어진 경우, 이 값은 전달받은 함수는 함수 내의 다른 함수들을 왼값으로 호출할 수 있게 되고, 오른값( r-value )을 인수로 함수를 호출하면, 다시 오른값으로 다른 함수들을 호출할 수 있게 됩니다.
이러한 기능을 구현하는 가장 편한 방법은, 왼값을 매개변수로 받는 함수와 오른값을 매개변수로 받는 함수 두 종류의 함수를 작성하는 것입니다.
void logging_and_login( const Person& p ); // 왼값 매개변수
void logging_and_login( Person&& p ); // 오른값 매개변수
그런데, 얼마 안 가 이 해결 방법이 가진 문제점을 알게 될 것입니다.
만약, 위 함수의 매개변수 개수가 2개라면, 모든 가능성의 인수들을 처리할 함수의 개수가 4개 필요합니다.
같은 일을 하는 함수가 4개입니다.
함수에 필요한 인수의 개수가 3개라면, 작성해야 하는 함수가 무려 8개가 됩니다.
그래서, C++ 에선 템플릿 함수에서 사용할 보편 참조( universal reference )를 고안합니다.
보편 참조( universal reference )
아래 함수 템플릿에서 T&&가 보편 참조입니다.
이 참조 형식은 오른값 참조와 형태가 같지만, 왼값과 오른값을 모두 받을 수 있도록 고안된 형식입니다.
( 오른값 참조는 오른값만 받을 수 있습니다. )
template< typename T >
void func( T&& param ); // 보편 참조
그리고, 이것이 가능하도록, C++은 형식 템플릿 매개변수( type template parameter )를 추론하는 규칙에, 함수의 매개변수가 보편 참조일 때 템플릿 형식을 추론하는 규칙을 추가합니다.
템플릿의 형식 추론( template type deduction )에 관한 자세한 내용은 여기서 볼 수 있습니다.
그 추가된 규칙의 내용은, 템플릿 함수의 매개변수가 보편 참조일 때, 인수가 왼값이면 왼값 참조로 추론하고, 오른값이면 인수의 참조 속성을 무시한 참조 타입으로 추론한다는 내용입니다.
이 규칙을 위의 템플릿 함수에 적용하면 다음과 같습니다.
( 위 글을 참조하면 좀 더 쉽게 이해할 수 있습니다. )
Person p; // 왼값
Persion RegisterPerson(); // 오른값. 함수 선언
void func( p ); // ParamType은 Person&, T도 Person&
void func( RegisterPerson() ); // ParamType은 Person&&, T는 Person
여기서, T는 형식 템플릿 매개변수( type template parameter )입니다.
그리고, 위의 추론에 따라 컴파일러에 의해 구체화된 함수들은 아래와 같습니다.
template <> // 구체화( specialization ) 표시
void func( Person& p ); // 왼값 인수가 전달되었을 때
template <>
void func( Persion&& p ); // 오른값 인수가 전달되었을 때
즉, func 템플릿 함수를 왼값으로 호출하면, 인수를 왼값 참조로 받는 함수가 만들어지고, 오른값으로 호출하면, 오른값 참조 매개변수를 가진 함수가 만들어지게 됩니다.
이로써, 왼값, 오른값 인수를 하나의 템플릿 함수로 모두 처리할 수 있게 된 것입니다.
따라서, 8개의 조금씩만 다른 함수를 작성해야 했던 문제가 해결되었습니다.
이러한 내용을 글 처음에 예로 들었던 함수에 적용시켜 보면 다음과 같이 될 것입니다.
우선, logging_and_login 함수를 보편 참조를 매개변수로 하는 템플릿 함수로 변경합니다.
template< typename T >
bool logging_and_login( T&& p ){
logging_current_time();
return login( p ); // 함수로 전달된 인자를 다시 다른 함수로 전달
}
그리고, login 함수도 왼값과 오른값을 모두 넘겨받을 수 있도록 다음과 같이 템플릿 함수로 변경합니다.
template< typename T >
bool login( T&& p ); // 왼값, 오른값을 모두 받기 위한 템플릿 함수
이제, 이 logging_and_login 함수를 왼값으로 호출하면, 왼값 참조를 받는 함수가 생성됩니다.
그리고, 왼값 참조인 p가 다시 왼값 참조를 받는 login 함수에 전달됩니다.
이번엔, 이 함수를 오른값으로 호출하면, 오른값 참조를 받는 함수가 생성됩니다.
// 오른값으로 호출했을 때, 생성된 함수
template <>
bool logging_and_login( Person&& p ){
logging_current_time();
return login( p ); // 함수로 전달된 인자를 다시 다른 함수로 전달
}
그리고, 이 함수에 전달된 인수인 오른값을 다시 오른값을 받는 login 함수에 전달하면, 제일 처음의 인수 속성을 유지한 채, 함수에서 함수로 전달하는 완벽한 전달( perfect forwarding )을 구현하게 됩니다.
그런데, 안타깝게도 이 함수의 매개 변수인 p는 오른값이 아닙니다.
p의 타입은 오른값 참조이지만, p 자체는 이름을 갖고 있으므로 왼값입니다.
따라서, p를 그대로 login 함수에 전달하면, 왼값 참조를 받는 login 함수가 생성되고, 왼값이 전달됩니다.
결과적으로, 오른값이 왼값 형태로 전달되었습니다.
그렇기 때문에, 이를 해결할 장치가 하나 더 필요한 것입니다.
std::forward
위의 문제는 왼값으로 호출했을 때는 발생하지 않았습니다.
그러므로, 오른값으로 호출했을 때 생긴 왼값 매개변수를 오른값으로 바꿔주는 장치가 필요합니다.
이 같은 기능을 하는 함수가 바로 std::forward입니다.
template< typename T >
T&& forward( typename remove_reference<T>::type& param){
return static_cast<T&&>(param);
}
위의 코드가 forward 함수의 정의입니다.
( 몇 가지 설명을 하는데 불필요한 인터페이스 세부사항은 생략했습니다. )
이제, 이 함수를 포함시켰을 때, logging_and_login 함수가 어떻게 동작하는지 살펴봅시다.
먼저, 변경된 전체 함수의 코드는 다음과 같습니다.
template< typename T >
bool logging_and_login( T&& p ){
logging_current_time();
return login( std::forward<T>(p) ); // forward 함수를 추가
}
이 함수를 오른값으로 호출하면, 템플릿 타입 추론 규칙에 따라, 템플릿 타입 T는 Person 타입으로 추론되고 ( ParamType은 Person&& ), 이에 따라 다음과 같은 함수가 생성됩니다.
template <>
bool logging_and_login( Person&& p ){
logging_current_time();
return login( std::forward<Person>(p) ); // forward 함수를 추가
}
그리고, forward 템플릿 함수는 다음과 같이 인스턴스화됩니다.
( T를 전부 Person으로 변경합니다. )
template<>
Person&& forward( Person& param){
return static_cast<Person&&>(param);
}
그러면, logging_and_login의 왼값인 p가 forward 함수를 거쳐서 오른값으로 변경됩니다.
그리고, 이 오른값이 login 함수에 전달되므로, 원하는 대로 오른값 인수가 다시 오른값으로 전달됩니다.
이렇게 해서 완전한 전달이 구현되게 됩니다.
그런데, std::move 함수에 관한 내용을 안다면, 위의 forward가 move 함수와 똑같은 모습을 가졌다는 것을 알 것입니다.
즉, 이때의 forward 역할은 왼값 p를 오른값으로 변환하는 것입니다.
그럼, logging_and_login 함수를 왼값으로 호출하면 어떻게 될까요?
그러면, 템플릿 타입 추론 규칙에 따라, 왼값으로 호출된 함수의 템플릿 타입 T는 Person& 타입으로 추론되고, 이에 따라 다음과 같은 함수가 생성됩니다.
template <>
bool logging_and_login( Person& p ){
logging_current_time();
return login( std::forward<Person&>(p) ); // forward 함수를 추가
}
그리고, forward 템플릿 함수는 다음과 같이 인스턴스화됩니다.
( T를 전부 Person&으로 변경합니다. )
template<>
Person& && forward( Person& param){
return static_cast<Person& &&>(param);
}
그리고, 참조 축약( reference collapsing )이 일어납니다.
참조 축약( reference collapsing )
기본적으로 참조의 참조는 사용할 수 없습니다.
int val = 10;
int& & rval = val; // 참조의 참조는 정의 안됨. error !
그렇지만, 특정 상황일 때는 예외입니다.
- 템플릿의 인스턴스화 ( specialization )
- auto 변수에 대한 형식 추론
- typedef와 별칭 선언( using 구문 ) 평가 시
- decltype 형식 분석 시
이럴 경우, 참조 축약 실행되어 참조에 대한 참조를 제거합니다.
이러한 참조 축약의 규칙은 다음과 같습니다.
만일, 두 참조 중 하나라도 왼값 참조이면 결과는 왼값 참조이다.
그렇지 않으면( 즉, 둘 다 오른쪽 참조이면 ) 결과는 오른값 참조이다.
그리고, 지금 다루고 있는 문제는 보편 참조 매개변수를 가진 템플릿 함수를 인스턴스화하는 과정을 검토하고 있으므로, 참조 축약이 발생할 수 있는 예외 상황에 해당합니다.
그럼, 다시 원래의 문제로 돌아가 봅시다.
template<>
Person& && forward( Person& param){ // 참조 축약 전의 함수
return static_cast<Person& &&>(param);
}
위의 함수가 참조 축약을 거치면 다음과 같이 변경됩니다.
template<>
Person& forward( Person& param){ // 참조 축약 후의 함수
return static_cast<Person&>(param);
}
이 함수는 왼값 참조를 왼값 참조로 변환하므로, 실제로는 아무런 일도 발생하지 않습니다.
따라서, logging_and_login 함수를 왼값으로 호출하면, 이 왼값이 그대로 login 함수에 왼값으로 전달됩니다.
이제, 진짜로 완벽한 전달( perfect forwarding )을 완전히 구현했다고 할 수 있습니다.
정리
완벽한 전달은 인수의 속성을 그대로 다른 함수에 전달하는 것을 말합니다.
예를 들어, 왼값의 인수를 함수에 전달하면, 그 매개변수를 다시 다른 함수에 왼값으로 전달하는 것을 말합니다.
이러한 완벽한 전달을 수행하는 logging_and_login 함수 템플릿 최종본은 다음과 같습니다.
// 완벽한 전달을 하는 함수 템플릿
template< typename T >
bool logging_and_login( T&& p ){
logging_current_time();
return login( std::forward<T>(p) );
}
이 함수 템플릿은 보편 참조( universal reference ) 매개변수를 사용하고, 이 함수의 매개변수를 std::forward 함수를 통해서 다시 다른 함수로 전달합니다.
참고로, std::move는 모든 참조를 오른값 참조로 변환하지만, std::forward는 오른값 참조가 전달되었을 때만 다시 오른값 참조로 변환합니다.
이러한 std::move에 관한 내용은 여기에서 볼 수 있습니다.
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 컴파일러가 자동으로 작성하는 멤버 함수들 (2) | 2024.09.13 |
---|---|
[C++] auto의 형식 추론( type deduction ) 규칙 (0) | 2024.09.13 |
[C++] 템플릿의 형식 추론( template type deduction ) 규칙 (0) | 2024.09.07 |
[C++] 범위 있는( scoped ) enum에 관하여 (0) | 2024.09.05 |
[C++] 컴파일 시 사용 여부를 알려주는 constexpr 키워드 (2) | 2024.09.05 |