이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 121p ~ 132p
요약
항목 13: 자원 관리에는 객체가 그만!
auto_ptr
이 관리하는 객체는 두 개 이상의auto_ptr
객체가 물고 있으면 안 된다는 요구사항이 있기 때문에 동적으로 할당되는 모든 자원에 대한 관리 객체로서는 최선이 아닐 수 있다.- 예를 들어, STL 컨테이너는 원소들이 정상적인 복사 동작을 가져야 하기 때문에
auto_ptr
은 컨테이너의 원소로 허용되지 않는다.
- 예를 들어, STL 컨테이너는 원소들이 정상적인 복사 동작을 가져야 하기 때문에
auto_ptr
을 쓸 수 없는 상황이라면 그 대안으로 참조 카운팅 방식 스마트 포인터(reference-counting smart pointer: RCSP)가 좋다.- RCSP는 특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터이다.
- 동작이 가비지 컬렉션과 흡사하지만, 참조 상태가 고리를 이루는 경우를 없앨 수 없다는 차이가 있다.
void f()
{
// createInvestment에서 반환된 객체를 가리킴
std::tr1::shared_ptr<Investment> pInv1(createInvestment());
// pInv1과 pInv2가 그 객체를 동시에 가리킴
std::tr1::shared_ptr<Investment> pInv2(pInv1);
// 상동
pInv1 = pInv2;
} // 소멸
- 자원 관리의 핵심은 자원을 관리하는 객체를 써서 자원을 관리하는 것이다.
auto_ptr
,tr1::shared_ptr
은 그렇게 하는 방법 중 몇 가지이다.
auto_ptr
과tr1::shared_ptr
은 소멸자 내부에서 delete 연산자를 사용한다.(delete[] 아님!)- 따라서 동적으로 할당한 배열에 사용하면 안된다는 것이다.
- C++ 표준 라이브러리에서는 동적 할당된 배열을 위해 준비된
auto_ptr
,tr1::shared_ptr
같은 클래스가 없다. - 왜냐하면 동적으로 할당된 배열은
vector
및string
으로 대체가 가능하기 때문이다. - 그럼에도 불구하고 동적 할당된 배열에 쓰고 싶으면 부스트의
scoped_array
,shared_array
를 찾아보자.
항목 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
- 항목 13에서는 RAII(자원 획득 즉시 초기화) 기법을 공부했다.
- 힙 기반 자원에 대해 RAII를 적용한
auto_ptr
,tr1::shared_ptr
이 있다.
- 힙 기반 자원에 대해 RAII를 적용한
- 그러나 모든 자원이 힙에서 생기지는 않는다.
- Mutex 객체를 조작하는 C API를 사용 중이라고 가정해보자.
// pm이 가리키는 뮤텍스에 락을 검
void lock(Mutex* pm);
// pm이 가리키는 뮤텍스의 락을 품
void unlock(Mutex* pm);
- 이 상황에서 뮤텍스 잠금을 관리하는 클래스를 하나 만들려고 한다.
- 이 클래스의 목적은 이전에 락을 걸어놓은 뮤텍스를 잊지 않고 풀어주는 것이다.
- 이러한 용도의 자원 관리 클래스는 RAII 법칙을 따라 구성한다.
- 생성시 자원 획득, 소멸시 자원 반환
class Lock {
public:
explicit Lock(Mutex *pm): mutexPtr(pm) {
lock(mutexPtr);
}
~Lock() { unlock(mutexPtr); }
private:
Mutex *mutexPtr;
}
// 사용법
Mutex m; // 사용할 뮤텍스 정의
// 임계 영역을 정하기 위한 블록 생성
{
// 뮤텍스 락
Lock m1(&m);
} // 소멸되면서 언락
- 이 상황에서 Lock 객체가 복사되면 어떻게 작동해야 맞을까?
Lock ml1(&m);
Lock ml2(ml1);
- 이 질문은 이렇게 정리할 수 있다.
- RAII 객체가 복사될 때 어떤 동작이 이루어져야 할까?
- 질문에 대한 일반적인 답은 다음과 같다.
복사를 금지한다.
- 잘 생각해보면 RAII 객체가 복사되도록 놔두는 것 자체가 말이 안되는 경우가 꽤 많다.
Lock
같은 클래스도 어떤 스레드 동기화 객체에 대한 사본이라는게 실제로는 의미가 거의 없다고 볼 수 있다.
- 따라서 복사하면 안 되는 RAII 클래스에 대해서는 반드시 복사가 되지 않도록 막아야 한다.
- 복사를 막는 방법은 복사 연산을
private
으로 만들면 된다.
- 복사를 막는 방법은 복사 연산을
class Uncopyable {
protected:
// 파생 클래스에 생성과 소멸 허용
Uncopyable() {}
~Uncopyable() {}
private:
// 복사 생성자, 복사 대입연산자 방지
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
// 컴파일러가 생성하는 복사 함수는 기본 클래스의 대응 버전을 호출한다.
// 그런데 기본 클래스의 복사 함수가 private으로 선언되어 있어
// Lock 클래스의 복사(복사 생성자, 복사 대입 연산자)를 하려는 순간 컴파일 에러가 발생한다.
class Lock: private Uncopyable {};
관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
- 자원을 사용하고 있는 객체가 소멸될 때까지 그 자원을 해제하지 않는 것이 바람직할 경우도 있다.
- 이런 경우에는 해당 자원을 참조하는 객체의 개수에 대한 카운트를 증가시키는 방식으로 RAII 객체의 복사 동작을 만들어야 한다.
tr1::shared_ptr
이 떠오는 부분이다.- 자신의 RAII 클래스에
tr1::shared_ptr
을 데이터 멤버로 넣으면 될 것 같다. - 그러나
tr1::shared_ptr
은 참조 카운트가 0이 되면 가리키고 있던 대상을 삭제해버리는 것이 기본 동작이다. Lock
의 경우에는 참조 카운트가 0이 되면 잠금 해제만 하고 싶을 뿐이다.
- 따라서
tr1::shared_ptr
을 그냥 사용하는게 아니라 삭제자(deleter)를 지정해서 참조 카운트가 0이 되었을 때 호출되는 함수 혹은 객체를 별도로 지정해주면 된다.
class Lock {
public:
explicit Lock(Mutex *pm): mutextPtr(pm, unlock) {
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
}
- 클래스의 소멸자(사용자가 만들었든 컴파일러가 만들었든)는 비정적 데이터 멤버의 소멸자를 자동으로 호출하게 되어 있다.
mutexPtr
은 비정적 데이터 멤버이기 때문에Lock
클래스의 객체의 소멸자가 호출되면 자동으로mutexPtr
의 소멸자가 호출될 것이다.- 그리고
mutexPtr
은 삭제자로unlock
을 등록해놓았기 때문에 참조 카운트가 0이 되면Mutext
객체의 삭제가 아닌unlock
이 실행될 것이다.
관리하고 있는 자원을 진짜로 복사한다.
- 때에 따라서는 자원을 원하는 대로 복사할 수도 있다.
- 이 경우에는 자원을 다 썼을 때 각각의 사본을 확실하게 해제하는 것이 자원 관리 클래스가 가져야 할 책임이 된다.
- 자원 관리 객체를 복사하면 그 객체가 둘러싸고 있는 자원까지 전부 복사하는, 깊은 복사를 수행한다는 이야기이다.
관리하고 있는 자원의 소유권을 옮긴다.
- 흔한 경우는 아니지만, 어떤 특정 자원에 대해 그 자원을 실제로 참조하는 RAII 객체는 딱 하나만 존재하도록 만들고 싶은 경우가 있다.
- 따라서 해당 객체를 복사할 때는 그 자원의 소유권을 사본 쪽으로 아예 옮겨야 한다.
auto_ptr
의 복사 동작이 바로 이런 예시이다.- 객체 복사 함수(복사 생성자, 복사 대입 연산자)는 컴파일러에 의해 생성될 여지가 있기 때문에, 컴파일러가 생성한 버전의 동작이 원하는 바와 맞지 않으면 직접 객체 복사 함수를 만들 수 밖에 없다.
항목 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자
createInvestment
와 같은 팩토리 함수를 호출한 결과물인 포인터를auto_ptr
이나tr1::shared_ptr
과 같은 스마트 포인터를 사용하여 담으면 RAII 원칙에 따라 자원을 관리할 수 있게 된다.- 이 상황에서 다음과 같은 함수를 사용하고 싶다고 가정해보자.
std::tr1::shared_ptr<Investment> pInv(createInvestment());
// 투자금이 유입된 이후로 경과한 날수
int daysHeld(const Investment* pi);
// 에러
int days = daysHeld(pInv);
daysHeld
함수에서는Investment
의 실제 포인터를 원하지,tr1::shared_ptr<Investment>
타입의 객체를 원하지는 않는다.- 따라서 RAII 클래스의 객체가 감싸고 있는 실제 자원을 반환할 방법이 필요해지는데, 일반적으로는 다음과 같은 방법을 사용한다.
명시적 변환(explicit conversion)
암시적 변환(implicit conversion)
tr1::shared_ptr
과auto_ptr
은 명시적 반환을 수행하는 get이라는 멤버 함수를 제공한다.- 이 함수를 사용하면 실제 포인터의 사본을 얻어낼 수 있다.
int days = daysHeld(pInv.get());
- 제대로 만들어진 스마트 포인터 클래스는 역참조 연산자(operator->, operator*)도 오버로딩을 하고 있기 때문에 암시적 변환도 쉽게 할 수 있다.
class Investment {
public:
bool isTaxFree() const;
}
Investment* createInvestment();
std::tr1::shared_ptr<Investment> pi1(createInvestment());
bool taxable1 = !(pi1->isTaxFree());
std::auto_ptr<Investment> pi2(createInvestement());
bool taxabl2 = !((*pi2).isTaxFree());
- RAII 객체 안에 들어 있는 실제 자원을 얻어낼 필요가 종종 생기기 때문에, 암시적 변환 함수를 제공하여 자원 접근을 매끄럽게 할 수 있도록 만드는 경우도 있다.
// C API에서 가져온 함수(매개변수 생략)
FontHandle getFont();
// C API에서 가져온 함수
void releaseFont(FontHandle *fh);
class Font {
public:
explicit Font(FontHandle fh): f(fh) {}
~Font() { releaseFont(f); }
private:
FontHandle f;
}
- 하부 수준 C API는 FontHandle을 사용하도록 만들어져 있으며 규모도 무척 크다고 가정해보자.
- 그러면 Font 객체를 FontHandle 객체로 변환해야 하는 경우도 적지 않을 것이라고 예상할 수 있다.
- 따라서
Font
클래스에서 이를 위한 명시적 변환 함수인get
을 지원해볼 수 있다.
class Font {
public:
// ...
FontHandle get() const { return f; }
}
- 이러면 하부 수준 API를 사용할 때마다
get
을 호출해야 한다.
// 폰트 API의 일부
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f.get(), newFontSize);
- 문제는 없지만 매번
get
을 호출하는 것이 귀찮게 느껴질 수도 있으니 암시적으로Font
클래스를FontHandle
로 변환할 수 있도록 하자.
class Font {
public:
// ...
// 암시적 변환 함수
operator FontHandle() const { return f; }
}
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize);
- 이렇게 되면 C API를 사용하기가 훨씬 쉬워지고 자연스러워진다.
- 하지만 암시적 변환이 들어가면 실수를 저지를 여지가 많아진다.
Font f1(getFont());
FontHandle f2 = f1;
- 위와 같이 원래 의도는
Font
객체를 복사하는 것이었지만,f1
이FontHandle
로 변환되고 나서 복사될 수도 있다. - RAII 클래스를 실제 자원으로 바꾸는 방법으로 명시적 변환을 제공할 것인지, 아니면 암시적 변환을 허용할 것인지에 대한 결정은 RAII 클래스만의 특정한 용도와 사용 환경에 따라 달라진다.
- 늘 그런 것은 아니지만 암시적 변환보다는 get 등의 명시적 변환 함수를 제공하는 쪽이 나을 때가 많다.
- RAII 클래스는 애초부터 데이터 은닉이 목적이 아니다.
- 따라서 실제 자원에 접근하는 함수를 만드는 것이 캡슐화에 위배되는 것은 아니다.
메모
항목 13: 자원 관리에는 객체가 그만!
- 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.
- 일반적으로 널리 쓰이는 RAII 클래스는
tr1::shared_ptr
그리고auto_ptr
이다. 이 둘 가운데tr1::shared_ptr
이 복사 시의 동작이 직관적이기 때문에 대게 더 좋다. 반면auto_ptr
은 복사되는 객체(원본 객쳬)를 null로 만들어 버린다.
항목 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
- RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정된다.
- RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해주는 것이다. 이 외의 방법도 가능하니 참고하자.
항목 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자
- 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을 주어야 한다.
- 자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능하다. 안정성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면 암시적 변환이 괜찮다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(142p ~ 149p) (0) | 2024.01.18 |
---|---|
이펙티브 C++(132p ~ 142p) (0) | 2024.01.17 |
이펙티브 C++(111p ~ 121p) (0) | 2024.01.15 |
이펙티브 C++(99p ~ 111p) (0) | 2024.01.14 |
이펙티브 C++(89p ~ 99p) (1) | 2024.01.12 |