오른값 참조

200원짜리개발자·2023년 8월 28일
0

C++

목록 보기
37/39
post-thumbnail

C++11로 올라오면서 많은 것들이 바뀌었지만,
여기서 중요한 것은 auto, lambda, rvalue-ref이다.

여기서 auto, lambda는 없어도 없는데로 살 수 있다. (편의성 증가)
하지만 오른값 참조는 없던 개념이 생긴 것이여서 C++자체가 업그레이드 되고 빨라졌다고 할 수 있다.
그래서 C++11 추가 문법 중에서 가장 중요하다고 볼 수 있는 문법이다.

오른값 참조

일단 오른값 참조에 대해서 이해를 하기 위해서는 왼값(l-value)와 오른값(r-value)를 알고 있어야 한다.

l-value: 단일식을 넘어서 계속 지속되는 개체
r-value: l-value가 아닌 나머지

int a = 3;

a = 10;

이런식으로 a는 단일식을 넘어도 살 수 있을 것이다.

즉 a는 왼값이고

int a = 3;

3 = 10; // X

3에다가는 10을 넣을 수 없다.

즉 3은 오른값이다.

그럼 어디서 어떻게 사용이 될까를 알기위해서는 클래스까지 넘어가야한다.

class Knight
{
public:
	Knight()
    {
    
    }
    
    ~Knight()
    {
    
    }
    
    int _hp = 0;
};

라는 클래스가 있을 때, 다른 함수에다가 넘겨준다고 가정을 해보자

복사

일단 첫번째 경우는 복사를 해서 넘겼을 때이다.

void TestKnight_Copy(Knight knight)
{
	knight._hp = 100;
}

int main()
{
	Kngiht k1;
    
    TestKngiht_Copy(k1);
}

그러면 우리는 k1을 위처럼 넘길 수가 있었다.

여기서 C#이랑 다른 부분이 C#은 struct는 복사 class는 참조로 알아서 정해주지만,
C++은 자기자신이 지정을 해주는 방식에 따라 달라질 수 있다.

일단 이 방식이면 k1이 복사가 되었을 것이고 class안에 데이터가 많다면 비효율적인 방식일 것이다. (그리고 원본에 영향을 끼치지 않는다는 특징이 있었다)

참조 (왼값 참조)

이제 참조를 알아봐야 하는데 참조는 왼쪽참조라고 봐야 정확하다.

void TestKnight_LValueRef(Knight& knight)
{
	knight._hp = 100;
}

참조는 이제 복사와 다르게 원본을 넘겨준다는 차이점이 존재한다. (포인터를 넘겨주는 것과 차이가 없음)

그래서 함수를 호출해서 hp를 바꾸면 원본도 바뀐다.

여기서 하나 재밌는점은 왼값참조에 오른값을 넣어준다고 하면,
TestKnight_LValueRef(Knight());이런식으로 임시객체를 넣어줄 수 있다.
하지만 넣어주면 빨간줄이 뜨게 될 것이다.


그리고 이런식으로 비const 참조에 대한 초기 값은 lvalue입니다.라는 오류가 뜬다.

이게 무슨 소리이냐하면 TestKnight_LValueRef함수를 보면 왼값참조이지만 const를 붙이지 않았기 때문에 비const참조이고 그러면 왼값만 받아줄 수 있는 것이다.

그럼 테스트를 하기 위해서 const를 붙여본다면,

void TestKnight_ConstLValueRef(const Knight& knight)
{

}

이렇게 만들어준다면 실행이 잘 된다는 것을 볼 수 있다. (const가 붙으면 오른값도 넣어줄 수 있다)

이걸 왜 이런식으로 만들었나 생각을 해본다면,
const가 안 붙은 버전으로 받아버리면 임시객체와 같은 오른값을 받으면 애초에 오른값은 일회성으로 등장하고 사용을 안할 예정인데 열심히 함수 안에서 고쳐주는 것이 앞뒤가 안 맞다. const를 붙이면 고치지는 않을 것이지만 참고는 할 수 있기때문에 사용되는 것이 맞다고 볼 수 있다.

이러한 차이가 있다.

오른값 참조

여기서 이제 마지막 버전이 나오는데 그것이 바로 오른값 참조이다.

void TestKnight_RValueRef(const Knight&& knight)
{

}

오른값 참조는 참조에다가 참조를 한 번더 붙여주면 된다. &&
포인터같은 경우는 **이런식이면 포인터의 포인터 였지만 &&는 오른값 참조라는 뜻이다.

그러면 오른값 참조는 무엇이 달라질까?
원본 넘겨줄테니까! 까지는 똑같다. (받은 애를 고치면 실제로 원본도 바뀐다)
그리고 더 이상 활용하지 않을테니 맘대로 해라는 힌트를 준다.

그냥 참조같은 경우에는 원본을 고친다고 하더라도 원본을 날려버리는 것이 아니지만,
오른값 참조같은 경우는 일회성으로 넘기는 것이여서 원본은 이제 활용하지 않으니 니가 알아서 해라라고 하는 것이다.

이런식으로 임시객체를 넣는 상황도 생각해볼 수 있다.
TestKnight_RValueRef(Knight());
이런 경우도 있겠지만 이것보다 더 재밌는 사실은 k1이라는 애를 저 함수에 넘기면 Rvalue이니 안된다고 할 것이다.

하지만 우리가 어떠한 이유의 의해서 k1이라는 애를 사용하지 않고 모든 소유권을 안으로 넘겨주고 싶다면 TestKnight_RValueRef(static_cast<knight&&>(k1));이런식으로 오른값 참조로 캐스팅을 해줄 수 있다.

그리고 이동이랑 참조랑 달라지는 경우가 생길 수 있다.

복사 생성자, 복사 대입 생성자

