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