Rvalue Reference는 c++ 11에서 성능 개선을 위해 추가된 문법이다.
형태는 다음과 같다.
int&& a = 3;
이 문장은 rvalue인 3을 a라는 lvalue로 참조한다는 뜻이다.
이를 확실히 이해하기 위해서는 rvalue,lvalue를 구분하는 c++의 value category를 이해할 필요가 있다.
c++의 표현식(expression)은 크게 2가지 분류, 작게는 3가지 분류로 나눌 수 있다.
나누는 기준은 expression이 move가능하면 rvalue, identity가 있다면 glvalue로 나뉘고 다시 glvalue가 move가능하면 xvalue 아니면 lvalue, rvalue가 identity가 있다면 xvalue 아니면 prvalue로 나뉜다.
Identity가 있다는 의미는 microsoft사이트에서 찾을 수 있었다.
What does it mean for a value to have identity? If you have (or you can take) the memory address of a value, and use it safely, then the value has identity 출처
move가 가능하다는 의미는 move semantic에 의해 해당 데이터 영역을 가리키는 변수가 변한다는 의미이다.
말보다는 위에 그림을 참고하는것이 더 이해가 잘 될것이다.
중요한 점은 move를 한다는 것이 '메모리 공간을 움직이는 것'이 아니라 '메모리 공간의 이름을 바꾸는 것'이라는 것이다. 참 헷갈리는 개념이다.
int a = 0;//a는 lvalue, 0은 prvalue
int& b = a;//a,b모두 lvalue
Obj o = Obj();//o는 lvalue, Obj()는 prvalue
int&& c = std::move(a);//std::move(a)는 xvalue, c는 lvalue
&& 기호가 바로 c++11에서 추가된 rvalue reference기호이다.
rvalue reference타입으로 선언된 변수는 lvalue이며 해당 rvalue가 저장된 메모리 영역을 참조할 수 있다.
int a = 0;
int& b = a;//b는 lvalue reference
int&& c = a;//컴파일 에러(rvalue reference는 rvalue만 받을수 있다)
int&& c = a + 1;//c는 a+1의 계산결과 생성되는 임시데이터를 참조한다(rvalue reference)
cout << c << endl;//1출력(본래 rvalue는 표현식이 끝나고 메모리에서
//삭제되지만 c라는 변수가 참조해서 int데이터가 메모리에 남아있다)
c++11의 rvalue reference문법을 이용해서 Move Semantics구현이 가능하다.
Move Semantics:어떤 변수의 값을 새로운 변수에 할당한다고 할 때,
새로운 메모리 영역을 할당받는 것이 아니라 어떤 변수의 기존 메모리 영역을 새로운 변수의 메모리 주소로 의미를 '이동'시키는 방법
Obj.h
#pragma once #include <iostream> using namespace std; class Obj { public: Obj(int num);//기본 생성자 Obj(const Obj& obj);//복사 생성자 Obj(Obj&& obj);//이동 생성자 Obj& operator=(const Obj& obj); Obj& operator=(Obj&& obj); void print(); ~Obj();//소멸자 private: int* a;//과부하 테스트용 뻥 데이터 };
Obj.cpp
#include "Obj.h" Obj::Obj(int num) { a = new int[10000000]; for (int i = 0; i < 100000; i++) a[i] = num; cout << "기본 생성자 호출" << endl; } Obj::Obj(const Obj& obj) { a = new int[10000000]; for (int i = 0; i < 10000000; i++) { a[i] = obj.a[i]; } cout << "복사 생성자() 호출" << endl; } Obj::Obj(Obj&& obj) { cout << "이동 생성자() 호출" << endl; a = obj.a; obj.a = nullptr; } Obj& Obj::operator=(const Obj& obj) { a = new int[10000000]; for (int i = 0; i < 10000000; i++) { a[i] = obj.a[i]; } cout << "복사 연산자= 호출" << endl; return *this; } Obj& Obj::operator=(Obj&& obj) { a = obj.a; obj.a = nullptr; cout << "이동 연산자= 호출" << endl; return *this; } void Obj::print() { cout << a[0] << endl; } Obj::~Obj() { delete[] a; cout << "소멸자 호출" << endl; }
main.cpp
#include <iostream> #include <time.h> #include "Obj.h" using namespace std; int main() { clock_t start, end; start = clock(); Obj aaa(3); //Obj bbb(Obj(3));//copy elision 규칙에 의해서 이동 생성자를 호출하지 않음 //Obj bbb1(aaa);//복사 생성자 호출(deep copy) //Obj bbb2(move(aaa));//이동 생성자를 명시적으로 호출하는 방법(shallow copy) //aaa.print();//런타임 에러(move를 호출한 이후 반환된 rvalue reference를 다른 변수에 할당하면 aaa는 존재하지 않음) end = clock(); cout << "걸린 시간" << end - start << endl;//복사생성자 : 50ms, 이동생성자:13ms return 0; }
std::move(lvalue)함수는 lvalue를 rvalue reference로 type casting해주는 함수이다. 이 함수덕분에 lvalue를 rvalue reference로 전환시켜서 이동생성자를 호출할 수 있다. 참고로 move(aaa)는 xvalue이다.
예시는 새로운 Obj객체를 생성할 때,복사생성자로 deep copy를 하는 경우랑 이동생성자로 shallow copy를 하는경우 성능차를 계산하는 코드이다(실험결과를 잘 알아보기 위해 의도적으로 1000000개의 int array를 Obj객체에 동적할당하였다). 결과는 복사생성자는 50ms 이동생성자는 13ms로 이동생성자가 확실히 더 빨랐다.
이 실험에서 몇가지 재미있는 사실도 발견하였다.
첫번째는 Obj bbb(Obj(3))처럼 prvalue를 인자로 넘겼을때 vc컴파일러가 copy elision규칙에 의해서 이동 생성자의 호출을 무시하였다는 것이다. 최신식 컴파일러의 최적화 기법이라고 한다. copy elision은 이 글의 주제에 맞지 않으니 여기까지만 알아봤다.
두번째는 rvalue reference로 인해 의미를 도둑맞아버린 aaa변수에 다시 접근하면 런타임 에러가 난다는 것이다. aaa라는 단어의 의미는 본래 특정 메모리 영역을 의미했지만 bbb2라는 단어가 그 특정 메모리 영역을 의미하게 바꾸어 버렸으니 이렇게 될 수 밖에 없나보다.
C++11의 Rvalue Reference문법은 Move Semantics를 가능하게 하고
Move Semantics는 불필요한 복사방지를 실현해서 성능 향상을 기대할 수 있다. 하지만, 그렇다고 무조건 Move Semantics를 쓰는것이 좋은것인가? 라고 물어본다면 그렇지 않다. Move Semantics는 분명 성능상의 이점은 있지만, 문법적으로 어렵고 복잡하다는 단점이 있다..(예를 들어,move(lvalue);로 의미가 이동당한 lvalue의 소멸자가 멤버 포인터 변수 대상으로 delete하지 않게 하기 위해 lvalue의 멤버 포인터를 nullptr로 바꿔야 한다)
오래 전 글이신거 같지만 지나가다 질문 올립니다.
이동 생성자의 구현 부분에서 obj.a 를 nullptr로 바꾸신 거 같은데,
Obj::Obj(Obj&& obj)
{
cout << "이동 생성자() 호출" << endl;
a = obj.a;
obj.a = nullptr;
}
프린트의 구현 이 이 a를 사용하기 때문에
void Obj::print()
{
cout << a[0] << endl;
}
이렇게 되면 마지막 main.cpp 에시에서
//aaa.print();
는 당연히 런타임 에러가 날수밖에 없지 않나요?
두번째는 rvalue reference로 인해 의미를 도둑맞아버린 aaa변수에 다시 접근하면 런타임 에러가 난다는 것이다. aaa라는 단어의 의미는 본래 특정 메모리 영역을 의미했지만 bbb2라는 단어가 그 특정 메모리 영역을 의미하게 바꾸어 버렸으니 이렇게 될 수 밖에 없나보다.
마지막 요 문장이 잘못된 결론이 아닌가 하여 질문 드려봅니다