effective c++(chapter3,4)

64
자자 자자 NHN NEXT 장장장

Upload: -

Post on 23-Jun-2015

257 views

Category:

Software


3 download

TRANSCRIPT

Page 1: Effective c++(chapter3,4)

자원 관리

NHN NEXT 장문익

Page 2: Effective c++(chapter3,4)

resource

사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것 가져와서 다 썻으면 해제해야 , 즉 놓아 주어야 한다 .

동적 할당 메모리 ( 메모리를 할당하고서 해제하지 않으면 memory leak)

Page 3: Effective c++(chapter3,4)

자원을 항상 해제되도록 하는 방법

자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며 ,

그 소멸자가 반드시 호출되도록 만든다 .

Page 4: Effective c++(chapter3,4)

auto_ptr

포인터와 비슷하게 동작하는 객체 (smart pointer)

가리키고 있는 대상에 대해 소멸자가 자동으로 delete 를 불러주도록 설계되어 있다 .

한 객체를 동시에 둘이 가리킬 수 없다 . 복사생성하거나 대입하면 한 auto_ptr 은 null이 된다 .

std::auto_ptr<Investment>(createInvestment());

Page 5: Effective c++(chapter3,4)

자원 관리에 객체를 사용하는 방법 1

자원을 획득한 후에 자원 관리 객체에 넘긴다 .

자원 획득 즉 초기화 (Resource Acquisition is Initialization:RAII)

자원 획득과 자원 관리 객체의 초기화가 한 문장에서 이루어 진다 .

Page 6: Effective c++(chapter3,4)

자원 관리에 객체를 사용하는 방법 2

자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다 .

소멸자는 어떤 객체가 소멸될 때 자동적으로 호출되므로 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가 제대로 이루어지게 된다 .

Page 7: Effective c++(chapter3,4)

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

Page 8: Effective c++(chapter3,4)

auto_ptr 가 답인가 ?

복사 동작이 이루어지면 이전의 것은 null 이 된다 .

auto_ptr 가 관리하는 객체는 두개 이상의 auto_ptr 가 물고 있으면 안 된다 .

이 두 가지 특성때문에 동적으로 할당되는 모든 자원에 대한 관리 객체로서 auto_ptr 를 쓰는 것은 최선이 아닌 듯하다 .

Page 9: Effective c++(chapter3,4)

참조 카운팅 방식 스마트 포인터

reference-counting smart pointer(RCSP)

특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0 이 되면 해당 자원으로 삭제하는 스마트 포인터

garbage collection 방식과 유사

std::tr1::shared_ptr<Investment>pInv(createInvestment());

Page 10: Effective c++(chapter3,4)

reference-counting smart pointer 의 특성

std::tr1::shared_ptr<Investment>pInv1(createInvestment());

std::tr1::shared_ptr<Investment>pInv1(pInv2); // auto_ptr 과 달리 pInv1, pInv2 가 //

동시에 객체를 가리킬 수 있다 .

pInv1 = pInv2; // 변화없음

Page 11: Effective c++(chapter3,4)

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

Page 12: Effective c++(chapter3,4)

RAII 객체가 복사될 때 어떻게 해야하나 ?

복사를 금지한다 .

class Lock : private Uncopyable {

public :

};

Page 13: Effective c++(chapter3,4)

RAII 객체가 복사될 때 어떻게 해야하나 ?

관리하고 있는 자원에 대해 참조 카운팅을 수행한다 .

자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 해제하면 안 되는 경우 tr1::shared_ptr 를 이용 ?

tr1::shared_ptr 를 참조 카운트가 0 이 될 때 자신이 가리키고 있던 대상을 삭제하도록 되어있기 때문에 안 된다 .

tr1:shared_ptr 는 ‘삭제자 (delete)’ 지정을 허용한다 .

Page 14: Effective c++(chapter3,4)

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;

}

Page 15: Effective c++(chapter3,4)

RAII 객체가 복사될 때 어떻게 해야하나 ?

관리하고 있는 자원을 진짜로 복사한다 .

깊은 복사 (deep copy)

Page 16: Effective c++(chapter3,4)

RAII 객체가 복사될 때 어떻게 해야하나 ?

관리하고 있는 자원의 소유권을 옮긴다 .

Page 17: Effective c++(chapter3,4)

명시적 변환 (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());

Page 18: Effective c++(chapter3,4)

자원 접근과 명시적 , 암시적 변환

