2. 생성자, 소멸자 및 대입 연산자
11) operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자
자기대입이란 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것이다.
Widget w;
w = w;
별 문제가 없어보이지만 문제가 있다는 게 가장 큰 문제이다.
대입 연산자 내에서 어떤 동작을 할지 모르기 때문이다.
따라서 대입 연산자에서는 반드시 일치성 검사를 넣어 다음과 같이 구현해야 한다.
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
...
}
혹은 예외 안정성을 높인 다음과 같은 방법도 있다
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrginal = pb;
pb = new Bitmap(*rhs.pb);
delete pOriginal;
return *this;
}
원래의 pb를 기억해두고 새 객체를 생성한 다음 원래의 pb를 삭제하는 방법이다.
이러한 자기대입 연산은 자주 일어나는 일이 아니므로, 일치성 검사보다 위 방법이 나을 수 있다.
세번째 방법으로 copy and swap이 있다. 자세한 사항은 항목 29 참조.
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp); // *this와 temp의 데이터를 맞바꾼다.
return *this;
}
12) 객체의 모든 부분을 빠짐없이 복사하자
복사 생성자와 복사 대입 연산자와 같이 복사 함수를 직접 구현할 때에는 객체의 모든 부분을 빠짐없이 복사하고 있는지 항상 주의해야 한다.
구현하는 도중 새로운 멤버가 추가되는 일도 부지기수니, 그럴때도 놓치지 않도록 하자.
상속된 클래스에서도 기본 클래스의 복사 함수를 호출해 줌으로써 모든 멤버를 복사할 수 있도록 유의하자.
단, 복사 생성자와 복사 대입 연산자는 서로를 호출해서는 절대 안된다. 차라리 공통된 동작을 제 3의 함수에 구현해서 양쪽이 각각 이 함수를 호출하도록 하는 편이 좋다.
3. 자원 관리
13) 자원 관리는 객체에게 맡기는 것이 좋다.
어떠한 자원을 얻어냈다면 반드시 반환하는 것이 원칙이다.
자원을 객체에 넣고 그 자원의 해제를 객체의 소멸자가 맡도록 하고, 그 소멸자가 자원을 얻어낸 부분을 탈출할 때 호출되도록 만드는 것이 좋다.
- 자원을 획득하여 자원 관리 객체에 넘긴다 (RAII)
- 자원 관리 객체는 소멸자를 활용해 자원이 확실히 해제되도록 한다
스마트 포인터인 std::auto_ptr가 이 아이디어를 구현하고 있다.
어떠한 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 삭제를 두번 하게 되므로 절대로 안된다.
그래서 auto_ptr은 객체를 복사하면 원본 객체를 null로 만든다.
이러한 특성 때문에 원소가 정상적으로 복사 동작을 해야하는 STL 컨테이너의 원소로는 auto_ptr을 사용할 수 없다.
만약 이러한 특성때문에 auto_ptr을 사용하기 꺼려질 때에는 참조 카운팅 방식 스마트 포인터인 RCSP를 사용할 수 있다.
std::shared_ptr이 대표적인 rcsp이다.
shared_ptr은 stl의 원소로도 사용할 수 있다.
이 두 포인터는 소멸자 내부에서 delete[]가 아니라 delete를 사용하므로, 동적으로 할당한 배열에 대해서 사용하는 것은 좋지 않다.
동적 배열은 이제 vector와 string으로 거의 대체되므로 괜찮을 것이다.
만약 정말 필요하다면 boost::scoped_array와 boost::shared_array를 참조하라
14) 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
auto_ptr과 shared_ptr은 힙에 생성되는 객체를 처리할 수 있는 스마트 포인터이다.
만약 힙에 생성되지 않는 자원은 어떻게 해야할까?
예를 들면 뮤텍스의 lock과 같은 것 말이다. 잠금을 잊지 않고 풀어줘야 하기 때문이다.
그럴땐 다음과 같이 직접 RAII를 적용한 클래스를 만든다.
class Lock{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
}
만약 이 객체에 대해 복사를 시도할 때에는 동작을 어떻게 정의해야 할까?
- 복사를 금지한다. 복사 연산자를 private으로 만들면 된다.
- shared_ptr과 같이 관리 자원에 대해 참조 카운팅을 수행한다. shared_ptr은 원래 참조 카운트가 0이 되면 해당 객체를 삭제하지만, deleter를 정의해 해당 동작을 지정할 수 있다.
class Lock{ public: explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) { lock(mutexPtr.get()); } private: std::shared_ptr<Mutex> mutexPtr; }
- 진짜로 복사한다. deep copy를 통해 해당 자원까지 복사시킨다.
- 관리중인 자원의 소유권을 옮긴다. auto_ptr처럼 말이다.
15) 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자
RAII 객체에서 직접 실제 자원으로 변환할 방법이 필요하다.
명시적 변환과 암시적 변환 두가지 방법이 있다.
- 명시적 변환: get()과 같은 함수로 자원의 포인터를 얻을 수 있도록 한다
- 암시적 변환: ->와 *의 연산자 오버로딩으로 자원에 접근하도록 만들거나 암시적 변환 함수를 직접 제공하도록 한다
명시적 변환이 귀찮다 하더라도 암시적 변환은 원하지 않는 타입 변환이 일어날 수 있는 가능성을 키우므로, 시의 적절하게 사용하자.
이러한 접근 방법을 열어주는 것이 캡슐화에 위배될까 걱정은 하지말자.
애초에 자원 관리 객체의 목적은 은닉이 아니다!
shared_ptr처럼 참조 카운팅 메커니즘만 잘 은닉되면 좋은 설계로 보여진다.
'C++ > Effective C++' 카테고리의 다른 글
Effective C++ (26) ~ (30) (0) | 2021.08.22 |
---|---|
Effective C++ (21) ~ (25) (0) | 2021.08.16 |
Effective C++ (16) ~ (20) (0) | 2021.08.14 |
Effective C++ (6) ~ (10) (0) | 2021.08.09 |
Effective C++ (1) ~ (5) (0) | 2021.07.31 |
댓글