-
[Effective Modern C++] 참조 축약 (Reference collapsing)C++/Effective Modern C++ 2022. 8. 10. 13:47
참조에 대한 참조는 기본적으로 위법입니다.
따라서, 다음과 같은 코드는 컴파일 되지 않습니다.
int x; auto& & rx = x;
C2529 : 'rx': you cannot create a reference to a reference
하지만 이것이 허용되는 몇 가지 상황이 존재합니다. 이것을 Reference collapsing이라고 합니다.
이번 글에서는 이 참조 축약에 대하여 살펴보도록 하겠습니다.
1. 참조 축약 (Reference collpasing)
더보기참조에 대한 참조는 위법이라는 것을 서론에서 살펴보았습니다.
하지만 아래와 같은 코드는 적법합니다.
class MyClass {}; template<typename T> void func(T&& param) { } int main() { MyClass c; func(c); return 0; }
위 코드의 c는 L-value이기 때문에, 인스턴스화 된 func는 다음과 같습니다.
void func(MyClass& && param);
서론과는 달리 위 코드는 정상적으로 컴파일 됩니다. 추가적으로, 위 코드는 아래와 같이 연역됩니다.
void func(MyClass& param);
위 템플릿 인스턴스가 L-value참조로 연역되는 자세한 내용은 다른 글 (Type deduction : Template)에 서술되어 있습니다.
돌아와서, 위 코드가 적법한 이유는 일부 문맥에서 참조에 대한 참조가 허용되기 때문입니다.
특정 문맥에서 허용되는 참조에 대한 참조는 다음과 같은 규칙에 의거하여 한 개의 참조로 축약됩니다.
- 두 참조 모두 R-vlaue참조일 경우 R-value참조로 축약됩니다.
- 그 외의 경우 (한 개라도 L-value일 경우) L-value참조로 축약됩니다.
2. std::forward에 대하여
더보기참조 축약은 이전 글 (std::move와 std::forward)에서 잠시 소개된 적이 있습니다.
std::forward는 인수가 R-value일 경우에 R-value로 캐스팅을 수행하는 함수였습니다.
template<typename T> void func(T&& fParam) { ... func2(std::forward<T>(fParam)); }
따라서 위와 같은 경우, func의 인수가 R-value일 경우(T가 비참조 형식일 경우) fParam을 R-value로 캐스팅합니다.
이것을 구현하는 std::forward의 가능한 코드 중 하나는 아래와 같습니다.
template<typename T> T&& forward(typename std::remove_reference<T>::type& param) { return static_cast<T&&>(param); }
위 func와 forward, 그리고 사용자 정의 클래스인 MyClass를 통해 조금 더 자세히 살펴보도록 하겠습니다.
func에 MyClass형식의 L-value가 전달되었을 경우를 살펴보겠습니다.
이 경우 T는 MyClass&로 연역되며, std::forward는 std::forward<MyClass&>의 형태로 인스턴스화 합니다.
이것을 std::forward의 구현에 대입하면 아래와 같습니다.
MyClass& && forward(typename std::remove_reference<MyClass&>::type& param) { return static_cast<MyClass& &&>(param); }
대입된 코드를 하나씩 풀어보도록 하겠습니다.
- std::remove_reference<MyClass&>::type은 MyClass가 됩니다.
- 반환 형식 및 캐스팅에 참조 축약 규칙을 적용하면, 두 가지 모두 MyClass&가 됩니다.
따라서 std::forward의 인스턴스화 된 코드는 아래와 같습니다.
MyClass& forward(MyClass& param) { return static_cast<MyClass&(param); }
결과적으로, L-value가 전달되었을 경우 std::forward는 L-value참조를 받고 L-value참조를 반환합니다.
func에 MyClass형식의 R-value가 전달되었을 경우를 살펴보겠습니다.
이 경우 T는 MyClass입니다.std::forward는 std::forward<MyClass>의 형태로 인스턴스화 합니다.
이것을 std::forward에 대입하면 아래와 같습니다.
MyClass&& forward(typename std::remove_reference<MyClass>::type& param) { return static_cast<MyClass&&>(param); }
대입된 코드를 하나씩 풀어보도록 하겠습니다.
- std::remove_reference<MyClass>::type은 MyClass가 됩니다.
- 반환 형식 및 캐스팅 등, 참조에 대한 참조가 있는 구문은 추가적으로 없습니다.
따라서 std::forward의 인스턴스화 된 코드는 아래와 같습니다.
MyClass&& forward(MyClass& param) { return static_cast<MyClass&&>(param); }
결과적으로, func에 전달된 R-value인수는 R-value로 전달됩니다.
위 std::forward의 인자에 사용된 typename std::remove_reference<T>::type은 C++11에서 아래와 같이 더 간결하게 구현 가능합니다.
template<typename T> T&& forward(std::remove_reference_t<T>& param) { return static_cast<T&&>(param); }
3. 참조 축약이 일어나는 문맥
더보기참조 축약은 네 가지 문맥에서 일어납니다.
- 템플릿 인스턴스
- auto 변수
- decltype
- typedef와 using
이 중, typedef의 사용에 대하여 예제를 살펴보도록 하겠습니다. (using도 유사합니다.)
template<typename T> class MyClass { public: typedef T&& rValueRef; ... };
위 클래스를 L-value참조 형식으로 인스턴스화 할 경우를 생각해보도록 하겠습니다.
MyClass<int&> c;
이 때, 템플릿 인스턴스화는 다음과 같이 이루어집니다.
typedef int& && rValueRef;
참조 축약에 의해, 이는 다음과 같이 축약됩니다.
typedef int& rValueRef;
변수의 이름인 rValueRef와는 맞지 않게, 변수가 L-value 참조로 연역되었습니다.
이전 글 (보편 참조)에서 보편 참조를 R-value참조와 구분되는 것 처럼 설명했습니다.
R-value참조와 다른 것은 맞지만, 아래의 조건이 만족될 경우 보편 참조는 R-value참조와 동일합니다.
- 형식 연역에서 R-value와 L-value가 구분될 때
- 참조 축약이 적용될 때
템플릿, auto, decltype의 형식 연역과 관련한 내용은 다른 글에 정리되어 있습니다.
참조 축약 문맥에 대한 정보, R-value및 L-value의 형식 연역에 대한 정보, 그리고 연역된 형식들을 대입 및 규칙을 적용하는 등, 내부적으로는 다소 복잡할 수 있는 과정들이 보편 참조라는 개념 하나에 녹아있습니다.
보편 참조의 존재로 위 내용들을 숙지하며 프로그래밍 할 필요는 없지만, 이것이 왜 이렇게 되는지에 대한 참고 자료가 되었으면 합니다.
감사합니다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 완벽 전달(Perfect forwarding)의 실패 (0) 2022.08.23 [Effective Modern C++] 이동 시맨틱의 맹점 (0) 2022.08.18 [Effective Modern C++] Universal reference와 Overloading (2) (0) 2022.08.01 [Effective Modern C++] Universal reference와 Overloading (0) 2022.07.28 [Effective Modern C++] std::move와 std::forward (2) (0) 2022.07.25