안전성만 따지만 명시적 변환이 대체적으로 낫다 .

하지만 사용자 편의성을 놓고 보면 암시적 변환이 괜찮다 .

“ 맞게 쓰기에는 쉽게 , 틀리게 쓰기에는 어렵게“

Page 19: Effective c++(chapter3,4)

new

메모리가 할당된다 .

할당된 메모리에 대해 한 개 이상의 생성자가 호출된다 .

Page 20: Effective c++(chapter3,4)

delete

기존에 할당된 메모리에 대해 한 개 이상의 소멸자가 호출된다 .

그 후에 그 메모리가 해제된다 .

Page 21: Effective c++(chapter3,4)

객체 배열 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;

Page 22: Effective c++(chapter3,4)

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 타입의 객체로 바꾸는 암시적인 변환이 없다 .

Page 23: Effective c++(chapter3,4)

매개변수 호출 순서에 따른 위험성

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

연산이 실행되는 순서priority -> “new Widget” -> tr1::shared_ptr?

반드시 위의 순서대로 되지는 않는다 .

priority 가 몇 번째로 호출될지는 모른다 .

문제는 priority 호출부분에서 error 가 발생하면 “ new Widget” 으로 만들었던 포인터가 유실될 수 있다 .

Page 24: Effective c++(chapter3,4)

해결책

std::tr1::shared_ptr<Widget> pw(new widget); // new 로 생성한 객체를 스마트 포인터에

processWidget(pw, priority()); // prority 호출에 문제가 발생해도 포인터 유실 없음// memory leak 방지

그래서 new 로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장을 만들어야 한다 .

Page 25: Effective c++(chapter3,4)

설계 및 선언

Page 26: Effective c++(chapter3,4)

제대로 쓰기 쉽게 , 엉터리로 쓰기 어렵게

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;

};

Page 27: Effective c++(chapter3,4)

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));

Page 28: Effective c++(chapter3,4)

Investment * createInvestment();

std::tr1::shared_ptr<Investment>createInvestment();

반환값이 shared_ptr 이라는 것을 사용자가 알 수 있게 한다 .

이 객체를 삭체하는 것을 깜빡하고 넘어가더라도 share_ptr 가 알아서 해제한다 .

Page 29: Effective c++(chapter3,4)

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

Page 30: Effective c++(chapter3,4)

tr1::shared_ptr 의 특성 이용 2

교차 DLL 문제 해결 객체 생성 시에 어떤 동적 링크 라이브러리의 new 를 썼는데 그 객체를 삭제할 때는 이전의 DLL 과 다른

DLL 에 있는 delete 를 섰을 경우 tr1::shared_ptr 의 기본 삭제자는 tr1::shared_ptr 이 생성된 DLL 과 동일한 DLL 에서 delete 를

사용하도록 만들어져 있다 .

Page 31: Effective c++(chapter3,4)

좋은 클래스 설계

새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가 ?

객체 초기화는 객체 대입과 어떻게 달라야 하는가 ?

새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가 ?

새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가 ?

기존의 클래스 상속 계통망에 맞출 것인가 ?

어떤 종류의 타입 변환을 허용할 것인가 ? (implicity, explicit)

어떤 연산자와 함수를 두어야 의미가 있을까 ?

Page 32: Effective c++(chapter3,4)

좋은 클래스 설계

표준 함수들 중 어떤 것을 허용하지 말 것인가 ?

새로운 타입의 멤버에 대한 접근권한은 어느 쪽에 줄 것인가 ?

‘ 선언되지 않은 인터페이스’로 무엇을 둘 것인가 ?

새로 만드는 타입이 얼마나 일반적인가 ?

정말로 필요한 타입인가 ?

Page 33: Effective c++(chapter3,4)

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;

};

Page 34: Effective c++(chapter3,4)

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 개 생성 소멸도 생성과 마찬가지…

Page 35: Effective c++(chapter3,4)

reference-to-const

호출에 따르는 비용을 줄일 수 있다 .

bool validateStudent(const Student& s); // 새로 만들어지는 객체 같은 것이 없기 때문에

// 생성자와 소멸자가 전혀 호출되지 않는다 .

// const 로 전달된 student 객체를 보호

Page 36: Effective c++(chapter3,4)

복사손실 문제 (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);

Page 37: Effective c++(chapter3,4)

복사손실 문제

매개변수 w 생성된다 .

