기본개념

C++11 이전까지 LValue와 RValue는 이름과 마찬가지로 코드에서의 연산 중 왼쪽에 올 수 있는 값과 오른쪽에 존재하는 값으로 구분되어 있었습니다. 이 분류는 현재에서도 통용되는 부분이나 C++11 이후 약간의 좀 더 추가 설명이 필요하게 되었습니다.

int main()
{
	// Left Right Result는 모두 LValue로써 식별자(identifier)를 가지고
    // 다른 데이터를 복사해서 자신의 상태를 변경 할 수 있다.
    // 3 7 0은 모두 RValue이다.
	int left = 3;
    int right = 7;
    int result = 0;
    
    // RValue는 여러곳에서 볼 수 있는데, RValue는
    // 다른 데이터의 메모리를 복사할 공간이 없거나
    // 식별자(identifier)가 따로 존재하지 않기 때문에 대입이 불가능하다.
    3 = Right // error 2106: '=': 왼쪽 피연산자는 l-value이어야 합니다.
    Left + Right = 10; // error C2106: '=': 왼쪽 피연산자는 l-value이어야 합니다.
    Func() = 20; // error C2106: '=': 왼쪽 피연산자는 l-value이어야 합니다.
    
    return -1;
}

값들을 보면 알겠지만 기본적으로 LValue는 식별자를 가지고 다른 값을 복사 받을 수 있기 때문에 대입이 가능하지만 RValue에 해당하는 값들은 상수거나 따로 식별자가 존재하지 않기 때문에 자신의 상태를 수정할 수가 없습니다.

C++11 이전의 RValue와 LValue는 앞서 설명했던 RightValue와 LeftValue의 약어로써 말그대로 코드식의 왼쪽에 올 수 있는 값들과 오른쪽에 올 수 있는 값의 의미였습니다.

LValue

C++11 이후로 LValue는 Locaor Value라는 뜻으로 존재하는 메모리를 식별할 수 있는지도 포함하게 되었고, const의 등장으로 메모리의 위치를 식별할 수 있다고 하더라도 무조건 수정할 수 있는 종류의 LValue만 존재하는 것은 아니게 되었습니다.

메모리의 영역을 가지고 주소도 가지지만 수정 불가능한 LValue의 종류는 다음과 같습니다.

  1. 배열타입
  2. 불완전한 타입(사실 큰 의미는 없음.. 불완전한 타입은 컴파일 에러)
  3. const 키워드가 붙은 데이터 타입

RValue

RValue는 메모리의 위치와 식별자를 특정할 수 없는 데이터들을 의미하는데, 여기에서 3 = Right; 식에 대해서는 쉽게 이해가 갈 수 있지만 Left + Right와 Func()에 대해서는 이해가 잘 가지 않을 수가 있으므로 좀 더 부연설명을 하겠습니다.

Left + Right의 2개의 int를 연산한 결과를 보통 RValue 혹은 Value of An Expression이라고 부르는데, 쉽게 설명해서 식을 계산한 후의 값이라는 뜻입니다.

이때 분명 연산의 결과인 10의 메모리는 분명히 존재하지만 그 메모리의 위치를 알아내야 할 식별자가 존재하지 않습니다. 처음 배우는 사람들에게 int와 int 더하기 연산을 설명할 때 이 연산을 하기 위해서 필요한 메모리를 8byte라고 생각하면 안된다고 말하는 것과 같이 int(4) + int(4)를 통해서 나온 결과값 int(4) 바이트가 합쳐져 결론적으로 12바이트를 사용하게 되기 때문입니다.

그렇다면 RValue와 LValue의 구분이 왜 중요하며 C++11이후부터 어째서 이 RValue와 LValue 레퍼런스에 대한 세부적인 형 변환 함수와 레퍼런스를 지원하게 되었는지 알아보도록합시다.

LValue와 RValue는 왜 구분하는 걸까?

일단 LValue 레퍼런스는 여러분들이 C++에서 레퍼런스를 처음 접할 때 배우는 녀석으로 일반적으로 레퍼런스라고 부르는 자료형&을 통해서 선언되며 다른 LValue를 통해서 초기화 시켜주지 않으면 사용할 수가 없습니다.

다음으로 RValue 레퍼런스는 마치 2중포인터처럼 보이는 녀석으로 자료형&&을 통해서 선언되며 일반적인 LValue 레퍼런스에 대입되지 않는 값들을 참조할 수 있습니다.

int TestFunc() { return 10; }

int main ()
{
	int testValue = 0;
    
    // LValue 레퍼런스
    // 메모리가 확실히 존재하고 위치를
    // 식별할 수 있는 변수들만 대입이 가능하다.
    int& leftRef1 = testValue;
    // RValue등 메모리가 명확하지 않은 RValue들의 참조가 불가능. (컴파일 에러)
    int& leftRef2 = TestFunc();
    
    // RValue 레퍼런스
    // 오히려 메모리의 위치가 명확한 LValue에 대한 참조가 불간으하다.
    int&& leftRef1 = testValue;
    // 기본적으로 메모리의 위치를 특정할 수 없는
    // RValue에 대한 참조를 받을 수 있다.
    int&& leftRef2 = Func();
    
	return -1;
}

그럼 이런 RValueRef는 도대체 왜 생겨났고 어디서 유용한 것일까요? 모던 C++을 사용하고 깊게 연구하지 않는 유저들은 대체 저것들이 어째서 유용한지에 대해서 일반적으로 이해하지 못합니다.

