Language/C++

C++ 11 :: Move Semantics

VallistA2015. 2. 14. 18:44

C++ 복사와 C++ Copy Semantics의 문제점은.. C++에서는 어떤 객체의 상태를 다른 객체로 복사하는 일이 가능하다.

복사와 관련된 매커니즘은 복사 생성자와 할당 연산자가 처리한다. 이 둘은 프로그래머가 정의할 수도 있으며 컴파일러에 의해 정의되기도 한다. 컴파일러는 클래스 안에서 복사 생성자와 할당 연산자를 찾을 수 없다면 자동으로 생성한다.

하지만 클래스 내부에서 다른 타입의 리소스를 가지고 무언가를 하려면 깊은 복사를 위해 프로그래머가 직접 정의한 복사 생성자와 할당 연산자가 있어야 한다.


복사 생성자와 할당 연산자의 전형적인 구현 패턴은 매우 비슷하다.

1. 멤버 변수를 위해 새로 리소스 할당.

2. 인자로부터 this 포인터로 복사 실행

3. 복사가 완료되면 기존 리소스 해제

4. 해제된 기존 리소스에 새로운 리소스를 할당해 이를 반환한다.


먼저 Copy Semantics의 문제를 보겠다.


1.



vec.push_back(o); 를 만나기 전 vec이 모두 채워졌다고 가정하면 std::vector는 동적으로 크기가 증가하거나 감소하는 배열의 일종이다. 벡터가 꽉 차면 컴파일러는 이러한 연산을 하게 된다.


1. 크기가 더 큰 새로운 백터를 만든다. (STL을 구현한 벤더에 따라 다르지만 대부분 1.5~2배씩 증가하게 된다.)

2. 이곳에 기존 백터에 있던 요소 모두 복사한다.

3. 복사가 모두 끝나면 기존 벡터 안에 있던 요소를 모두 파괴하게 된다. 벡터 내부 요소가 객체인 경우엔 해당 객체 소멸자를 호출한다.

4. 기초 벡터 자체를 파괴한다.

5. 새로운 벡터의 이름을 기존 벡터 이름으로 바꾼다.


위의 모든 과정이 끝나고 vec.push_back(o) 가 실행된다.

즉 프로그래머의 의도가 벡터 긑에 객체 2개만 더 추가하고 싶었을 뿐이라도 C++ 컴파일러는 두개든 1000개든 상관없이 기존 벡터에 있던 요소를 복사하고 기존 벡터를 파괴하는 과정을 수행하게 된다.


다른 예로 백터를 반환하는 팩토리 함수에 대해 말하자면.

STL 구현에 따라 정의된 기본 크기로 백터가 생성되고.. 


1. createMyVec() 함수는 내부에서 임시로 비어있는 MyVec 객체를 하나 만들게 된다.

2. createMyVec() 함수 안에서 정의된 일을 수행한다.

3. Vec2를 vec에 대입한다.


3-1. createMyVec() 함수가 vec2를 반환할 때 우선 전달 받아야 할 vec에 빈 자리가 있어야한다.

3-2. vec이 가졌던 요소들을 파괴한다. (무엇이 들어있던 파괴)

3-3. vec 내부 공간을 확보하면 vec2 안의 요소를 vec으로 하나씩 복사한다.

3-4. 복사가 끝나면 vec2는 소멸되어야 하므로 vec2 내부에 들어있는 요소를 파괴한다.

3-5. vec2의 요소가 모두 소멸된 후 vec2가 소멸한다.


이러한 형식의 Copy Semantics는 굉장히 비효율적인 작업을 일련으로 하기 때문에 이를 대체하고자 Move Semantics가 생기게 되며 Move Semantics가 대두되면서 C++ STL 에도 이런 메커니즘이 대거 적용되어 기존 레거시 코드의 성능이 향상되었다.

이런일이 가능했던 것은 R-Value Reference의 기능 때문이었다.


이제 Move Semantics를 알아보도록 하자.


