• 카테고리

    질문 & 답변
  • 세부 분야

    프로그래밍 언어

  • 해결 여부

    미해결

14:00부근에서

21.03.16 11:17 작성 조회수 123

0

설명하실 때, resource의 copy연산자로 가는게 아니고, 거기서 그냥 포인터만 복사한다고 하셨습니다. 

근데 이 경우에 파라미터 안에 R-Value가 들어오는 경우에는 shallow copy를 하기 위해 AutoPtr의 operator로 가는 것이고, L-Value가 들어오는 경우 deep copy를 하기 위해 resource의 copy operator로 간다고 생각하는게 맞나요? 
혹시, AutoPtr에서 그냥 shallow, deep copy를 구현할 수 있는 코드를 짜면, Resource에서는 굳이 copy constructor나 operator를 넣지 않아도 되는지가 의문입니다.(AutoPtr<Resource>를 이용할 때)

그리고 AutoPtr<Resource> 이걸 어떻게 해석해야 하는지가 너무 헷갈리는데 정리해주실 수 있을까요?ㅜ 예전 강의 보면 vector<int>나 array<int>와 같이 해석하려는데, 여기서는 AutoPtr과 Resource가 둘다 class라서 해석하는데 어려움이 있는 것 같습니다.

답변 1

답변을 작성해보세요.

10

안소님의 프로필

안소

2021.03.17

안녕하세요. 답변 도우미입니다.

1. "파라미터 안에 R-Value가 들어오는 경우에는 shallow copy를 하기 위해 AutoPtr의 operator로 가는 것이고, L-Value가 들어오는 경우 deep copy를 하기 위해 resource의 copy operator로 간다고 생각하는게 맞나요? "

우선 이 강의의 주제는 "함수의 리턴값처럼 곧 사라지는 R-value 값을 어떻게 안전하게 소유권을 이전시킬 것인가"로 생각해볼 수도 있습니다. (L-value는 이런 문제가 생길일이 없겠죠) 왜 R-value 의 소유권을 이전시키는 것이 중요한 문제냐면, 강의의 코드에서 generateResource() 함수의 리턴값으로 이 함수에서 생성한 Resource 객체를 main_res에서 받게 되잖아요! 그럼 이 객체를 가리키는 존재가 main_res 와   generateResource() 함수 안에서의 res 지역변수 이렇게 2개인데 이 res 지역변수는 함수가 끝나고나면 사라진다는 것을 알고 계실겁니다. 그러면 소멸자가 호출되어 res 가 가리키는 객체가 사라지게되므로 이 리턴 객체를 받아야하는 main_res 에서 문제가 생기기 때문입니다. 그래서 임시로 잠시 살아있다가 사라지는 이런 R-value 객체들에 대해서는 "소유권 이동" 문제가 중요한 것입니다. res 는 이 객체에 대한 소유권을 박탈시켜야하고 main_res 로 소유권이 안전하게 이전되게끔 해야 앞서 말씀드린 그런 문제가 발생하지 않겠지요. 이것이 바로 스마트 포인터의 역할입니다. (일반 포인터는 이런 처리까진 관여할 수가 없죠.) 그래서 이번 강의에서 교수님께서 작성하신 Resource 클래스와 AutoPtr 클래스를 통해 소유권을 이전시켜주는 '스마트 포인터' 원리를  배우셨다고 생각하시면 되겠습니다. (다음 강의에서 배우실 unique_ptr 스마트 포인터의 원리이기도 합니다.)

R-value 를 참조할 수 있는 매개변수 타입은 2가지가 있습니다.

1. const & 👉 const 참조 타입은 L-value, R-value 모두 참조할 수 있습니다.

2. 그냥 && 👉 오직 R-value 만 참조할 수 있습니다.

소유권을 이전시키려면 직관적으로 쉽게 생각해봤을 때 그냥 집문서만 달랑 넘겨주면 되겠죠. 집은 그대로지만 집의 주인만 바뀐 격으로요! 이게 바로 얕은 복사 개념입니다. m_ptr = a.m_ptr 이렇게 포인터끼리만 복사하면 m_ptr 과 a.m_ptr 는 같은 객체를 가리키게 되므로 이 객체의 소유권은 두 포인터가 가지게 됩니다. 그럼 이제 a.m_ptr 의 집 소유권은 박탈시켜야 합니다. a.m_ptr 도 여전히 이 객체에 대해 소유하고 있는 상태에서 수명이 끝나버리면 소멸자가 호출되어 이 객체도 소멸되버릴테니 m_ptr 입장에선 내가 가지고 있는 집이 날라가버렸네.. 하는 상황이 생길 수도 있으니까요. 소유권 박탈이 가장 중요한 부분입니다. a.m_ptr = nullptr 이렇게 a.m_ptr 을 nullptr 로 초기화시켜서 더이상 해당 객체를 가리키지 않도록, 소유권을 박탈시켜야 합니다.

