C++프로그래밍/이론 정리

L- value 와 R - value / move

season97 2024. 8. 22. 14:40
728x90
반응형

 

▶ 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로 변환할뿐, 객체의 상태를 변경하지는 않는다. 객체의 상태가 안전하게 이동될 수 있도록 이동 생성자와 이동 대입 연산자를 적절히 구현해야 한다.

 

728x90
반응형