ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 전달 참조(보편 참조)
    개발/C·C++ 2022. 1. 8. 23:00

    값의 카테고리는 기본적으로 lvalue, xvalue, prvalue 나눌 수 있습니다.

    prvalue

    옛날의 rvalue입니다. 쉽게 말해 1과 상수, 42, true, nullptr 같은 리터럴(문자열 리터럴은 제외)입니다.

    함수의 반환값이나 a + b처럼 오른쪽에만 올 수 있는 값입니다.

     

    xvalue

    eXpiring value로서 prvalue는 아니지만 prvalue처럼 기한이 있는 값입니다. 대표적으로 std::move(x)입니다. 

     

    lvalue

    나머지가 lvalue입니다. 변수 이름, 데이터 멤버, 심지어 타입이 rvalue reference라면, 이름으로 되어 있는 표현식은 lvalue입니다. 예를 들어 T&& a = T()일 때 a는 lvalue입니다.

     

    prvalue와 xvalue를 묶어 rvalue라고 하며 lvalue와 xvalue를 묶어 glvalue라고 합니다.


    rvalue reference를 이용하면 rvalue를 참조변수가 가리키게 할 수 있습니다.

    int main()
    {
    	int&& num = 10;
    }

     

    num의 타입은 rvalue reference인데 num 자체는 어떤 변수일까요? 확인해봅시다

     

    void goo(int& i)
    {
    	cout << "int&" << endl;
    }
    
    void goo(int&& i)
    {
    	cout << "int&&" << endl;
    }
    
    void bar(int&& value) 
    {
    	goo(value); // what is the value?
    }
    
    int main()
    {
    	bar(10);
    }

    goo(value)를 호출하면 어느 문자열이 출력될까요?

    prvalue를 인수로 넘겨 rvalue reference로 받았으며 그대로 전달했는데 lvalue reference를 파라미터로 하는 함수가 호출됐습니다. value는 타입이 rvalue reference이지만 value 자체는 lvalue이기 때문입니다. 이름 있는 변수는 lvalue입니다. 때문에 lvalue reference를 파라미터로 하는 goo 함수의 호출은 당연합니다. 하지만 rvalue를 넘긴 이상 계속해서 타입을 유지하고 싶다면 어떻게 해야 할까요? 가장 쉬운 방법은 하드 코딩을 하는 겁니다.

    void bar(int&& value) 
    {
    	goo(std::move(value));
    }

    이렇게 move 함수를 이용해 직접 캐스팅을 하면 rvalue reference가 반환되기 때문에 int&&가 출력됩니다.

     

    예제 코드는 인수가 하나라서 rvalue로 받을 때와 lvalue로 받을 때의 경우가 두 가지뿐이지만 훨씬 많은 인수를 받아야 하거나 혹은 가변인자로 받을 경우에는 일일이 오버로딩 함수를 만드는 것은 불가능에 가깝습니다. 효율적이지도 않고 유지 보수에도 좋지 않을 겁니다. 이 문제는 전달 참조(보편 참조)와 완벽 전달(perfect forwarding)을 이용하면 해결할 수 있습니다.

    void goo(int& i)
    {
    	cout << "int&" << endl;
    }
    
    void goo(int&& i)
    {
    	cout << "int&&" << endl;
    }
    
    template<typename T>
    void bar(T&& value) // 전달 참조
    {
    	goo(std::forward<T>(value));
    }
    
    int main()
    {
    	int num = 0;
    	
    	bar(num);
    	cout << endl;
    	bar(10);
    }

    bar를 함수 템플릿으로 작성하고 인수를 전달할 때 std::forward 함수를 이용합니다. 어떤 변수가 들어와도 원래 타입 그대로 전달하게 해주는 forward 함수의 정의는 다음과 같습니다.

    // FUNCTION TEMPLATE forward
    template <class _Ty>
    _NODISCARD constexpr _Ty&& forward(
        remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
        return static_cast<_Ty&&>(_Arg);
    }

    forward 함수에서 인수를 받을 때 불필요한 복사를 막기 위해 lvalue reference로 받고, value를 static_cast<_Ty&&>로 캐스팅해주는데요. reference collapsing을 통해 forward 함수에서 반환되는 값은 bar에서 넘기는 인수의 타입에 맞게 반환이 됩니다.

     

    최초에 lvalue를 넘기면 lvalue reference 타입으로 반환이 되고 rvalue를 넘기면 rvalue reference 타입으로 반환되는 것입니다. 예제에서 void bar(T&& value)의 T가 reference collapsing을 통해 상황에 따라 어떤 타입으로 변화되는지 알아볼 필요가 있습니다. 다음 코드를 실행해봅시다. <type_traits> 헤더를 추가해야 합니다.

    template<typename T>
    void bar(T&& value) 
    {
    	cout << "lvalue ref T : " << std::is_lvalue_reference_v<T> << endl;
    	cout << "rvalue ref T : " << std::is_rvalue_reference_v<T> << endl;
    }
    
    int main()
    {
    	int num = 0;	
    	bar(num);
    	cout << endl;
    	int& refNum = num;
    	bar(refNum);
    	cout << endl;
    	bar(std::move(num));
    }

    변수 num이나 refNum을 넘기면 타입 T는 lvalue reference가 되며 prvalue인 10을 넘기면 non-reference가 됩니다. 이 예제에선 int 타입을 의미합니다. lvalue reference도 결국 lvalue이기 때문에 num을 넘기나 refNum을 넘기나 똑같습니다.

     

    lvalue를 넘기면 T는 int&으로 바뀝니다. void bar(T&& value)가 void bar(int& && value)로 바뀌는 것이지요(T->int&). &와 &&가 만나면 &가 됩니다(reference collpase 규칙). 즉 void bar(int& value)가 최종 형태입니다. std::forward<T>(value)는 std::forward<int&>(value)가 됩니다. forward 함수에서 _Ty에 해당하는 타입이 int&로 바뀌게 되며 _Ty&&으로 되어 있는 부분은 int& &&가 되어 최종 타입은 int&가 됩니다.

     

    rvalue를 넘기면 T는 int가 되어야 합니다. int&&로 되어야 rvalue를 묶을 수 있으니까요. void bar(T&& value) 시그니처는 void bar(int&& value)로 바뀝니다(T->int). 이 경우 호출되는 std::forward<T>(value)는 std::forward<int>(value)이 되고, forward 함수에 int 타입이 전달되기 때문에 _Ty&&이 전부 int&&로 바뀌면서 rvalue reference가 반환됩니다.

     

    reference collapsing은 아래와 같이 정리할 수 있습니다. 

    case result
    T&& & T&
    T& && T&
    T& & T&
    T&& &&  T&&

     

    전달 참조라는 용어는 보편 참조라고 하는데 보편 참조는 Effective C++의 저자인  스콧 마이어가 만든 용어이며 공식적으로는 전달 참조가 맞습니다. 두 용어 모두 알아둡시다.

    '개발 > C·C++' 카테고리의 다른 글

    전역변수의 반환  (0) 2022.01.09
    shared_ptr 사용할 때 주의 사항  (0) 2022.01.09
    RVO, NRVO  (0) 2022.01.08
    constexpr  (0) 2022.01.06
    noexcept  (0) 2022.01.06

    댓글

Designed by Tistory.