effective c++(chapter3,4)
TRANSCRIPT
자원 관리
NHN NEXT 장문익
resource
사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것 가져와서 다 썻으면 해제해야 , 즉 놓아 주어야 한다 .
동적 할당 메모리 ( 메모리를 할당하고서 해제하지 않으면 memory leak)
자원을 항상 해제되도록 하는 방법
자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며 ,
그 소멸자가 반드시 호출되도록 만든다 .
auto_ptr
포인터와 비슷하게 동작하는 객체 (smart pointer)
가리키고 있는 대상에 대해 소멸자가 자동으로 delete 를 불러주도록 설계되어 있다 .
한 객체를 동시에 둘이 가리킬 수 없다 . 복사생성하거나 대입하면 한 auto_ptr 은 null이 된다 .
std::auto_ptr<Investment>(createInvestment());
자원 관리에 객체를 사용하는 방법 1
자원을 획득한 후에 자원 관리 객체에 넘긴다 .
자원 획득 즉 초기화 (Resource Acquisition is Initialization:RAII)
자원 획득과 자원 관리 객체의 초기화가 한 문장에서 이루어 진다 .
자원 관리에 객체를 사용하는 방법 2
자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다 .
소멸자는 어떤 객체가 소멸될 때 자동적으로 호출되므로 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가 제대로 이루어지게 된다 .
auto_ptr 의 특성
std::auto_ptr<Investment>pInv1(creteInvestment());
std::auto_ptr<Investment>pInv2(pInv1); // pInv2 가 가리지는 객체는 pInv1 이 가리키는 객체
// pInv1 은 null, auto_ptr 의 특성pInv1 = pInv2 // pInv1 는 객체를 가리키고 , pInv2 는 null
auto_ptr 가 답인가 ?
복사 동작이 이루어지면 이전의 것은 null 이 된다 .
auto_ptr 가 관리하는 객체는 두개 이상의 auto_ptr 가 물고 있으면 안 된다 .
이 두 가지 특성때문에 동적으로 할당되는 모든 자원에 대한 관리 객체로서 auto_ptr 를 쓰는 것은 최선이 아닌 듯하다 .
참조 카운팅 방식 스마트 포인터
reference-counting smart pointer(RCSP)
특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0 이 되면 해당 자원으로 삭제하는 스마트 포인터
garbage collection 방식과 유사
std::tr1::shared_ptr<Investment>pInv(createInvestment());
reference-counting smart pointer 의 특성
std::tr1::shared_ptr<Investment>pInv1(createInvestment());
std::tr1::shared_ptr<Investment>pInv1(pInv2); // auto_ptr 과 달리 pInv1, pInv2 가 //
동시에 객체를 가리킬 수 있다 .
pInv1 = pInv2; // 변화없음
auto_ptr, tr1::shared_ptr 의 공통 특성
소멸자 내부에서 delete 연산자를 사용한다 .
delete [] 연산자가 아니다 .
즉 , 동적 할당한 배열에 auto_ptr, tr1::shared_ptr 을 사용하면 안 된다 .
std::auto_ptr<std::string>aps(new std::string[10]); // error
std::tr1::shared_ptr<int> spi(new int[1024]);; // error
RAII 객체가 복사될 때 어떻게 해야하나 ?
복사를 금지한다 .
class Lock : private Uncopyable {
public :
…
};
RAII 객체가 복사될 때 어떻게 해야하나 ?
관리하고 있는 자원에 대해 참조 카운팅을 수행한다 .
자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 해제하면 안 되는 경우 tr1::shared_ptr 를 이용 ?
tr1::shared_ptr 를 참조 카운트가 0 이 될 때 자신이 가리키고 있던 대상을 삭제하도록 되어있기 때문에 안 된다 .
tr1:shared_ptr 는 ‘삭제자 (delete)’ 지정을 허용한다 .
shared_ptr 삭제자 활용
class Lock {
public :
explicit Lock(Mutex *pm) // shared_ptr 를 초기화하는데 , 가리킬 포인터로: mutexPtr(pm, unlock // Mutex 객체의 포인터를 사용하고 삭제자로 unlock 함수를 사용{
lock(mutexPtr.get());
)
private :
std::tr1::shared_ptr<Mutex>mutexPtr;
}
RAII 객체가 복사될 때 어떻게 해야하나 ?
관리하고 있는 자원을 진짜로 복사한다 .
깊은 복사 (deep copy)
RAII 객체가 복사될 때 어떻게 해야하나 ?
관리하고 있는 자원의 소유권을 옮긴다 .
명시적 변환 (explicit conversion)
tr1::shared_ptr 및 auto_ptr 은 명시적 변환을 수행하는 get 이라는 멤버 함수를 제공한다 .
get 함수를 사용하면 각 타입으로 만든 스마트 포인터 객체에 들어있는 실제 포인터를 얻어낸다 .
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment *pi);
// int days = daysHeld(pInv); // error
int days = daysHeld(pInv.get());
자원 접근과 명시적 , 암시적 변환
안전성만 따지만 명시적 변환이 대체적으로 낫다 .
하지만 사용자 편의성을 놓고 보면 암시적 변환이 괜찮다 .
“ 맞게 쓰기에는 쉽게 , 틀리게 쓰기에는 어렵게“
new
메모리가 할당된다 .
할당된 메모리에 대해 한 개 이상의 생성자가 호출된다 .
delete
기존에 할당된 메모리에 대해 한 개 이상의 소멸자가 호출된다 .
그 후에 그 메모리가 해제된다 .
객체 배열 delete
st::string *stringPtr1 = new std::string;
st::string *stringPtr1 = new std::string[100];
…
delete stringPtr1;
delete [] stringPtr2;
typedef std::string AddressLines[4];
std::string *pal = new AddressLines
// delete pal;
delete [] pal;
new 로 생성한 객체는 스마트 포인터로…
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
// processWidget(new Widget, priority()); error!
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
std::tr1::shared_ptr 의 생성자는 explicit 로 선언되어 있기 때문에 ‘ new Widget’ 표현식에 의해 만들어진 포인터가 tr1::shared_ptr 타입의 객체로 바꾸는 암시적인 변환이 없다 .
매개변수 호출 순서에 따른 위험성
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
연산이 실행되는 순서priority -> “new Widget” -> tr1::shared_ptr?
반드시 위의 순서대로 되지는 않는다 .
priority 가 몇 번째로 호출될지는 모른다 .
문제는 priority 호출부분에서 error 가 발생하면 “ new Widget” 으로 만들었던 포인터가 유실될 수 있다 .
해결책
std::tr1::shared_ptr<Widget> pw(new widget); // new 로 생성한 객체를 스마트 포인터에
processWidget(pw, priority()); // prority 호출에 문제가 발생해도 포인터 유실 없음// memory leak 방지
그래서 new 로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장을 만들어야 한다 .
설계 및 선언
제대로 쓰기 쉽게 , 엉터리로 쓰기 어렵게
class Date {
public :
Date(int month, int day, int year);
…
};
Date d(30, 3, 1995); // 월 , 일이 바뀜Date d(3, 40, 1955); // 일이 이상한 값
struct Day {
explicit Day(int d) : val (d) {}
int val;
};
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
…
};
Date d(30, 3, 1955) // 자료형이 틀렸다 .
Date d(Day(30), Month(3), Year(1995));
// 자료형이 틀렸다 .
Date d(Month(3), Day(30), Year(1995));
Investment * createInvestment();
std::tr1::shared_ptr<Investment>createInvestment();
반환값이 shared_ptr 이라는 것을 사용자가 알 수 있게 한다 .
이 객체를 삭체하는 것을 깜빡하고 넘어가더라도 share_ptr 가 알아서 해제한다 .
tr1::shared_ptr 의 특성 이용 1
생성 시점에 자원 해제 함수 (‘ 삭제자‘ ) 를 엮을 수 있다 .
std::tr1::shared_ptr<Investment>pInv(0, getRidOfInvestment); // error
std::tr1::shared_ptr<Investment*>pInv(static_cast<Investment*>)(0), getRidOfInvest-mentl
// 0 은 int, tr1::share_ptr 가 요구하는 것은 Investmet* 타입의 실제 포인터이기 때문에 static_cast
tr1::shared_ptr 의 특성 이용 2
교차 DLL 문제 해결 객체 생성 시에 어떤 동적 링크 라이브러리의 new 를 썼는데 그 객체를 삭제할 때는 이전의 DLL 과 다른
DLL 에 있는 delete 를 섰을 경우 tr1::shared_ptr 의 기본 삭제자는 tr1::shared_ptr 이 생성된 DLL 과 동일한 DLL 에서 delete 를
사용하도록 만들어져 있다 .
좋은 클래스 설계
새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가 ?
객체 초기화는 객체 대입과 어떻게 달라야 하는가 ?
새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가 ?
새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가 ?
기존의 클래스 상속 계통망에 맞출 것인가 ?
어떤 종류의 타입 변환을 허용할 것인가 ? (implicity, explicit)
어떤 연산자와 함수를 두어야 의미가 있을까 ?
좋은 클래스 설계
표준 함수들 중 어떤 것을 허용하지 말 것인가 ?
새로운 타입의 멤버에 대한 접근권한은 어느 쪽에 줄 것인가 ?
‘ 선언되지 않은 인터페이스’로 무엇을 둘 것인가 ?
새로 만드는 타입이 얼마나 일반적인가 ?
정말로 필요한 타입인가 ?
pass_by_value?
class Person {
public :
Person();
virtual ~ Person();
…
private :
std::string name;
std::string address;
};
class Student : public Person {
public :
Student();
~Student();
…
private:
std::string schoolName;
std::string schoolAddress;
};
pass-by-value? 비용이 너무 크다 .
bool validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);
plato 로부터 매개변수 s 초기화시키기 위해 Sudent 복사 생성자 호출
s 는 validateStudent 가 복귀할 때 소멸 결국 , 복사 생성자 1 번 , 소멸자 1 번 Student 객체가 생성될 때마다 string 객체 2
개 생성 Student 객체는 Person 객체 파생이므로 Per-
son 생성 , string 객체 2 개 생성 소멸도 생성과 마찬가지…
reference-to-const
호출에 따르는 비용을 줄일 수 있다 .
bool validateStudent(const Student& s); // 새로 만들어지는 객체 같은 것이 없기 때문에
// 생성자와 소멸자가 전혀 호출되지 않는다 .
// const 로 전달된 student 객체를 보호
복사손실 문제 (slicing problem)
class Window {
public:
…
std::string name() const;
virtual void display() const;
};
class WindowWithScrollBars : public Window {
public:
…
virtual void display() const;
};
void printNameAndDisplay(Window s)
{
std::cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
복사손실 문제
매개변수 w 생성된다 .
하지만 매개변수가 Windows 객체이므로 , 이 객체를 상속받은 WindowWithScrollBars 객체의 고유 정보들은 손실된다 .
결국 , 호출하는 display() 함수도 Windows 객체의 것이다 .
복사손실 문제 해결
void printNameAndDisplay(const Windows& w)
{
std::coutn << w.name();
w.display();
}
w 를 참조자로 전달하면 w 는 어떤 종류의 윈도우가 넘겨지더라고 그 윈도우의 성질을 갖게 된다 .
함수에서 객체 반환할때 참조자 반환하지말자
class Rational {
public :
Rational(int numerator = 0, int denominator = 1);
…
private :
int n, d;
friend
const Rational
operator*(const Rational& lhs, const Rational rhs);
};
Rational a(1, 2);
Rational b(3, 5);
Rational c = a * b; // 객체가 생성되지 않았는데 참조자를 반환하고 있다 .
스택을 이용한 객체 반환
cosnt Rational& operation*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 생성자를 호출retrun result; // result 는 지역객체으로 함수가 끝날 때 소멸됨에도 불구하고
참조자를 반환}
힙을 이용한 객체 반환
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 생성자 호출 , delete 부담
return *result;
}
static 을 이용한 객체 반환
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational result; // 반환할 참조자가 가리킬 정적객체result = …;
return result;
}
정적객체는 공유하는 값이므로 활용에 제한이 있을 뿐더러 , 스레디 안전성 문제도 있다 .
객체를 반환하는 올바른 방법
inline const Rational operator*(const Rational& 1hs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs,d * rsh.d);
}
새로운 객체를 반환하게 만드는 것이 정도이다 .
데이터 멤버가 선언될 곳은 private
private 으로 선언하면 데이터 멤버에 접근하기 위해서 멤버 함수를 써야 한다 .
어떤 데이터 멤버는 public 이고 , 어떤 데이터 멤버는 private 이라면 일관성이 사라진다 .
함수를 통해서만 데이터 멤버에 접근할 수 있도록 구현해 두면 , 데이터 멤버를 나중에 계산식으로 대체할 수 있을 것이다 .(캡슐화 , encapsulation)
public 데이터 멤버가 있고 , 이것을 제거한다고 가정해보자 . 캡슐화가 얼마나 중요한지 알 수 있다 .
protected 도 마찬가지로 안전하지 않다 .
결국은 캡슐화를 고려하였을 때 , 데이터 멤버는 private 에 선언하는 것이 답이다 .
멤버함수보다는 비멤버 비프렌드 함수
class WebBrowser {
public:
…
void clearCache();
void clearHistory();
void removeCookies();
}
class WebBrowser {
public:
…
void clearEverything(); // clearCache,
// clearHistory,
// removeCookies
…
};
비멤버 버전
void clearBrowser (WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
멤버 버전이 좋을까 ? 비멤버 버전이 좋을까 ?
비멤버 버전의 장점
캡슐화 피키징 유연성 (packaging flexibility) 가 높아진다 .
컴파일 의존도도 낮춘다 .
확장성이 높아진다 .
결국 , 비멤버 방법이 멤버 함수보다 여러모로 낫다 .
namespace 를 활용한 비멤버 함수
namespace WebBrowserStuff {
class WebBrowser {…};
void clearBrowser {WebBrowser& wb};
…
}
namesapce 를 활용한 비멤버 함수의 장점
namespace 는 class 와 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있다 .
다시 말해 , 필요할 때 기능을 불러다 쓰면 된다 . WebBrowser 객체가 없어도 쓸 수 있다 .
응용도가 높은 편의 함수들과 응용도가 낮은 편의 함수를 구분하여 관리하면 컴파일 의존성에서 비교적 자유로울 수 있다 .
클래스 멤버 함수로 사용하게 되면 기능을 쪼개는 것이 불가능하다 .
편의 함수 전체를 여러 개의 헤더 파일에 ( 그러나 하나의 namespace) 나누어 놓으면 편의 함수 집합의 확장도 쉬워진다 . 해당 네임스페이스에 비멤버 비프렌드 함수를 추가하면 끝 .
타입변환이 모든 매개변수에 적용된다면 비멤버 함수로
// 유리수를 나타내는 클래스class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
const Rational operator*(const Rational& rhs) const;
private:
…
};
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // OK
result = result * oneEighth; // OK
result = oneHalf * 2; // OK
result = 2 * oneHalf; // error
result = oneHalf.operator*(2); // OK
result = 2.operator*(oneHalf); // error
암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어있어야 한다 .
암시적 변환을 가능하게 하기위해서
class Rational {
…
};
const Rational operator*(const Rational& lhs, const Rational& rhs) // 비멤버 함수로 만들었다 .
{ // 모든 인자에 대해 암시적 타입 변환 return Raional(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth;
예외를 던지지 않는 swap
// 표준 라이브러리에서 제공하는 swap
namespce std {
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
pimpl 관용구
class Widget Impl {
public :
…
private :
int a, b, c;
std::vector<double> V;
…
};
class Widget {
public :
Widget (const Widget& rhs);
Widget& operator = (const Widget& rhs)
{
…
*pImpl = *(rhs.pImpl);
…
}
…
private :
WidgetImpl *pImpl;
};
표준 swap 알고리즘의 빈틈
위의 Widget 객체를 직접 맞바꾼다면 ? pImpl 포인터만 바꾸는 것이 아니다 .
Widget 객체 세 개를 복사하고 , WidgetImpl 객체 세 개도 복사한다 .
비효율적이다 .
std::swap 특수화
namespace std {
template<> // 완전 템플릿 특수화를 컴파일러에게 알려 준다 .
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl);
}
}
class Widget {
public :
…
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
…
};
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
함수는 부분 특수화가 되지 않는다 .
// Widget 이 클래스가 아닌 클래스 템플릿template<typename T>
class WidgetImpl {…};
template<typename T>
class Widget {…};
namespace std {
template<typename T>
void swap<Widget<T>>(Widget<T>& a,
Widget<T>& b)
{ a.swap(b); }
}
위 코드는 적법하지 않다 .
C++ 은 클래스 템플릿의 부분 특수화는 인정함수 템플릿에 대해서는 허용하지 않는다 .
함수 템플릿의 부분적 특수화는 오버로드
namespace std {
template<typename T>
void swap(Widget<T>& a,
Widget<T>& b)
{ a.swap(b) };
}
위의 코드는 유효하지 않다 .
std 내의 템플릿에 대한 완전 특수화는 OK
std 에 새로운 템플릿을 추가하는 것은 OK 아님
템플릿 전용 버전으로 만들자
namespace WidgetStuff {
…
template<typename T>
class Widget {…};
…
template<typename T> // 비멤버 swap 함수void swap(Widget<T> a, Widget<T> b) // 이번엔 std 네임스페이스의 일부가 아님{
a.swap(b);
}
}
인자 기반 탐색 (argument-dependent lookup) 혹은 쾨니그 탐색 (Koenig lookup)
어떤 swap 이 호출될까 ?
template<typename T>
void doSomething(T& obj1, T& obj2)
{
…
swap(obj1, obj2);
…
}
std 에 있는 일반형 swap?
std 의 일반형을 특수화한 버전 ?
T 타입 전용의 버전
T 타입 전용버전이 우선 호출 , 없으면 일반형
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // std::swap 을 이 함수 안으로 끌어온다 .
…
swap(obj1, obj2); // T 타입 전용의 swap 을 호출한다 .
}
정리하면
std::swap 이 특정 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자 .
멤버 swap 을 제공했으면 , 이멤버를 호출하는 비멤도 swap 도 제공하자 .
클래스에 대해서는 , std::swap 도 특수화 하자 .
사용자 입장에서 swap 을 호출할 때는 , std::swap 에 대한 using 선언을 넣어 준 후 네임스페이스 한정 없이 swap 을 호출하자 .
사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하지만 std 에 어떤 것이라도 새로 추가할 수는 없다 .