선행 지식 :
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 |