본문 바로가기

# Lang/C++

4.3 우측값 참조 (rvalue reference)


선행 지식 :  

 

4.2 값의 유형 (glvalue, prvalue, xvalue)

값의 유형 (Value Category) 값의 분류기준 : have identity? 값이 식별성을 갖고 있는가? can be moved from? 값이 메모리에서 이동할 수 있는가? 기본적인 분류단위를 Primary Category라고 하고, 두 개 이상..

aerocode.net

 


 1. 우측값 참조변수  (rvalue reference variable)


prvalue 또는 xvalue 값을 참조하는 변수.


일반 참조는 선언문에서 lvalue를 요구하지만,  // 좌측값 참조.

우측값 참조는 rvalue를 요구한다.



example : 

	int val = 3;

	int &	l_ref = val;    // OK.  val은 lvalue.
	int &&	r_ref = 3;    // OK.  3은 rvalue.





 2. 우측값 참조의 효과

rvalue가 이름을 갖게 함으로써 아래의 효과가 발생한다.



우측값 참조변수를 선언하면 :

  • 우측값 참조변수는 이름이 있으므로, lvalue로 취급된다.

  • lvalue에는 주소를 취할 수 있으므로, 임시객체의 주소를 얻을 수 있다.

  • 표현식이 끝나더라도 rvalue가 소멸되지 않는다.


example : 

    // rvalue reference 선언.
    int &&r_ref = 3;
    
    
    // 표현식이 끝나더라도 rvalue는 r_ref에 참조되어,
    // rvalue의 생명주기가 연장된다.
    cout << r_ref * r_ref << endl;
    
    
    // r_ref는 변수의 이름이므로 lvalue이다.
    // lvalue는 주소를 얻을 수 있다.
    cout << &r_ref << endl; 
    





 3. 우측값 참조는 왜 쓰는가? 

  • 우측값을 재활용 할 수 있게 도와준다.

우측값은 한번 쓰고 버리는 값이므로,  표현식이 벗어나면 사라지지만.

우측값을 참조하면 소멸을 방지하고 재활용하는 효과를 일으킨다.


아래는 우측값 참조를 쓰지않아 문제가 발생하는 예시이다.

콜백함수의 전, 중, 후 마다 객체의 주소를 출력한다.



example (잘못된 예) :

/// @brief  생성비용이 비싼 데이터 박스 클래스.
///
class DataBox {
private:
    //! 비싼 생성비용의 원인.
    int data[100];


public:
    //! 기본 생성자.
    DataBox() {
        printLog("생성자");
    }


    //! 현재 객체의 주소를 출력하는 로깅함수.
    void printLog(const string &tag) {
        printf("%-10s : %p \n", tag.c_str(), this);
    }
};


/// @brief  입력받은 박스를 리턴하는 콜백함수.
///
DataBox loopback(DataBox dataBox) {
    dataBox.printLog("콜백함수 안");
    return dataBox;
}


int main() {
    loopback( DataBox() ).printLog("콜백함수 후");
}

result : 

생성자  : 0x7fff0c70a0e0 

콜백함수 안 : 0x7fff0c709f20 

콜백함수 후 : 0x7fff0c70a400 


전부 서로다른 객체가 로깅되었다.


비참조값은 rvalue이므로, 각자 자신만의 임시객체를 생성했기 때문이며,

새로운 임시객체를 만들때마다, 꽤나 비싼 비용을 지불했을 것 이다.  (int[100])




새로운 객체를 생성하지 않으려면, 

이미 만들어진 객체의 참조 값을 리턴해야 한다.


콜백함수의 인자형식과 반환형식을 참조로 바꾸자.

인자의 값은 우측값이므로 &&로 가져오도록 하고,

참조변수의 이름은 좌측값이므로 &로 반환한다.



example (올바른 예) :

/// @brief  입력받은 박스를 리턴하는 콜백함수.
///
DataBox& loopback(DataBox&& dataBox) {
    dataBox.printLog("콜백함수 안");
    return dataBox; // dataBox는 이름을 가지므로 lvalue.
}

result :

생성자  : 0x7ffd5a71cf80 

콜백함수 안 : 0x7ffd5a71cf80 

콜백함수 후 : 0x7ffd5a71cf80





 4. 좌측값을 우측값 참조로  (std::move)

위 예시의 콜백함수를 다시 살펴보자.



example (수정 전) :

/// @brief  입력받은 박스를 리턴하는 콜백함수.
///
DataBox& loopback(DataBox&& dataBox) {
    dataBox.printLog("콜백함수 안");
    return dataBox; // dataBox는 이름을 가지므로 lvalue.
}


인자로 받은 우측값을 dataBox에 바인딩했지만,

우측값이 이름을 가졌으므로, 좌측값으로 반환했다.


일관성을 위해 move()함수를 사용하여,

좌측값을 다시 우측값으로 바꾼뒤 반환하자!



example (수정 후) :

/// @brief  입력받은 박스를 리턴하는 콜백함수.
///
DataBox&& loopback(DataBox&& dataBox) {
    dataBox.printLog("콜백함수 안");
    return std::move(dataBox);  // return static_cast<databox&&>(dataBox); 와 같다.
}





 5. 일반 변수도 우측값 참조로?

move()를 통해 좌측값을 우측값 참조로 변환할 수 있었다.

그렇다면 어떤 상황에서 move()를 적용해야 될까?


우측값이란 잠시 만들어지고 버려지는 값에 가깝기 때문에,

일반변수도 쓰고 버릴거라면, 우측값으로 캐스팅하여 표시해둘 수 있다.




좌측값으로 준다면, 아직 사용할 값이라는 뜻이고.

우측값으로 준다면, 쓰고 버릴것이라는 뜻을 전달할 수 있다.


물론 문맥별 동작을 구현하는것 또한 프로그래머의 일이다.





'# Lang > C++' 카테고리의 다른 글

5.2 완벽한 전달  (0) 2019.04.19
5.1 이동 의미론  (0) 2019.04.18
4.2 값의 유형 (glvalue, prvalue, xvalue)  (0) 2019.04.11
4.1 표현식  (0) 2019.04.10
3.2 기억영역 클래스 지정자  (0) 2019.04.06