이를 제대로 이용하려면 클래스를 통해서 실제 복사와 생성 함수들의 실행과정을 통해서 이해하는 것이 졿습니다. 일단 다음의 코드를 보도록 합시다.

class BaseTest
{
private: 
	int* data;
    
public:
	void SetValue(int _value)
    {
    	if (data != nullptr)
        {
        	*data = _value;
        }
    }
    
 public:
 	BaseTest(const BaseTest& _otherTest) : data(new int(0))
    {
    	if(_otherTest.data != nullptr)
        {
        	*data = *otherTest.data;
        }
        
        std::cout << "BaseTest LValue 레퍼런스 생성자 int 재할당" << std::endl;
    }
    
    BaseTest() : data(new int(0))
    {
    	std::out << "BaseTest 생성 int 할당" << std::endl;
    }
    
    ~BaseTest()
    {
    	if(data != nullptr)
        {
        	delete data;
            data = nullptr;
        }
        
        std::cout << "BaseTest 소멸" << std::endl;
    }
};
BaseTest TestCreateFunc()
{
	BaseTest newTest = BaseTest();
    newTest.SetValue(50000);
    return newTest;
}

int main()
{
	_CrtSetDbgFalg(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    
    // 이 경우에는 실제 새롭게 만들어진 메모리를 통해서
    // 깊은 복사를 해야할 이유가 존재한다.
    BaseTest otherTest = BaseTest();
    BaseTest type1 = otherTest;
    
    // 이와같이 어떠한 함수에서 만들어진 임시객체나
    // 리턴된 객체를 사용할때도 과연 깊은 복사가 필요할까?
    // 이때는 상대가 할당한 객체의 정보를 효율적으로 복사만 해도 되지 않을까?
    // 하지만 이것에 대한 구분을 어떻게 해야할까?
    BaseTest type2 = TestCreateFunc();
    
	return -1;
}

위의 코드의 실행결과까지 보면 알겠지만 int 할당은 2번 일어나게 되고 그에 따라서 깊은 복사도 2번 일에나게 됩니다.

BaseTest 생성 int 할당
BaseTest LValue 레퍼런스 생성자 int 재할당
BaseTest 생성 int 할당
BaseTest LValue 레퍼런스 생성자 int 재할당
BaseTest 소멸
BaseTest 소멸
BaseTest 소멸
BaseTest 소멸

하지만 잘 생각해본다면 두번째 생성방식인 함수 내부에서 생성된 지역변수를 받아서 생성하는 경우에는 int를 재할당할 이유가 없고 그 int를 새롭게 생성된 객체가 받아서 사용하면 되는 일입니다.

그래서 다음과 같은 RValue 레퍼런스 생성자를 추가합니다.

BaseTest(BaseTest&& _otherTest) : data(_otherTest.data)
{
	_otherTest.data = nullptr;
    
    if(data == nullptr)
    {
    	data = new int(0);
    }
    
    std::cout << "BaseTest RValue 레퍼런스 생성자 상대의 int를 얕은 복사." << std::endl;
}

이에 따라서 만약 자신이 RValue로 생성될 때는 추가적인 생성이 아닌 상대의 int를 그냥 받아서 사용하는 이동생성자를 만들 수 있게 됩니다. 이러한 이동생성자는 클래스가 값형으로 전달될 때 내부에 큰 객체나 동적할당한 객체를 굳이 재할당하지 않고 데이터의 소유권을 이전하는 정도로 처리할 수 있게 됩니다.

BaseTest 생성 int 할당
BaseTest LValue 레퍼런스 생성자 int 재할당
BaseTest 생성 int 할당
BaseTest RValue 레퍼런스 생성자 상대의 int를 얕은 복사.
BaseTest 소멸
BaseTest 소멸
BaseTest 소멸
BaseTest 소멸

이와 같이 RValue를 기반으로 해서 새로운 모던 C++의 표준으로 std::Move와 std::Forward같은 새로운 인터페이스들이 생겨났고 이를 잘 이용하면 다양하게 인터페이스를 정리하고 최적화에 대한 효율을 높일 수 있게 되었습니다.

다음으로 RValue 레퍼런스 이동생성자에도 주의할 점은 있습니다.

class BaseTest
{
public:
	BaseTest(int _number) : data(new int(_number))
    {
    }
    
  	BaseTest(int&& _number) : data(new int(_number))
    {
    }
};

다음과 같이 기본자료형에 대한 RValue 레퍼런스를 사용햇을 경우에는 다음과 같이 모호성이 생기기 때문에 주의해야합니다.

int main()
{
	// 다음과 같이 기본자료형의 경우에는 모호성이 일어날 수 있습니다.
    // 10은 RValue 레퍼런스로도 인식될수 있지만 그냥 int로도 변환되기 때문입니다.
	BaseTest(10);
    
	return -1;
}

'DevLog > C & C++' 카테고리의 다른 글

스택(Stack)과 힙(Heap) 차이점  (0) 2021.03.01
포인터(Pointer)와 레퍼런스(Reference)  (0) 2021.03.01
C++ 포인터 개념  (0) 2021.02.04
C++ 구조체(Struct)  (0) 2021.01.25
C++ STL 맵 기본 사용법과 예제  (0) 2021.01.24
복사했습니다!