L- value 와 R - value / move
▶ L - value 와 R - value
ㆍ 프로그래밍 언어에서 변수와 값의 메모리 상태, 수명에 관련된 개념.
1. L - value
ㆍ 메모리의 특정 주소를 가지며, 그 주소에 저장된 값을 참조할 수 있는 표현. 즉, 변수나 객체와 같이 메모리에 위치한 값을 나타낸다.
ㆍ L-value는 변경 가능한 상태를 가질 수 있다. 예를들어 변수, 배열의 요소, 역참조된 포인터 등이 L-value다.
int x = 5; // x는 L-value
x = 10; // x의 값을 변경
2. R - value
ㆍ R - value는 메모리 주소를 가지지 않으며, 값 자체를 나타내는 표현, 주로 리터럴이나 임시 객체, 연산결과가 R벨류다.
ㆍ 일반적으로 변경할 수 없는 상태를 가진다, 이동생성자와 이동 대입 연산자를 통해 소유권을 효율적으로 이전할 수 있다. move키워드를 사용하면 R-value로 변환할 수 있다.
ㆍ lvalue가 아닌 나머지는 rvalue다 (임시값, 열거형, 람다, i++ 등등...)
int x = 5; // 5는 R-value
int y = x + 2; // x + 2는 R-value
▶ L-value 참조와 R-value 참조
ㆍ c++11부터는 L-value와 R-vlaue를 명확하게 구분하기 위해 L-value참조(&)와 R-value 참조(&&) 개념이 도입되었다.
1. L-value참조
ㆍ L-value를 참조하는 포인터, 즉 메모리의 특정 위치를 가리키며 해당 위치에 있는 데이터를 수정할 수 있다.
void increment(int& n) //L - value참조 매개변수
{
n++;
}
int main()
{
int a = 5;
increment(a); //a는 L-value로 call - by - ref
cout << a; //6출력
return 0 ;
}
2. R-value 참조(&&)
ㆍ R-value참조는 R-value를 참조하는 포인터다..... ? 예시를 보자
void process(int&& rvalueRef) //R-value 참조 매개변수
{
cout << rvalueRef << endl;
}
int main()
{
process(10); //R-value 전달
int x = 5;
process(std::move(x)); // std::move를 통해 R-value로 전환후 전달
// 10과 5가 출력
return 0;
}
ㆍ R-value란 임시 객체를 의미한다 했다 (리터럴정수 전달)
※ r-value에 참조하고 싶으면 &&키워드를 사용하면 된다.
ㆍ 이렇게 하면 임시 객체의 수명이 연장된다. 함수의 매개변수로 r벨류 참조를 사용하면 함수가 종료될 때 까지 임시 객체가 소멸하지 않는다.
ㆍ r벨류 참조는 자원을 효율적으로 관리할 수 있다. 예를들어 무브시멘틱(std::move) 을 통해 객체의 소유권을 이전할 수 있어 불필요한 복사를 방지할 수 있다.
ㆍ 주로 이동 생성자와 이동 대입 연산자를 정의하는데 사용된다.
특성 | L-value참조 (&) | R-value 참조(&&) |
참조대상 | L-value (변수, 배열 요소 등) | R-value(임시객체, 리터럴 등) |
주소를 가질 수 있음 | 예 | 아니요 |
수정 가능 | 예 | 아니요(자원 이동 가능(move)) |
사옹 예 | 변수 전달, 수정 | 임시 객체 처리, 무브시멘틱 |
▶ move키워드란?
ㆍ 객체를 r-value로 변환하는함수로, 이 함수를 사용해 객체의 자원을 다른 객체로 "이동" 할 수 있다.
#include <iostream>
#include <utility> // move는 여기에 포함되어있다.
using namespace std;
class Resource
{
public:
Resource() { cout << "리소스 생성\n"; }
~Resource() { cout << "리소스 해제\n"; }
//이동생성자
Resource(Resource&& other) noexcept // <-예외를 던지지 않는 키워드
{
cout << "리소스 이동\n";
//자원 이동 로직
}
//이동 대입 연산자
Resource& operator=(Resource&& other) noexcept
{
cout << "Resource move assigned\n";
//기존 자원 해제 및 자원 이동 로직
return *this
}
}
int main()
{
Resource res1; //자원 할당
Resource res2 = move(res1); //이동 생성자 호출
Resource res3;
res3 = move(res2); //이동 대입 연산자 호출
return 0;
}
/*
위 코드에서 move의 역할
1. 이동 생성자 호출 : Resource res2 = move(res1) 에서 move키워드는 res1을 R-value로
변환 시켰기 때문에 이동 생성자가 호출된다 (매개변수가 R-value를 요구하는 &&키워드)
2. 이동 대입 연산자 호출 : res3은 move키워드를 통해 res2를 R-value로 변환시켰기 때문에
위 클래스에 따라 이동 대입 연산자가 호출된다.
*/
※ 동작 원리와 기본 개념
1. R벨류로 전환: L-value를 R-value로 전환하는 키워드, 이를 통해 함수의 매개변수로 R-value참조를 사용하는 경우 자원을 이동할 수 있다.
2. 소유권 이전: 소유권이 이전되면 원본 객체는 더 이상 자원을 소유하지 않게 되며, 일반적으로 포인터를 nullptr로 설정해 자원 해제를 방지한다. 아래코드의 예시를 보라 .
3. 불필요한 복사 방지: move를 사용함으로써 객체를 복사하는 대신 자원을 이동하므로 성능이 크게 향상된다. 특히 자원이 큰 객체였다면 엄청나게 이점이 된다.
#include <iostream>
#include <utility>
class MyClass {
public:
int* data;
MyClass(int size) {
data = new int[size];
std::cout << "리소스 할당\n";
}
// 이동 생성자
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 원본 객체의 데이터 포인터를 nullptr로 설정
std::cout << "이동 생성자 호출\n";
}
// 소멸자
~MyClass() {
delete[] data; // 리소스 해제
std::cout << "리소스 해제\n";
}
};
int main() {
MyClass obj1(10); // 리소스 할당
MyClass obj2 = std::move(obj1); // obj1의 자원을 obj2로 이동
return 0;
}
※ 장점
ㆍ 데이터를 복사하는 대신 소유권을 이전하여 성능을 향상시킬 수 있다. (불필요한 복사 x)
ㆍ 대용량의 객체를 복사하는 대신 그 객체의 내부 포인터나 자원만 이동시키면 된다. (효율적인 자원관리)
※ 주의사항
ㆍ 이동후에는 사용하면 안된다, move키워드를 사용한 객체는 더이상 유효한 상태가 아니다. (nullptr로 안정성 확인하면 좋다)
ㆍ move는 단순히 L-value를 R-value로 변환할뿐, 객체의 상태를 변경하지는 않는다. 객체의 상태가 안전하게 이동될 수 있도록 이동 생성자와 이동 대입 연산자를 적절히 구현해야 한다.