C++ 11의 이동 생성자와 이동 할당 연산자의 구현 원리는 복사 생성자나 할당 연산자의 원리와 유사하다.

Move Semantics와 Copy Semantics의 같은 점은 자신과 동일한 타입의 객체를 인자로 받아서 새롭게 생성한 객체에 해당 내용을 이식해 준다는 것이다.


Copy Semantics의 경우에는 멤버변수를 위해 새로 생성한 인스턴스에 메모리를 할당해야 하지만, Move Semantics에서는 메모리를 할당하지 않아도 된다.


Move Constructor & Move Assignment Operator (이동 생성자 & 이동 할당 연산자)


C++ 11의 이동 생성자나 이동 할당 연산자의 구현 원리는 복사 생성자나 할당 연산자의 원리와 유사하다.

그리고 Move Semantics와 Copy Semantics의 같은 점은 자신과 동일한 타입의 객체를 인자로 받아서 새롭게 생성한 객체에 해당 내용을 이식해 준다는 것이다. 그러나 Copy Semantics의 경우에는 멤버 변수를 위해 새로 생성한 인스턴스에 메모리를 항당해야 하지만, Move Semantics에서는 메모리 할당을 하지 않아도 된다.


C++ 11의 이동 생성자를 고려하지 않고 원래 방법대로 복사 생성자를 사용해 보도록 하겠다.

배열을 제공하는 클래스를 예로 들어보자면 클래스 멤버 변수로 배열을 위한 포인터 m_pArr가 있다면 복사할 때 깊은 복사를 해야 제대로 복사가 이루어 진다. 이는 컴파일러가 제공해주는 복사 생성자를 믿을 수 없다는 뜻이다.

깊은 복사를 위해 복사 생성자만 구현하도록 하겠다.


2.



이동 생성자는 훨씬 간단하다. 새롭게 메모리를 할당할 필요가 없다.

이동생성자에서 눈 여겨 봐야 할 것은 두가지가 있다.


몸체 안에서 other.m_pArr을 null로 설정하며 파라미터가 const 타입이 아니다.

첫번째로 other.m_pArr을 null로 설정하는 이유는 other 객체도 언젠가는 (범위 scope를 벗어날 때) 소멸되는 임시 객체이다.

이 객체가 소멸하는 순간 당연히 소멸자를 불러오며 소멸자를 보면 m_pArr을 삭제하는 루틴이 있다.


Move Semantics에서 m_pArr은 이미 다른 인스턴스에게 소유권이 이전된 메모리 영역이다. 따라서 other.m_pArr을 null로 설정하지 않으면 다른 인스턴스(this)에게 이미 소유권이 넘어간 m_pArr을 삭제하고, 이는 당연히 이동 생성자를 호출한 해당 인스턴스(this)가 가진 m_pArr을 삭제하고, 이는 당연히 이동 생성자를 호출한 해당 인스턴스(this)가 가진 m_pArr을 삭제하는 결과를 초래하며 이는 깊은 복사가 아닌 얕은 복사와 마찬가지인 상황이다.

따라서 other.m_pArr를 null로 설정해야 소멸자에서 delete[] m_pArr이 호출되는 것을 방지 가능하다.


두번째는 파라미터가 const 타입이 아닌 것은 너무나 당연한데, 내부에서 other의 멤버인 m_pArr를 수정해야 하니까 말이다.

만약 const면 접근 할 수 없다.


이번에는 Copy Semantics에 비해서 Move Constructor Performance 가 얼마난지 한번 보도록 하자.


3.



4.



댓글

댓글쓰기 폼

VallistA

병특이 끝나서 게임에서 웹으로 스위칭한 프로그래머.
프로그래밍 정보등을 공유합니다.

자고 싶습니다. ㅠㅠ

페이스북    :: 링크
카카오톡    :: kingbye1
Github      :: 링크

궁금한점 문의 주시면 답변드리도록 하겠습니다

VISITED

Today : 72

Total : 413,765

SNS

  • 페이스북아이콘
  • 카카오톡아이콘
  • 트위터아이콘