lvalue 는 left-value 로, rvalue 는 right-value로 대부분 풀어쓰고 있다.
그래서 = (대입 연산자) 에서 왼쪽에 올 수 있는 값은 lvalue , 오른쪽에 올 수 있는 값은 lvalue 또는 rvalue 라고 정의하고 있다.
하지만 C++11 에서 std::move 와 std::forward 의 도입으로 이제는 다른 식으로 표현해야 할 필요성이 있음.
간단하게 표현하기 위해서, lvalue는 주소값을 읽어올 수 있는 객체 또는 이름이 있는 객체로,
rvalue는 이름이 없거나 주소값을 읽어올 수 없는 객체 라고 보면 된다.
1. int x = 3 에서 x 는 상수 3을 가지고 새로운 주소를 할당해 만들었으므로 (이름이 있으므로) lvalue
하지만 3은 주소가 있던놈이 아니므로 rvalue
2. const int y = x 에서, x가 lvalue 였으므로 y도 lvalue
3. int z = x + y 에서, x + y 는 x 와 y 를 더한값을 임시객체로 내놓고 z에 대입함
그러므로 x + y는 rvalue, z는 lvalue
4. string("abc") 는 들어갈 변수가 없음 또한 임시객체임, 고로 rvalue
5. Widget&& abc = returnWidget() 은 팩토리함수에서 임시객체를 생성받아 대입합니다. 임시객체 자체는 rvalue 이지만 abc가 할당받았습니다. (이름이 생김), 고로 abc가 Widget&& 형이여도 lvalue ( Widget& 형으로 변환됨 )
6. Widget&& abc2 = abc 는, abc2 로 abc를 참조하려고 하나, abc는 lvalue이기 때문에 가능하지 않음.
x는 lvalue
++x는 lvalue ( 전위 연산자는 자신을 증가시키고, 증가된 자신을 리턴함 )
x++는 rvalue ( 후위 연산자는 자신을 증가시키고, 증가되기 전의 임시객체를 리턴하기 때문에 )
그러므로 int&& 에 값을 넣는다면?
x++은 rvalue 이기때문에 가능하고
++x는 lvalue 이기때문에 불가능
C++11 표준에서, rvalue reference 가 등장했다.
lvalue reference는 lvalue만, rvalue reference 는 rvalue만 참조할 수 있습니다.
move semantics가 생겨나며, rvalue 참조자가 등장했습니다.
만약 사용자가 T라는 사용자 정의 타입을 만들고 그것을 배열로 선언한 후, 각 각 초기화 시켜야 한다면
for문을 통한 초기화가 가장 편할 것이다.
#include <iostream>
using namespace std;
class Widget{
private:
int* a;
public:
Widget() : a(nullptr) { }
Widget(int _data) : a(new int){
*a = _data; // int 크기 하나를 할당받고, 값을 _data로 채움
}
Widget(const Widget& rhs) : a(new int) {
*a = *rhs.a;
}
Widget& operator=(const Widget& rhs) {
if (a != nullptr)
delete a;
a = new int;
*a = *rhs.a;
return *this;
}
~Widget() { delete a; }
};
int main() {
Widget B[10];
for (int i = 0; i < 10; ++i)
B[i] = (Widget(i));
}
1. Widget(i) 를 통해 임시객체를 생성함
2. B[i] = (Widget(i)) 를 통해 Widget 의 대입연산자를 호출함
3. 새로운 메모리를 할당, 값을 대입, 기존 값 해제
4. 자기 자신을 반환
이를 통해, 임시 객체가 만들어질때마다 메모리를 할당하고, 그것을 다시 B[i] 에게 대입함.
그럼 대입연산자 내에선 또 자신의 메모리를 할당하고, Widget(i)의 값을 가져온 후, 다시 Widget(i)을 해제함
초기화 한번에 할당이 2번, 해제가 1번 일어나는 셈
이런 불필요한 연산을 없애자는게 Move Semantics
Widget(i)는 rvalue 이므로 ( 임시객체이므로 ) 어차피 임시로만 사용되고 다시 삭제될 것이기 때문에, 소유권을 이전시킨다 라고 한다.
이 소스를 다시 수정하면
#include <iostream>
using namespace std;
class Widget{
private:
int* a;
public:
Widget() : a(nullptr) { }
Widget(int _data) : a(new int){
*a = _data; // int 크기 하나를 할당받고, 값을 _data로 채움
}
Widget(const Widget& rhs) : a(new int) {
*a = *rhs.a;
}
Widget& operator=(const Widget& rhs) {
if (a != nullptr)
delete a;
a = new int;
*a = *rhs.a;
return *this;
}
Widget& operator=(Widget&& rhs) { // 이제 이동 대입 연산자가 있음
if (a != nullptr)
delete a;
a = rhs.a;
rhs.a = nullptr;
return *this;
}
~Widget() { delete a; }
};
int main() {
Widget B[10];
for (int i = 0; i < 10; ++i)
B[i] = (Widget(i));
}
Widget(i)는 i값에 맞춰 할당을 한번 할거고, Widget(i)가 rvalue 이기때문에
Widget& operator=(Widget&& rhs) 로 진입하게 됨
B[i]의 a는 rhs.a 의 주소값만 가져온 뒤, rhs는 이제 사용하지 않을 객체이기 때문에 nullptr로 바꿔줌
이렇게 할당 2번 + 해제 1번이 할당 1번으로 변경되었음
보다싶이, Move semantics는 객체의 리소스 (동적할당 된 변수) 를 다른 객체로 이동시키는 행위
어짜피 사용되지 않을 임시객체이니 리소스 소유권만 이동시키고 불필요한 메모리 할당과 복사를 막자는 것
이 처럼 이동 생성자 또한 가능하다.
void func(const Widget& rhs) {
cout << "const Widget&" << endl;
}
void func(Widget&& rhs) {
cout << "Widget&& " << endl;
}
int main() {
Widget A;
func(A);
func(Widget());
}
다음과 같은 함수가 있다고 해보자 ( Widget는 위에 있는 Widget 그대로 활용 )
func(A); 와 func(Widget()); 는 어떤 함수가 호출될까?
당연히 func(A)는 이름 있는 객체이므로 func(const Widget& rhs) 함수가 호출될 것이고,
func(Widget())는 무명 객체(임시 객체)이므로 func(Widget&& rhs) 함수가 호출될 것이다.
( 사실 func(Widget()) 은 func(const Widget& rhs) 버전도 호출 가능하나, 명확한 func(Widget&& rhs)
가 더 높은 우선순위에 오르게 된다. )
하지만 std::move 함수를 사용하면 A를 rvalue로 캐스팅 할 수 있게되는데,
이는 static_cast<Widget&&>(A) 와 같다. ( std::move 함수가 이와 같이 작성되어 있다. )
void func(const Widget& rhs) {
cout << "const Widget&" << endl;
}
void func(Widget&& rhs) {
cout << "Widget&& " << endl;
}
int main() {
Widget A;
func(std::move(A)); // 이제 func(Widget&& rhs) 버전이 호출됨
func(static_cast<Widget&&>(A)); // std::move 와 같음
}
std::move 는 결국 lvalue 가 더 이상 필요 없을때, 소유권을 이전시킬 수 있게 해준다.
rvalue를 함수내에서 받았을땐, lvalue처럼 취급함.
사실 이건 auto 때도 한번 작성했던 내용인데, 한번 더 작성함.
위의 Widget 이동 연산자 내에서의 rhs는 rvalue로 받았지만, lvalue 처럼 작동함.
그 이유는 rvalue reference 를 통해 Widget 의 임시객체를 전달받았지만, rhs 를 사용할 수 있을 때부터,
그 것은 주소공간에 이미 저장된 (이름이 붙여진) 변수이기때문에 lvalue 와 동일하게 작동함.
이는 Reference Collapsing Rule 과 연결됨
<Reference Collapsing Rule>
lvalue reference와 rvalue reference, 두 종류의 레퍼런스 종류로 인해
4가지의 레퍼런스의 레퍼런스 조합이 존재할 수 있다.
- & & (L + L)
- & && (L + R)
- && & (R + L)
- && && (R + R)
template <typename T>
void func(T&& _argv) {
// working...
}
int main() {
Widget A;
func(A); // 1. lvalue 이것은 곧 Widget& &&_argv와 같다.
func(Widget()); // 2. rvalue 이것은 곧 Widget&& &&_argv와 같다.
}
1. 규칙에 의해, A는 lvalue, func에 들어간다면 template 추론으로 의해
void func(Widget& &&_argv) 가 호출이 되고 이것은 func(Widget& _argv)와 같다. ( L + R )
2. 규칙에 의해, func(WIdget())는 rvalue, func에 들어간다면 template 추론으로 의해
void func(Widget&& &&_argv) 가 호출이 되고 이것은 func(Widget&& _argv) 와 같다. ( R + R )
[ 만약 이 글만으로도 이해가 되지 않는다면, 더 좋은 예시가 있으니 확인 바람 ]
★링크 : https://m.blog.naver.com/devmachine/176442133
'C++ > Modern' 카테고리의 다른 글
C++11) 람다식 (Lambda Expression) 과 std::function (4) | 2019.02.02 |
---|---|
C++11) std::forward (perfect forwarding) (4) | 2019.02.02 |
C++11) auto 키워드 (0) | 2019.01.30 |
C++11) 범위 기반 for문 (loop) (4) | 2019.01.27 |
C++11) 초기화 리스트(initialize_list) (2) | 2019.01.27 |