다음과 같은 코드를 짰을 때, 컴파일러가 알아서 만들어주는 메소드들이 있다.
1. Constructor
2. Destructor
3. copy/move Constructor
4. copy/move Assignment
: 일반적으로 Constructor는 직접 만들어주고 destructor을 포함한 나머지 5가지 메소드들은 직접 만들어주지 않는다.
but
이렇게 포인터를 활용해서 리소스를 관리하게 되면 개발자가 직접 이 5가지의 메소드를 관리해 주어야 한다.
구글에서 c++ rule of three / rule of five 로 검색하게 되면 cppreference에서 관련 내용을 확인 가능하다.
https://en.cppreference.com/w/cpp/language/rule_of_three
The rule of three/five/zero - cppreference.com
[edit] Rule of three If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three. Because C++ copies and copy-assigns objects of user-defined types in va
en.cppreference.com
간단하게 정리하면 멤버 변수로 raw pointer를 사용해서 리소스 관리를 해주게 되면,
destructor,copy constructor, copy assignment, move constructor, move assignment를 직접 구현해 주어야 한다는 것이다.
특별히 포인터를 사용해 주지 않는다면 다섯가지 함수들을 직접 만들 필요는 없지만 학습 목적으로 이 메소드들을 직접 만들어보자.
<Constructor, Destructor, default>
고양이 object를 만들고 바로 컴파일하고, 나이와 이름을 관리해 주기 위해 컨스트럭터로 두 가지 멤버 변수를 설정해 보자.
그러기 위해서 파라미터를 받는 컨스트럭터를 만들어 보면,
만약 여기서 고양이 object nabi를 하나 더 만들 때, 파라미터를 넘기지 않는다면 컴파일 에러가 난다.
이미 매개변수를 받는 컨스트럭터를 만들어 주었기 때문에 default 컨스트럭터가 자동으로 disable 되었기 때문이다.
이 경우 이렇게 Cat() = default; 이런 방식으로
디폴트 컨스트럭터는 디폴트다 라고 알려주면
컴파일러가 자동으로 만들어주는 default 컨스트럭터를 사용해서 구현하라고 알려줄 수 있다.
컨스트럭터에서 "constructor" 문자열을 출력하도록 바꿔주고
디스트럭터를 만들어 주고 실행해 보면
*참고로 포인터를 사용해서 리소스 관리 해준다면 디스트럭터 파트에 메모리 리소스 해제 delete 해 주어야 한다.
<Copy Constructor>
기존 object가 존재하고, 새로운 object를 만드려고 할 때
기존 object 내부의 정보를 전부 복사해서 새로운 object를 만들 때 사용하는 생성자가 copy constructor이다.
즉, 기본 object를 똑같이 copy해서 새로운 object를 만들 때 사용하는 컨스트럭터가 copy constructor이다.
copy constructor는 똑같이 class 이름을 가지는 대신 그 argument를 같은 클래스의 레퍼런스로 받아준다.
initializer member list 를 사용해서 더 최적화된 copy constructor를 만들어 보면,
copy constructor를 부를 수 있는 방법에는 여러가지가 있는데,
기본적으로는 오브젝트를 생성할 때 {}를 사용해서 kitty를 넣어주는 방법이 있고
= 오퍼레이터를 사용해서 넣어줘도 copy constructor가 불려진다.
(= 오퍼레이터 사용할 때 assignment가 개입될 거라고 생각할 수도 있지만 실제로는 새로운 오브젝트가 만들어지는 과정이기 때문에 copy constructor가 호출된다. 이런 헷갈리는 부분 때문에 {} 사용해서 오브젝트 생성하는 것이 권장된다.)
<Move Constructor>
:object가 하나 있고 새로운 object를 만들고 싶을 때,
이 내부의 리소스들을 copy 하는 것이 아니라 move해서 새로운 object를 만들 때 호출 되는 컨스트럭터이다.
원래 있던 object 안의 리소스는 새로운 object에 ownership을 빼앗기게 된다!
move constructor는 Rvalue 레퍼런스를 받아와서 구현할 수 있다.
만약 포인터를 사용해서 리소스 관리 해 주었다면 copy constructor에서는 std::memcpy() 를 통해 리소스 전체를 복사해 줄 수 있고,
move constructor의 경우에는 포인터를 빼앗아오고 빼앗긴 포인터들에는 nullptr 를 넣는 식으로 해 줄 수 있다.
하지만 지금은 포인터를 사용하는 것이 아니라 string object를 사용하였기 때문에
other의 string object mName을 std::move()를 통해 ownership을 빼앗는 것이다.
move 컨스트럭터를 호출하기 위해서는 Rvalue(&&)가 들어가야 하는데
Rvalue 는 std::move를 통해서 kitty 오브젝트의 ownership을 가져올 것이다. (Cat kitty3{std::move(kitty)})
그림으로 나타내 보면,
1) kitty 오브젝트를 생성, 이 안에는 나이정보 1살과 "kitty" 문자열을 가리키는 string 정보가 들어가 있다.
2) kitty2 오브젝트를 만들 때 copy 컨스트럭터가 호출되며 나이정보는 복사되고, string 정보 또한 그림과 같이 string이 복사가 되어 만들어 진다.
3) kitty3를 만들 때는 move constructor가 호출되었는데,
나이정보는 간단하게 복사해 주었지만 문자열 정보 같은 경우 Rvalue로 바꾼 다음 mName에 넣어 주었다.
즉, kitty ownership을 빼앗아서 copy역시 kitty 문자열을 가져갔다는 말!
빌드하여 실행해 보면
처음 constructor는 첫번째 오브젝트를 만들 때 호출된 것이고
두 번째 copy constructor는 kitty2를 만들 때 호출 된 것이다.
move constructor는 3번째 kitty를 만들 때 호출된다.
destructor가 불려졌는데 이 때는 이 kitty3 object가 없어지며 destructor를 호출한 것이고, 바로 이어서 kitty2가 사라지고, 마지막 오브젝트 kitty가 사라질 때 destructor를 호출해야 하는데 이 오브젝트는 kitty3에게 string ownership을 빼앗겼기 때문에 아무것도 가리키지 않는다. (위 그림에서 살펴 보아도 첫번째 kitty는 아무것도 가리키고 있지 않다!)
따라서 destructor 앞에 mName 이 비어있는 것이다.
<Copy Assignment>
: 하나의 오브젝트에서 다른 오브젝트로 할당될 때 호출되는 메소드이다.
object1, object2 이렇게 2개의 오브젝트가 있다고 가정하면,
object1 = object2
일 경우 object2에 있던 내용들이 object1 으로 전부 복사가 될 것이다.
이것을 copy assignment라고 한다.
코드로 보면,
고양이의 레퍼런스를 리턴해주고, assignment의 리턴 타입은 레퍼런스이다.
copy assignment를 콜 해주기 위해서는 object가 2개 필요하다.
2번째 object nabi 를 2살로 생성해준다.
그리고 nabi object를 kitty object로 assignment 해 준다.
출력해보면,
nabi copy assignment가 출력되고
kitty 오브젝트의 정보를 출력하였는데 nabi 2 가 출력된 걸 볼 수 있다.
왜냐하면 assignment가 될 때 nabi object의 정보가 모두 kitty로 복사가 되었기 때문이다.
<Move Assignment>
2개의 object1, object2가 있을 때, object2 가 Rvalue라면
할당 했을 때 그 내용이 copy가 되는게 아니라 move가 되면서 move assignment가 호출되는 것.
생성자에 move assignment의 경우 고양이의 레퍼런스를 리턴해 주고 Rvalue 레퍼런스를 받아 온다.
string object 의 경우 std::move 를 통해서 Rvalue를 빼앗아 올 수 있고,
mAge는 복사를 해 주었다.
이후 문자열 " move assignment" 를 출력해주도록 만들고
*this 를 리턴해 준다.
이 move assignnemt를 호출해 주기 위해 오른쪽에 std::move() 를 사용해 주고 nabi.print 를 해 준후에
출력해 보면
결과가 나온다.
move assignment 문자열이 호출된 것을 확인할 수 있고
바로 이어서 kitty의 print,
nabi의 print - string object의 ownership을 빼앗겼기 때문에 2만 나왔다.
그리고 destructor 하고 끝.
<self assignment>
kitty 에 kitty object를 다시 넣거나
Rvalue kitty object를 넣어주는 경우가 있다.
이상하지만 아무런 문제 없이 빌드가 된다.
만약 char* 등을 사용해서 리소스 관리 해 주었다면 예상치 못한 일이 생길 수 있다.
이런 문제들을 예방하기 위해서,
if 로 2개가 같은 object라면 바로 자기 자신을 리턴해주는 코드가 필요하다!
또한 destructor move assignment/ constructor move assignment는 exception 이 던져지지 않기 때문에
noexcept를 붙여주는 것이 매우 중요하다.
destructor, move constructor, move assignment 는 새로운 리소스를 요청하지 않는다.
따라서 exception이 던져질 리가 전혀 없기 때문에, noexcept를 붙여주게 되면 컴파일러가 move가 필요할 때 noexcept를 보고 확실하게 move를 표현해 주게 된다.
결론은 destructor, move constructor, move assignment에서는 noexcept를 붙여준다는 사실을 기억하면 된다!
<직접 만들어줄 필요 없는 경우>
pointer를 사용해서 리소스 관리를 해주지 않는다면 move constructor 등을 직접 만들어 줄 필요는 없다.
컴파일러가 알아서 만들어 주기 때문이다!
현재 사용한 예시 코드에서는 string과 int만 가지고 사용하고 있기 때문에 필요없는 부분들을 모두 지워 보면,
그래도 아무런 문제 없이 컴파일이 완료된다! 컴파일러가 알아서 만들어 준단 의미!
만약에 class에서 리소스를 포인터를 사용해서 관리해 주었다면 개발자가 직접 모든 것을 새로 정의해 주어야 함.
따라서, 이런 이유 때문에 생산성이 높은 c++ 개발을 하고 싶다면 포인터를 사용한 리소스 관리는 최대한 피해주는 것이 좋다.
<delete 키워드>
컴파일러가 알아서 copy/move constructor, copy/move assignment 를 만들어 주었다면 이것을 막는 방법도 있다.
delete 키워드를 써 주면 된다.
인터페이스를 써 준 후에 delete 키워드를 넣어주면 컴파일러가 copy constructor를 만들어 주지 않는다.
copy constructor가 지워졌는데 부르려고 했기 때문이다.
assignment 또한 delete 키워드로 막을 수 있다.
object가 만들어 지는 것 자체를 막을 수도 있다.
디폴트 컨스트럭터 옆에 delete를 써주면 object가 만들어 지는 것 자체를 막을 수 있다.
static 메소드만 가지고 있는 클래스의 경우 object를 만들 필요가 없으니 constructor 자체를 이렇게 막는 것이 좋은 방법이고, 또한 싱글턴의 경우에도 copy move assignment/constructor가 디자인 패턴의 관점에서 넌센스이기 때문에 (하나만 있어야 되는 인스턴스이니까) 막아주면 좋을 것이다.
+또한 가끔 private 안에 컨스트럭터 등이 선언되어 있는 경우가 있는데, 이는 delete와 같은 기능을 한다.
(C++ 11 이전의 방식)
결론
1. Constructor에서 매개변수가 다를 때는 default 컨스트럭터를 사용해서 추가로 만들 수 있다.
2. rule of three, rule of five 법칙 때문에 클래스 내부에서 포인터를 사용해서 힙에서 리소스 관리를 해 준다면,
개발자가 copy/move constructor와 copy/move assignment를 직접 구현해야 된다.
3. 컴파일러가 알아서 만들어주는 이런 함수들이 싫다면, delete 키워드를 사용해서 강제적으로 없애주는 방법이 있다.
'모던C++ > OOP' 카테고리의 다른 글
5. function overloading, operator overloading (0) | 2022.08.07 |
---|---|
3. Member Init List - Class Constructor, Destructor (0) | 2022.08.05 |
2. Static 스태틱 멤버 (0) | 2022.08.04 |
1. OOP 란? - 객체 지향 프로그래밍 소개 (0) | 2022.07.24 |