c++ 11에서 부터 지원되는 이동 연산자에 대해서 알아 봅시다.
우선 이동 연산자가 왜 필요하게 되었는지에 대해서 보기전에 lvalues와 rvalues에 대해서 알아 봅시다.
Lvalues 와 Rvalues
msdn에 설명되어 있는 Lvalues와 Rvalues를 참조해서 설명드립니다.
c++의 모든 표현은 타입을 가지며 값 카테고리에 속합니다.
값 카테고리는 컴파일러가 생성, 복사, 이동 임시 객체들을 표현할 때 따라야하는 규칙을 기반으로 만들어집니다.
lvalue
lvalue는 접근할 수 있는 주소를 가진 변수를 말합니다. 이동 연산을 할 수 없습니다.
- 이름이 있는 변수, 함수
- int a;
- &foo()
- 선행 증감 연산자
- ++a, --b
- lvalue 배열의 인덱스 접근
- l[n]
- string 리터럴
- "hello move sementics"
- 등등
prvalue
prvalue는 접근은 할 수 있지만 주소를 가지지 못한 표현식을 말합니다. 이동 연산이 가능합니다.
pure rvalue라고도 합니다.
- 리터럴 값
- 1, 1.3f
- sting 리터럴 값은 제외
- 후행 증감 연산자
- a++, b--
- 값 리턴 함수 호출
- return str1 + str2; 리턴 타입이 값인 함수
- 비 참조 캐스팅 ( static_cast<double>(x), (int) 42 같은)
- 등등
xvalue
xvalue는 접근 할 수 있는 주소를 가지지만 이동 연산을 할 수 있습니다.
이동 연산 후에는 객체의 안정성을 보장 하지 않습니다.
- rvalue 참조를 리턴하는 함수
- std::move(x)
- rvalue 배열의 인덱스 접근
- r[n]
- rvalue 참조 캐스팅 ( 같은)
- static_cast<int&&>(i)
- 등등
rvalue
prvalue + xvalue를 묶어서 rvalue라고 합니다.
glvalue
lvalue + xvalue를 묶어서 glvalue라고 합니다.
예 제
int tmain(int argc, _TCHAR* argv[])
{
int num1 = 5, num2 = 3;
num1 = 10; // num1은 lvalue, 10은 int형 prvalue
num1 = num2; // num1은 lvalue, num2은 lvalue
int num3 = num1 + num2; // num3은 lvalue, num1 + num2은 prvalue
std::move(num3); // rvalue 참조를 리턴 xvalue
static_cast<DWORD>(num1) // 값으로 캐스팅 lvalue
static_cast<DWORD&&>(num1) // rvalue 참조로 캐스팅 xvalue
}
간단하게 rvalue와 lvalue 구분하려면 접근 할 수 있는 주소가 있는지 확인 합니다.
주소가 없다면 rvalue라고 할 수 있고 만약에 주소가 있다면 rvalue 참조타입으로 캐스팅 되어있는지 확인 합니다.
rvalue 참조로 캐스팅 되었다면 xvalue라고 할 수 있고 아니라면 lvalue에 속합니다.
https://docs.microsoft.com/ko-kr/cpp/cpp/lvalues-and-rvalues-visual-cpp?view=vs-2019
https://en.cppreference.com/w/cpp/language/value_category
Move semantics
c++11에서는 lvalue와 rvalue를 구분한 파라미터를 전달받아 처리 할 수 있습니다.
이렇게 지원을 하게 된 이유는 효율성을 증가시키기 위함입니다.
그럼 어떻게 효율성을 증가 시킬 수 있을까요? 예제를 통해서 설명 드리겠습니다.
using namespace std;
struct Person
{
string name;
int* year = nullptr;
Person()
{}
Person(string p_name, const int p_year) : name(p_name), year(nullptr)
{
year = new int(p_year);
cout << "constructed" << endl;
}
Person(const Person& other) noexcept :
name(other.name), year(nullptr)
{
year = new int(p_year);
cout << "copy constructed" << endl;
}
Person(Person&& other) noexcept :
name(move(other.name)), year(nullptr)
{
// Rvalue의 힙에서 할당된 year를 이동시킵니다.
year = other.year;
// Rvalue의 year를 nullptr 초기화 시킵니다.
other.year = nullptr;
cout << "move constructed" << endl;
}
Person& operator=(const Person& other) noexcept
{
if (this != &other)
{
this->name = other.name;
this->year = new int(*other.year);
}
cout << "copy Assignment operator" << endl;
return *this;
}
Person& operator=(Person&& other) noexcept
{
if (this != &other)
{
this->name = std::move(other.name);
if (this->year) delete this->year;
this->year = other.year;
other.year = nullptr;
}
cout << "move Assignment operator" << endl;
return *this;
}
virtual ~Person()
{
if (nullptr != year)
{
delete year;
}
cout << "destructed " << endl;
}
};
int main()
{
vector<Person> Container;
cout << "call push_back(Person(\"Ahn\", 1985))" << endl;
Container.push_back(Person("Ahn", 1985));
return 0;
}
main 함수의 3라인을 보면 vector의 push_back값으로 "Person("Ahn", 1985)" Rvalue를 넘기고 있습니다.
이렇게 호출을 하면 Person의 Person(Person && other) 이동 연산자가 호출되게 됩니다.
Rvalue의 경우 앞서 말한대로 해당 실행라인이 지나면 무효화 되는 임시객체라고 설명을 드렸는데
임시 객체에 대해서 깊은 복사를 하는 것은 비효율적으로 보입니다.
어짜피 없어질 객체이니 얕은 복사를 통해서 데이터를 이동시키면 효율적으로 작동 할 수 있습니다.
msdn의 이동 생성자 및 이동 할당 연산자 정의 방법입니다.
'c++ > modern c++' 카테고리의 다른 글
[c++] auto와 decltype 키워드 (0) | 2020.03.15 |
---|---|
[c++] std::move와 std::forward (4) | 2020.03.07 |
[c++] 람다(Lambda) 표현식 (0) | 2020.03.04 |
[c++] weak_ptr (1) | 2020.03.02 |
[c++] shared_ptr (0) | 2020.02.29 |