하지만 매개변수가 Windows 객체이므로 , 이 객체를 상속받은 WindowWithScrollBars 객체의 고유 정보들은 손실된다 .

결국 , 호출하는 display() 함수도 Windows 객체의 것이다 .

Page 38: Effective c++(chapter3,4)

복사손실 문제 해결

void printNameAndDisplay(const Windows& w)

{

std::coutn << w.name();

w.display();

}

w 를 참조자로 전달하면 w 는 어떤 종류의 윈도우가 넘겨지더라고 그 윈도우의 성질을 갖게 된다 .

Page 39: Effective c++(chapter3,4)

함수에서 객체 반환할때 참조자 반환하지말자

class Rational {

public :

Rational(int numerator = 0, int denominator = 1);

private :

int n, d;

friend

const Rational

operator*(const Rational& lhs, const Rational rhs);

};

Page 40: Effective c++(chapter3,4)

Rational a(1, 2);

Rational b(3, 5);

Rational c = a * b; // 객체가 생성되지 않았는데 참조자를 반환하고 있다 .

Page 41: Effective c++(chapter3,4)

스택을 이용한 객체 반환

cosnt Rational& operation*(const Rational& lhs, const Rational& rhs)

{

Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 생성자를 호출retrun result; // result 는 지역객체으로 함수가 끝날 때 소멸됨에도 불구하고

참조자를 반환}

Page 42: Effective c++(chapter3,4)

힙을 이용한 객체 반환

const Rational& operator*(const Rational& lhs, const Rational& rhs)

{

Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 생성자 호출 , delete 부담

return *result;

}

Page 43: Effective c++(chapter3,4)

static 을 이용한 객체 반환

const Rational& operator*(const Rational& lhs, const Rational& rhs)

{

static Rational result; // 반환할 참조자가 가리킬 정적객체result = …;

return result;

}

정적객체는 공유하는 값이므로 활용에 제한이 있을 뿐더러 , 스레디 안전성 문제도 있다 .

Page 44: Effective c++(chapter3,4)

객체를 반환하는 올바른 방법

inline const Rational operator*(const Rational& 1hs, const Rational& rhs)

{

return Rational(lhs.n * rhs.n, lhs,d * rsh.d);

}

새로운 객체를 반환하게 만드는 것이 정도이다 .

Page 45: Effective c++(chapter3,4)

데이터 멤버가 선언될 곳은 private

private 으로 선언하면 데이터 멤버에 접근하기 위해서 멤버 함수를 써야 한다 .

어떤 데이터 멤버는 public 이고 , 어떤 데이터 멤버는 private 이라면 일관성이 사라진다 .

함수를 통해서만 데이터 멤버에 접근할 수 있도록 구현해 두면 , 데이터 멤버를 나중에 계산식으로 대체할 수 있을 것이다 .(캡슐화 , encapsulation)

public 데이터 멤버가 있고 , 이것을 제거한다고 가정해보자 . 캡슐화가 얼마나 중요한지 알 수 있다 .

protected 도 마찬가지로 안전하지 않다 .

결국은 캡슐화를 고려하였을 때 , 데이터 멤버는 private 에 선언하는 것이 답이다 .

Page 46: Effective c++(chapter3,4)

멤버함수보다는 비멤버 비프렌드 함수

class WebBrowser {

public:

void clearCache();

void clearHistory();

void removeCookies();

}

class WebBrowser {

public:

void clearEverything(); // clearCache,

// clearHistory,

// removeCookies

};

Page 47: Effective c++(chapter3,4)

비멤버 버전

void clearBrowser (WebBrowser& wb)

{

wb.clearCache();

wb.clearHistory();

wb.removeCookies();

}

멤버 버전이 좋을까 ? 비멤버 버전이 좋을까 ?

Page 48: Effective c++(chapter3,4)

비멤버 버전의 장점

캡슐화 피키징 유연성 (packaging flexibility) 가 높아진다 .

컴파일 의존도도 낮춘다 .

확장성이 높아진다 .

결국 , 비멤버 방법이 멤버 함수보다 여러모로 낫다 .

Page 49: Effective c++(chapter3,4)

namespace 를 활용한 비멤버 함수

namespace WebBrowserStuff {

class WebBrowser {…};

void clearBrowser {WebBrowser& wb};

}

Page 50: Effective c++(chapter3,4)

namesapce 를 활용한 비멤버 함수의 장점