그러나 const & 는 말그대로 const 이기 때문에 참조 값을 절대 수정할 수가 없습니다. 그러므로 불가피하게 a 가 const & 이라면 a.m_ptr = nullptr 이렇게 값을 수정하는 행위 자체가 불가능합니다. const & 는 소유권 박탈을 할 수 없다는 것이 문제입니다. 그래서 이의 대안으로 깊은 복사를 하는 것입니다! 깊은 복사란 아예 새로운 집을 만들어서 그 집으로 이삿짐을 전부 옮기는 행위라고 보시면 됩니다. 집 문서를 넘겨주는 것이 아닌, 아예 짐 다 싸들고 이사가는 것이에요. 별개의 새로운 객체를 만들어 그곳으로 내용물들을 이사시키니 복사 대상이되는 기존 집은 없어지든 말든 상관이 없어집니다. 즉, 소유권 박탈 작업이 필요 없어지는 것이에요. *m_ptr = *a.m_ptr 은 간접참조이니 Resource 객체끼리의 복사이므로 Resource 의 대입 연산자가 호출이 됩니다. 이 대입 연산자 안에서 깊은 복사가 이루어지도록 코드가 짜여진 것이구요. 아예 별개의 새로운 Resource 객체를 만들어서 그 곳으로 m_data 같은 내용물만 전부 복사한 것이기 때문에 이제 a.m_ptr 이 가리키던 기존 Resource 객체는 없어지든 말든 상관이 없게 됩니다. 그러니 소유권 박탈을 할 필요가 없구요. 

&& 는 const 가 아니니 소유권 박탈을 할 수 있습니다. 그러니 단순하게 포인터를 복사하여 m_ptr 에게 소유권을 나눠주고 (m_ptr = a.m_ptr ) a.m_ptr 은 소유권을 박탈시켜버리면 (a.m_ptr = nullptr) 문제없이 간단하게 해결이 됩니다.

이렇게 R-value 객체를 복사 할 때 const & (복사생성자, 대입연산자) 를 사용하셔도 되고 && (이동생성자, 이동대입연산자)를 사용하셔도 됩니다. 두 방법 다 가능하지만 깊은 복사보단 얕은 복사가 훨씬 빠릅니다.(짐을 다 안옮기고 집문서만 건네주면 되니까요) 

디버깅을 통해 L-value Resource 객체, R-value Resource 객체 이렇게 각각 넘겨보시면서 디버깅으로 호출이 어떻게 이루어지는지 직접 확인해보시는 것도 공부에 도움 되실 것 같습니다.

2. "Resource에서는 굳이 copy constructor나 operator를 넣지 않아도 되는지가 의문"

AutoPtr(AutoPtr && a) 이렇게 이동생성자를 사용할 것이라면 깊은 복사를 굳이 사용할 필요가 없기에 Resource 에 복사생성자를 넣지 않아도 됩니다. 그러나 AutoPtr(const AutoPtr & a) 이렇게 복사 생성자를 사용할 것이라면 위에서 설명드린것처럼 const & 는 깊은 복사를 해야하므로 (소유권 박틸이 불가능하니) Resource 에 대입 연산자, 복사 생성자 만들어주셔야겠죠!

3. "AutoPtr<Resource> 이걸 어떻게 해석해야 하는지"

AutoPtr 은 포인터. Resource 는 데이터.라고 생각해주시면 됩니다.

AutoPtr 라는 이름의 포인터가 있는데 Resource 객체를 가리키는 특별한 포인터라고 생각해주세요.

int * 는 int 데이터를 가리키는 포인터죠? 그런것처럼 main_res는 Resource 객체를 가리키는, 소유권 이전 기능을 가지고 있는 포인터라고 생각하시면 됩니다. 이런 특별한 기능을 하는 포인터 역할을 하도록 교수님이 작성하신 클래스 이름이 AutoPtr 인 것이구요. AutoPtr 클래스의 T 자리에 모두 Resource 가 들어간다고 생각해보시면서 AutoPtr 클래스를 읽어보시길 권합니다.