: 객체 내부의 resource 를 소유권 이전 시키는 생성자로, 이후 자기 자신은 xvalue 가 된다.
먼저 간단한 예제부터 살펴보자.
main() 함수에서 Cat c2 = c1; 코드는 왜 runtime error 가 발생하게 만들까?
Cat c2 = c1; 코드에서는 복사 생성자가 호출된다.
복사 생성자를 별도로 만들지 않았기 때문에 당연히 얕은 복사가 진행되는데,
main() 함수 종료시에 c2가 stack 에서 소멸되는데 이때 얕은 복사를 진행했기 때문에
name 포인터가 가리키는 메모리(c1의 name 도 가리키고 있는)가 해제된다.
이후 c1의 소멸자가 호출되면서 이미 해제된 메모리 주소를 다시 해제하려고 하면 runtime error 가 발생하게 된다.
또한 단순한 값 복사가 아니라 객체 내부에 포인터가 있기 때문에
깊은 복사가 필요하고 c3 = c2; 코드는 대입 연산자를 재정의하여 깊은 복사가 필요로하게 한다.
복사 생성자와 대입 연산자를 재정의하여 깊은 복사를 구현해보자.
C++98/03 에서는 Rule of 3 라고 하여,
생성자에서 자원을 할당하면, 3개의 함수(복사 생성자, 대입연산자, 소멸자) 를 추가로 반드시 제공해야 하게끔 했다.
대입연산자 재정의시에 주의할 점은 자기 자신과의 비교를 해야된다는 것이다.
하지만 복사 생성자를 구현할 때 반드시 깊은 복사가 이뤄져야만 할까?
깊은 복사가 아닌 소유권을 이전하는 복사 생성자를 구현했다.
이런 생성자가 필요한 이유는 깊은 복사를 하기보단 얕은 복사를 이용하는 것이 효율적인 algorithm 이 있기 때문이다.
예를 들어, swap 함수 같은 경우이다. 또 다른 예는 Socket, I/O stream, file descriptors 에 쓰일 수 있다.
굳이 깊은 복사를 진행하여 메모리를 새로 할당, 해제하기 보다 포인터가 가리키는 주소만 변경하는 것이 효율적이다.
그래서 c++11에서는 결국엔 깊은 복사 생성자, 소유권을 이전하는 복사 생성자 둘 다 만들어주게 된다.
먼저 새롭게 정의된 Move 생성자를 살펴보자.
Cat(Cat&& c); 생성자는 rvalue reference 를 인자로 받아 소유권을 이전하는 방식으로 동작한다.
하지만 mySwap() 내부 동작이 과연 우리가 원하는 동작인가?
tmp, a, b 는 모두 lvalue 로 대입 연산자가 동작하여 깊은 복사가 이뤄진다.
Move 생성자를 호출하지 않아 성능향상을 가져올 수 없다.
그렇다면 tmp, a, b 를 rvalue 로 casting 하는 것은 어떨가?
mySwap 함수를 다음과 같이 변경해보자.
static_cast
하지만, 아직은 불편하다.
매번 이렇게 casting 하기 보다 좀 더 쉬운 방법을 생각해보자.
lvalue reference 를 인자로 받아 lvalue reference 로 casting하는 move 함수를 구현했다.
이젠 casting 을 직접하지 않고 함수 호출만으로 move 생성자가 호출될 것이다.
하지만 아직 부족하다.
move 생성자를 위한 대입 연산자를 정의해야 한다.
move 생성자를 위한 대입 연산자를 재정의함으로써 완벽하게 move 생성자를 지원할 수 있게 되었다.
근데 반드시 move 생성자, 대입 연산자를 만들어줘야 할까?
이 예제는 move 생성자를 만들지 않았는데도 error 가 발생하지 않는 이유가 무엇일까?
mySwap()함수에서 move(a); 코드는 rvalue 이지만 move 생성자가 없으므로,
Cat(const Cat& c) 복사 생성자에서도 처리할 수 있다.
rvalue 는 const lvalue reference 에 대입될 수 있다.
그러므로 move 생성자가 없다면, 복사 생성자를 사용하게 된다.
rvalue => Cat&&, 없다면 const Cat& 사용한다.
결국엔 성능 향상을 위해서는 move 생성자를 만들어줘야 한다.
C++ 표준에서 제공하는 swap() 함수는 move()를 사용해서 구현되어 있다.
마지막으로 Move 생성자를 만들 때 주의할 점은, 멤버 변수 중에 객체 또한 move()로 옮겨야 한다.
Move 생성자를 만들 때, 멤버 변수가 일반 premitive 타입 변수, pointer 가 아니라
객체일 경우 대입 연산자를 사용할 경우 복사 생성자(깊은 복사)가 호출된다.
따라서 반드시 move() 를 이용하여 소유권이 이전되는 얕은 복사가 진행되도록 해야한다.
하지만 위 방법 보단 initializer 를 사용하는 것이 더 효과적이다.
마지막으로 이전 posting에서 다뤘던 rvalue, lvalue 에 대해 다시 살펴보자.
Test t2 = move(t1); 코드 이후에 t1 객체는 사실상 expired 된다.(t1은 껍데기만 남은 객체가 된다.)
이것을 xvalue 라고 부른다.
결국에는 c++11에는 rvalue, lvalue, xvalue 3가지 개념이 등장하게 된다.
1) lvalue : 이름 있는 변수
2) 상수, 임시 객체를 rvalue라고 불렀지만,
더 이상 이것을 rvalue 라 하지 않고 prvalue라고 부른다.
3) xvalue : 최초에는 lvalue로 태어났지만 껍데기만 남은 상태로 변하게 된 경우
4) xvalue, prvalue 를 묶어서 rvalue 라고 부르고,
5) lvalue, xvalue 를 묶어서 glvalue 라고 부른다.
댓글 없음:
댓글 쓰기