namespace 는 class 와 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있다 .

다시 말해 , 필요할 때 기능을 불러다 쓰면 된다 . WebBrowser 객체가 없어도 쓸 수 있다 .

응용도가 높은 편의 함수들과 응용도가 낮은 편의 함수를 구분하여 관리하면 컴파일 의존성에서 비교적 자유로울 수 있다 .

클래스 멤버 함수로 사용하게 되면 기능을 쪼개는 것이 불가능하다 .

편의 함수 전체를 여러 개의 헤더 파일에 ( 그러나 하나의 namespace) 나누어 놓으면 편의 함수 집합의 확장도 쉬워진다 . 해당 네임스페이스에 비멤버 비프렌드 함수를 추가하면 끝 .

Page 51: Effective c++(chapter3,4)

타입변환이 모든 매개변수에 적용된다면 비멤버 함수로

// 유리수를 나타내는 클래스class Rational {

public:

Rational(int numerator = 0, int denominator = 1);

int numerator() const;

int denominator() const;

const Rational operator*(const Rational& rhs) const;

private:

};

Page 52: Effective c++(chapter3,4)

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

암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어있어야 한다 .

Page 53: Effective c++(chapter3,4)

암시적 변환을 가능하게 하기위해서

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;

Page 54: Effective c++(chapter3,4)

예외를 던지지 않는 swap

// 표준 라이브러리에서 제공하는 swap

namespce std {

template<typename T>

void swap(T& a, T& b)

{

T temp(a);

a = b;

b = temp;

}

}

Page 55: Effective c++(chapter3,4)

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;

};

Page 56: Effective c++(chapter3,4)

표준 swap 알고리즘의 빈틈

위의 Widget 객체를 직접 맞바꾼다면 ? pImpl 포인터만 바꾸는 것이 아니다 .

Widget 객체 세 개를 복사하고 , WidgetImpl 객체 세 개도 복사한다 .

비효율적이다 .

Page 57: Effective c++(chapter3,4)

std::swap 특수화

namespace std {

template<> // 완전 템플릿 특수화를 컴파일러에게 알려 준다 .

void swap<Widget>(Widget& a, Widget& b)

{

swap(a.pImpl, b.pImpl);

}

}

Page 58: Effective c++(chapter3,4)

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);

}

}

Page 59: Effective c++(chapter3,4)

함수는 부분 특수화가 되지 않는다 .

// 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++ 은 클래스 템플릿의 부분 특수화는 인정함수 템플릿에 대해서는 허용하지 않는다 .

Page 60: Effective c++(chapter3,4)

함수 템플릿의 부분적 특수화는 오버로드

namespace std {

template<typename T>

void swap(Widget<T>& a,

Widget<T>& b)

{ a.swap(b) };

}

위의 코드는 유효하지 않다 .

std 내의 템플릿에 대한 완전 특수화는 OK

std 에 새로운 템플릿을 추가하는 것은 OK 아님

Page 61: Effective c++(chapter3,4)

템플릿 전용 버전으로 만들자

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)

Page 62: Effective c++(chapter3,4)

어떤 swap 이 호출될까 ?

template<typename T>

void doSomething(T& obj1, T& obj2)

{

swap(obj1, obj2);

}

std 에 있는 일반형 swap?

std 의 일반형을 특수화한 버전 ?

T 타입 전용의 버전

Page 63: Effective c++(chapter3,4)

T 타입 전용버전이 우선 호출 , 없으면 일반형

template<typename T>

void doSomething(T& obj1, T& obj2)

{

using std::swap; // std::swap 을 이 함수 안으로 끌어온다 .

swap(obj1, obj2); // T 타입 전용의 swap 을 호출한다 .

}

Page 64: Effective c++(chapter3,4)

정리하면

std::swap 이 특정 타입에 대해 느리게 동작할 여지가 있다면 swap 멤버 함수를 제공하자 .

멤버 swap 을 제공했으면 , 이멤버를 호출하는 비멤도 swap 도 제공하자 .

클래스에 대해서는 , std::swap 도 특수화 하자 .

사용자 입장에서 swap 을 호출할 때는 , std::swap 에 대한 using 선언을 넣어 준 후 네임스페이스 한정 없이 swap 을 호출하자 .

사용자 정의 타입에 대한 std 템플릿을 완전 특수화하는 것은 가능하지만 std 에 어떤 것이라도 새로 추가할 수는 없다 .