값(Value Category)
C++ 에는 왼 값(L-Value)과 오른 값(R-Value)이 있다.
왼 값(L-Value)
- 메모리 위치를 가지며, 수정이 가능하다.
- 단일식을 넘어서 계속 지속되는 개체이다.
- 일반적으로 변수나 개체를 나타낸다.
아래 코드에서, 변수 x와 y는 L-Value이다.
변수 x는 메모리 주소를 가지며, 값을 읽고 수정할 수 있다.
int main()
{
int x = 10; // x = L-Vlaue, 10 = R-Value
int y = x; // y = L-Value, x = L-Value
}
오른 값(R-Value)
- 임시 값으로서, 메모리에 저장되지 않고 즉시 사용된다.
- 주소가 없는 객체, 임시 개체, 열거형, 람다, i++ 등
int main()
{
// 10 = 3은 안된다. 10이라는 값은 메모리 위치를 가지지 않으며, 값도 수정할 수 없다.
// 따라서, 10은 R-Value이다.
int x = 10;
// x + 5라는 값은 새로 생성된 임시 값이다. 따라서 이도 R-Value이다.
int y = x + 5;
// i는 R-Value이다. for문이 끝나면 사라지는 임시 객체이기 떄문이다.
for (int i = 0; i < 6; i++) { }
}
L-Value와 R-Value의 예시
Struct Player
{
};
int Function() { return 0; }
int main()
{
int count = 0;
count; // LValue.
0; // RValue.
Player player;
player; // LValue.
Player(); // RValue.
Function(); // RValue.
}
왼 값 참조(L-Value Reference)
- 왼 값(L-Value)을 참조하는 데 사용한다.
- 표준 C++ 문법에서 래퍼런스로 지칭하는 것이 L-Value Reference이다.
int main()
{
int x = 10;
int& ref = x; // ref = L-Value Reference
int& ref2 = 10; // 오류 발생.
// L-Value를 참조해야 하는데, 10은 R-Value이므로 오류가 발생한다.
}
오른 값 참조(R-Value Reference)
- 오른 값 참조(R-Value Reference)는 C++11부터 도입되었다.
- R-Value Reference는 R-Value를 참조하는 데 사용한다.
- L-Value Reference와 구분하기 위해서 &대신 &&을 사용해 선언한다.
- R-Value Reference는 임시 값을 다른 어딘가에 저장하는 목적으로 많이 활용된다.
- 이때, 값을 복사(Copy)하는 대신 이동(Move)하는 방법을 사용해서 속도를 향상할 수 있다.
int main()
{
int& lref = 10; // 오류 발생
int&& rref = 10;
}
L-Value Reference와 R-Value Reference의 예시
int main()
{
int count = 0;
int& lvalue1 = count;
int& lvalue2 = 10; // Error.
int&& rvalue1 = 10;
int&& rvalue2 = count; // Error.
}
상수
L-Value는 메모리 위치를 가지며, 수정이 가능한 값을 일컫는다.
그러면, const가 붙은 변수는 R-Value인가?
- 상수는 값을 변경할 수 없기 때문에 R-Value로 오해할 수 있다.
- const가 붙은 변수도 변수명과 메모리 위치를 가지며, 값을 변경할 수는 없지만, 읽기는 가능하다.
- 따라서 상수는 R-Value가 아니라 L-Value이다.
- 하지만, 상수는 단순한 L-Value가 아니라 값을 변경할 수 없는 Immutable L-Value라고 한다.
- C++은 상수를 Immutable L-Value로 분류한다.
int main()
{
const int number = 10;
number = 20; // 오류.
// number = Immutable L-Value이다.
}
오른 값 참조(R-Value Reference)
C++98, C++03을 보자.
이때에는 데이터를 옮길 때, 복사를 통해 데이터를 옮겼다.
동적 배열(vector)로 예를 들면, vector의 용량이 꽉 차서 용량을 확장할 때, 기존 요소들을 모두 복사하여 새 메모리로 옮겼다.
vector 같은 경우에는, 새 메모리로 데이터를 모두 옮기고 나서 기존의 데이터는 필요가 없기 때문에 모두 메모리를 해제해야 한다.
이 과정에서 메모리의 할당, 복제, 해제가 일어나기 때문에 성능에 많이 영향을 미치게 된다.
R-Value Reference의 도입
- C++ 11 이후, R-Value Reference가 도입되면서 이동(Move)이라는 개념이 생겼다.
- 복사가 아닌 메모리 상의 이동으로, 실제로는 이동하지 않지만 포인터 변수를 저장하여 이를 이동시키는 것이다.
- 이동(Move) 시, 이동에 사용된 값은 제거된다.
- 그렇기 때문에, 데이터를 이동해도 문제없는 R-Value를 대상으로만 사용할 수 있다.
- Move에 의해서 메모리 할당, 복사, 해제를 줄일 수 있어서 프로그램의 성능을 향상할 수 있다.
Move Semantics
- Move Semantics는 메모리상의 이동을 의미한다.
- Move Semantics를 사용하기 위해 Move 생성자, Move 대입 연산자를 도입했다.
- Move 생성자와 Move 대입 연산자는 암묵적으로는 생성되지 않는다.
- 클래스를 정의할 때 Move 생성자와 Move 대입 연산자를 정의하면 Move Semantics를 사용할 수 있다.
- Move 생성자와 Move 대입 연산자의 매개변수는 R-Value Reference(&&)이다.
- Move 생성자와 Move 대입 연산자의 매개변수는 제거돼야 하기 때문에, const를 붙여선 안된다.
- 복사 생성자가 Move 생성자보다 우선순위가 높다.
- 대입 연산자가 Move 대입 연산자보다 우선순위가 높다.
C++ 98 / C++ 03에서의 클래스 정의
class Player
{
public:
// 기본 생성자.
Player() { }
// 복사 생성자.
Player(const Player& other) { }
// 대입 연산자.
Player& operator=(const Player& other) { }
};
C++ 11 이후의 클래스 정의
Move 생성자와 Move 대입 연산자의 매개변수는 제거되기 때문에 const가 붙지 않는 것을 확인할 수 있다.
class Player
{
public:
// 기본 생성자.
Player() { }
// 복사 생성자.
Player(const Player& other) { }
// 대입 연산자.
Player& operator=(const Player& other) { }
// Move 생성자.
Player(Player&& other) { }
// Move 대입 연산자.
Player& operator=(Player&& other) { }
};
복사 생성자와 Move 생성자 비교
깊은 복사를 한 복사 생성자와 Move 생성자를 비교해 보자.
복사 생성자에서는 char* 타입의 name을 깊은 복사하기 위해서 새로 동적할당하는 모습이다.
반면, Move 생성자에서는 name의 주소값을 옮겨버리고, nullptr로 초기화하는 모습이다.
물론 복사 생성자는 복사에 사용한 데이터가 유지되지만, Move 생성자는 매개변수가 제거된다.
class Player
{
public:
// 복사 생성자 (깊은 복사)
Player(const Player& other)
: playerID(other.playerID)
{
size_t length = strlen(other.name) + 1;
name = new char[length];
strcpy_s(name, length, other.name);
}
// Move 생성자
Player(Player&& other)
{
playerID = other.playerID;
name = other.name;
other.name = nullptr;
}
int playerID;
char* name;
};
복사 대입 연산자와 Move 대입 연산자 비교
복사 생성자와 Move 생성자 비교와 크게 다를게 없다.
class Player
{
public:
// 복사 대입 연산자 (깊은 복사)
Player& operator=(const Player& other)
{
if (this == &other) { return *this; }
// 새 메모리 동적 할당
if (this->name == nullptr)
name = new char[50];
playerID = other.playerID;
strcpy(name, other.name);
return *this;
}
// Move 대입 연산자
Player& operator=(Player&& other) noexcept
{
if (this == &other) { return *this; }
// 이미 할당받은 메모리가 있으면 해제
if (this->name != nullptr)
delete this->name;
playerID = other.playerID;
name = other.name;
other.name = nullptr;
return *this;
}
int playerID;
char* name;
};
std::move
- L-Value를 R-Value Reference로 하는 것은 불가능하다.
- 기본적으로 R-Value만 R-Value Reference가 가능하다.
- L-Value를 형변환하면 R-Value Reference로 만들 수 있다.
- L-Value를 R-Value로 형변환하는 것이 바로 std::move이다.
int main()
{
int lValue = 200;
int&& rValueRef = std::move(lValue);
}
std::move를 사용할 때 주의할 점
- L-Value가 R-Value가 되어 임시 객체로 취급 받는다.
- std::move를 사용한 후의 객체의 데이터는 보증할 수 없다.
- std::move에 사용된 객체는 메모리상 이동을 했기 때문에, 이를 추가로 사용하며 생기는 사이드이펙트는 보증하지 못한다.
'📕Programming > 📝C/C++' 카테고리의 다른 글
| [C / C++] 스마트 포인터(Smart Pointer) (0) | 2023.12.06 |
|---|---|
| [C / C++] 완벽 전달(Perfect Forwarding)과 std::forward (0) | 2023.12.05 |
| [C / C++] delete (삭제된 함수) (0) | 2023.12.01 |
| [C / C++] enum class (0) | 2023.11.29 |
| [C / C++] typedef / using (0) | 2023.11.29 |