저번에 깊은 복사와 얕은 복사에 대해서 공부를 하였다.
한 번 복습을 해보자면,

class Pet
{

};

class Knight
{
public:
	Knight()
    {
    
    }
    
    ~Knight()
    {
    	if(_pet)
        	delete _pet;
    }
    
    // 복사 생성자
    Knight(const Knight& knight)
    {
    
    }
    
    복사 대입 연산자
    void operator=(const Knight& knight)
    {
    	_hp = knight._hp;
        _pet = knight._pet;
    }
    
    int _hp = 0;
    Pet* _pet = nullptr;
};

이런식으로 Pet이라는 클래스가 있고 Knight가 pet이라는 것을 들고 있다고 가정을 해보자

k1._pet = new Pet();

그리고 pet을 동적할당 시켜주자

Knight k2;
k2 = k1;

이렇게 k2에 k1을 넣어준다면 복사 대입 연산자로 가게 될 것이다.
그리고 실행시키면 크래시가 날 것이다. 무엇이 문제일까?

원래는 펫의 정보만 빼와서 k2의 펫에 정보만 넘겨줘야하는데 펫의 원본을 가져와버려서 둘 다 돌일한 원본펫을 가리키고 있다. (얇은 복사)

그래서 둘 중 한 객체가 소멸할 때 _pet도 소멸시켜버리기 때문에 크래시가 터질 것이다.

그래서 이것을 해결하기 위해서 깊은 복사를 사용하여

void operator=(const Knight& knight)
{
 	_hp = knight._hp;
        
    if(knight._pet)
    	_pet = new Pet(*knight._pet);
}

이런식으로 펫을 새로 만들어서 knight의 pet의 복사 생성자를 이용해서 넣어주면 서로 각기 다른 펫을 가지고 있게 되어 문제가 사라진다.

복사가 일어날 때 펫을 만드는 비용이 엄청나게 크다면 복사 비용이 심상치 않게 커질 수 있다고 예측할 수 있다.

이동 생성자, 이동 대입 생성자

하지만 이동이라는 것은 복사와는 조금 다르게 원본은 사용을 하지 않게다라는 힌트를 주고 있기때문에 만들어보자면

// 이동 생성자
Knight(Knight&& knight) noexcept
{
	_hp = knight._hp;
    
    _pet = knight._pet;
    knight._pet = nullptr;
}

void operator=(Knight&& knight) noexcept // noexcept는 예외가 처리가 되지 않는다고 해서 noexcept를 사용함 (이동 생성자 안에서 cpp 컨테이너 사용시 붙여야 함)
{
	_hp = knight._hp;
    
    _pet = knight._pet;
    knight._pet = nullptr; // 메모리를 차지했기에 delete대신 nullptr넣어주기
}

어차피 상대방이 사라질 것을 알고 있기 때문에 상대방의 펫을 nullptr로 밀어주면 된다. (소유권 이전이라고 보면 된다)

결국 복사랑 이동이 별차이 없으면 상관이 없겠지만 진짜로 복사하는 것과 상대방의 것을 내껄로 이전시키는 것은 아예 의미가 다르기 때문에 큰차이가 있다.

그래서 이동은 상대방의 것을 빼앗아 온다고 생각하면 된다.

그렇기에 이제부터는 k1을 k2로 이전 시키고 싶을 때

Knight k2;
k2 = static_cast<Knight&&>(k1);

이렇게 오른값으로 가져가 버리면 k2가 pet을 가져가고 k1은 펫을 잃어버릴 것이다.

하지만 static_cast를 사용해서 오른값 참조로 바꾸는 것은 너무 길다 생각할 것이다.
그래서 우리는 std::move();라는 함수를 사용해볼 것이다.
k2 = std::move(k1);이게 k2 = static_cast<Knight&&>(k1);이것과 뜻이 똑같다.

실제로 move코드를 들어가보면 오른값 참조로 바꾸는 코드만 있는 걸 볼 수 있다.

여기까지가 이동의 개념이라고 볼 수 있다.

이동 사용처

근데 사실 이걸 어디다가 사용을 해야할지 모를 수 있다.
우리가 저번에 배운 자료구조 중에서 막 이사를 하고 다니는 자료구조가 있었다.
바로 vector에서 사용이 될 수 있다.

우리가 이동을 몰랐을 때에는 복사를 해서 이사를 하였지만 이동을 사용해 이사를 하게되면 엄청난 효율은 없어도 복사하면 비용이 느는 클래스가 있다면 이동이 훨씬 좋을 것이다.

그리고 나중에 unique_ptr배우거나 소유권자체를 넘겨줘야되서 다른 애에게 복사는 막지만 이동하는 건 허용하겠다. 이런식으로 사용을 할 수도 있다. (활용도가 높음)

사실 오른값 참조를 우리가 직접적으로 사용할 일은 별로 없을 것이다.
라이브러리나 깊이 있는 것을 만들 때 사용하게 될 것이다.
하지만 최소한 &&일 때 오른값 참조구나! 라는 것을 기억을 할 필요가 있다.

마무리

기억해야 할 것

참조값만 받았더니 오른값을 건내줄 수 없을 때(비const 참조 오류)
const 붙여주기
오른값이라는 것 자체도 원리는 참조랑 다를바가 별로없고 추가적으로 상대방을 사용하지 않을 것이 확실하기에 모두 가져올 수 있다는 것이 다르다.

이런자체로도 중요하지만,
가끔 면접에서 오른값 참조를 물어보는 면접관도 있다고 한다.
그래서 어떻게 동작하고 어떤 느낌인지는 대략적으로라도 알고 있어야 한다.

다음시간에는 스마트 포인터에 대해 공부해볼 것이다.

profile
고3, 프론트엔드

0개의 댓글