U E D R , A S I H C RSS

More EffectiveC++/Techniques1of3

1. Techniques

1.1. Item 25: Virtualizing constructors and non-member functions

  • Item 25: 생성자와 비멤버 함수를 가상으로 돌아가게 하기.

1.1.1. Virtual Constructor : 가상 생성자

가상 생성자, 이것에 관해서 생각해 보기는 좀 생소하다. 사실 가상 함수를 부를려면, 객체에대한 참조나, 포인터를 가지고 있어야 하는데, 생성할때 부터 가상(virtual) 함수를 사용한다? 좀 이상하지 않은가? 어떨때, 어떻게 이 가상 생성자 라는걸 써먹을수 있을꺼?

사실 가상 생성자라는건 존재하지 않는다. 하지만 이를 비슷하게 구현하는 것이다. 예를들자면 당신이 newsletter을 보내는 어플리케이션을 짜는데, 여기에 글과 그림 데이터 인자로 구성시킨다고 가정하면 이렇게 만들수 있을 것이다.

~cpp 
class NLComponent{                      // News Letter 의 부모 인자
public:
    ...
};
class TextBlock:public NLComponent{     // 글을 표현하는 인자
public:
    ...
};
class Graphic:public NLComponent{       // 그림을 표현하는 인자
public:
    ...
};
class NewsLetter {                       // 글과, 그림을 가지는 News Letter
public:
    ...
private:
    list<NLComponent*> components;      // 글과 그림 인자의 저장소
};
이런 코드는 다음과 같이 표현된다.


list 클래스는 STL로 이루어져 있는데, Item 35를 참고하면 정보를 얻을수 있다. 단순히 이중 연결 리스크(double linked list)라고 생각하면 무리가 없을 것이다.

NewsLetter 객체는 아마 디스크에서 자료를 적재할 것이다. NewsLetter가 디스크에서 자료를 가지고 보여주기 위해 istream을 사용해서 NewsLetter를 구성할 객체들을 생성한다고 가정한다면 다음과 같은 코드들을 대강 만들수 있는데

~cpp 
class NewsLetter {
public:
    NewsLetter(istream& str);
    ...
};
가짜 코드로 이 생성자를 대강 표현하면

~cpp 
NewsLetter::NewsLetter(istream& str)
{
    while(str){
        다음에 추가될 인자 객체(component object)를 str로 부터 읽는다.

        newsletter의 인자중 리스트인 components에 만든 객체를 넣는다.
    }
}
혹은 약간 수를 써서 readComponent를 호출하는 식으로 바꾼다면

~cpp 
class NewsLetter{
publicc:
    ...
private:
    // 순수히 디스크에 있는 데이터를 읽어 드리는 istream과 자료 str에서
    // 해당 자료를 해석하고, 알맞는 객체를 만들고 반호나하는 readComponent객체
    static NLComponent * readComponent(istream& str);
};
NewsLetter::NewsLetter(istream& str)
{
    while (str) {
        // readComponent가 해석한 객체를 newsletter의 리스트 마지막에 추가시키는 과정
        components.push_back(readComponent(str));
    }
}
readComponent가 무엇을 어떻게 하는지 궁리해 보자. 위에 언급한듯이 readComponent는 리스트에 넣을 TextBlock나 Graphic형의 객체를 디스크에서 읽어 드린 자료를 바탕으로 만들어 낸다. 그리고 최종적으로 만들어진 해당 객체의 포인터를 반환해서 list의 인자를 구성하게 해야 할것이다. 이때 마지막 코드에서 가상 생성자의 개념이 만들어 져야 할것이다. 입력되 는자료에 기초되어서, 알아서 만들어 인자. 개념상으로는 옳지만 실제로는 그렇게 구현될수는 없을 것이다. 객체를 생성할때 부터 형을 알고 있어야 하는건 자명하니까. 그렇다면 비슷하게 구현해 본다?

가상 생성자의 방식의 한 종류로 특별하게 가상 복자 생성자(virtual copy constructor)는 널리 쓰인다. 이 가상 복사 생성자는 새로운 사본의 포인터를 반환하는데 copySlef나 cloneSelf같은 복사의 개념으로 생성자를 구현하는 것이다. 다음 코드에서는 clone의 이름을 쓰였다.

~cpp 
class NLComponent{
public:
    // 가상 복사 생성자 virtual copy constructor 선언
    virtual NLComponent * clone() const = 0;
    ...
};
class TextBlock: public NLComponent{
public:
    virtual TextBlock * clone() const // 가상 복사 생성자 선언
    { return new TextBlock(*this); }
    ...
};
class Graphic:public NLComponent{
public:
    virtual Graphic * clone() const // 가상 복사 생성자 선언
    { return new Graphic(*this); }
    ...
};
보다시피 클래스의 가상 복사 생성자는 실제 복사 생성자를 호출한다. 그러므로 "복사" 의미로는 일반 복사 생성자와 수행 기능이 같다 하지만 다른 점은 만들어진 객체마다 제각각의 알맞는 복사 생성자를 만든다는 점이 다르다. 이런 clone이 NewsLetter 복사 생성자를 만들때 NLComponent들을 복사하는데 기여를 한다. 어떻게 더 쉬운 작업이 되는지 다음 예제를 보면 이해 할수 있을 것이다.

~cpp 
class NewsLetter{
public:
    NewsLetter(const NewsLetter* rhs);  // NewsLetter의 복사 생성자
    ...
private:
    list<NLComponent *> components;
};
NewsLetter::NewsLetter(const NewsLetter* rhs)
{
    for (list<NLComponent*>::constiterator it = rhs.components.begin(); 
        it != rhs.components.end(); 
        ++it){
        // it은 rhs.components의 현재를 가리키는 인자로, 복사되어야 할 객체이다.
        // 형을 가리는 작업 없이 그냥 clone을 이용해 계속 복사해 버리면 각 형에 알맞는 복사
        // 생성자가 불리면서 복사 되는 것이다.
        components.push_back((it*)->clone());
    }
}
STL이 생소하다면 나중에 익혀라 하지만 위의 아이디어는 주석문으로 인해 이해가 갈것이다. 가상 복사 생성자로 인해서, 객체 복사가 상당히 간편해 진다.

1.1.2. Making Non-Member Functions Act Virtual : 비멤버 함수를 가상 함수처럼 동작하게 하기

생성자는 실제로 가상 함수가 될수 없다. 마찬가지로 비멤버 함수들 역시 마찬가지 이리라, 하지만 그러한 기능이 필요할 떄가 있다. 바로 앞 예제에서 NLComponent와 그것에서 유도된 클래스들을 예를 들어 보자면 이들을 operator<<으로 출력을 필요로 할때 이리라. 뭐 다음과 같이 하면 문제 없다.

~cpp 
class NLComponent{
public:
    // operator<<의 가상 함수
    virtual ostream& operator<<(ostream& str) const = 0;
    ...
};
class TextBlock:public NLComponent{
public:
    virtual ostream& operator<<(ostream& str) const;
};
class Graphic:public NLComponent{
public:
    virtual ostream& operator<<(ostream& str) const;
};

TextBlock t;
Graphic g;
...
t << cout;

g << cout;
하지만 출력해야할 스트림 객체가 righ-hand 객체라는것이 사용자 입장에서 불편하게 만든다. 우리가 보통 사용하는 것처럼 스트림 객체에 출력할 객체를 넣는 다는 개념이 적용되지 않는 것이리라. 하지만, 전역 함수나 friend함수를 이용해서 구현한다면 더이상 가상함수로 구현할수가 없게 된다. 여기서의 방법이 비멤버 함수를 이용하는 것이다.

다음과 같이 가상 함수로 출력을 구현하고, 전역이든, namespace로 묶여 있든, 비 멤버 함수로 스트림 객체와의 출력의 고리를 연결시켜서, 비멤버 함수가 가상함수처럼 돌아가도록 구현한다.

~cpp 
class NLComponent {
public:
  virtual ostream& print(ostream& s) const = 0;
  ...
};

class TextBlock: public NLComponent {
public:
  virtual ostream& print(ostream& s) const;
  ...
};

class Graphic: public NLComponent {
public:
  virtual ostream& print(ostream& s) const;
  ...
};

inline
ostream& operator<<(ostream& s, const NLComponent& c)
{
  return c.print(s);
}
가상 함수의 역할을 비멤버(non-member)함수로 구현한 사례이다. 비 멤버 함수이지만 inline을 통해서 가상 함수와의 직접 연결 통로를 만들었다.

비멤버 함수의 가상(virtual)관련 사용에 대하여 더 다루자면 내용이 복잡해 진다. 이에 대한 관련 사항은 Item 31을 참고하라

1.2. Item 26: Limiting the number of objects of a class

  • Item 26: 객체 숫자 제한하기.

자 지금까지 객체에 대한 이야기로 당신은 미칠 지경에 빠졌을 꺼다. 게다가 이것은 당신을 혼란에 빠트릴 수준까지 왔을 것이다. (첫줄만 직독직해.) 예를들어서 당신의 시스템에 프린터가 하나 밖에 없을때 프린터를 대변하는 객체의 숫자를 하나로 제한해야 하지 않을까? 아니면 하나의 파일에 대하여 16개의 파일 접근자만 허용할때 따위 같은거 말이다. 여기서는 그 해법에 관해서 생각해 본다.
  • 작성자주 : 이 부분은 Singleton 패턴과 연관해서 생각하면 재미있을 것 같다. Singleton 패턴이 DP에 논의될때 이것을 감안 안한것이 아쉽다. 1995년에 발간이라 STL도 제대로 다루지 않았고, C++의 기본적인 문법을 이용해 구현하였다. MEC++는 Techniques 부분은 C++의 문법과 개념을 극한으로 쓴다는 느낌이 든다.

1.2.1. Allowing Zero or One Objects : 0 혹은 하나의 객체 만을 허용하는 방법


객체들이 생성될때 꼭 하는 일이 있다. 바로 생성자를 부르는 일이다. 하지만 이걸 막을수 있는 방법이 있을까? 가상 쥐운 방법은 생성자를 private(사역)인자로 묶어 버리는 것이다. 다음을 보자

~cpp 
class CantBeInstantiated {
private:
    CantBeInstantiated();
    CantBeInstantiated(const CantBeInstantiated&);
    ...
};
자 이렇게 하면 생성자가 private상태라서 외부에서 생성자를 부를수 없기 때문에, 외부에서 객체를 생성할수 없다. 이런 아이디어를 바탕으로 생성 자체에 제한을 가해 버리는 것이다. 그럼 처음에 말한 프린터의 예제를 위의 아이디어를 구현해 보자.

~cpp 
class PrintJob;     // 미리 선언, 프린터 작업에 쓰이는 객체. 참고 Effective C++ Item 34
class Printer {
public:
    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...
    friend Printer& thePrinter();   // 이 friend 함수가 유일한 객체 하나를 유지 시키고
                                    // 객체의 사용권한을 부여 하는 역할을 한다.
private:
    Printer();
    Printer(const Printer& rhs);
    ...
};
Printer& thePrinter()
{
    static Printer p;               // 단일의 Printer 객체(the single printer object)
    return p;
}    
해당 디자인은 세가지의 중점으로 이해 하면 된다. 첫번째 Printer클래스의 생성자를 private(사역)인자로 설정한다. 이는 객체 생성을 제한한다. 두번째 전역 함수인 thePrinter를 Printer클래스의 friend로 선언한다. 그래서 이 thePrinter가 첫번째에서의 제한에 상관없이 관여 가능하도록 만든다. 마지막으로(세번째) 전역함수인 thePrinter 내부에 정적(static) Printer 객체를 만든다. 이는 오직 하나만의 객체를 thePrinter내부에 유지시킨다.

클라이언트 입장에서는 이렇게 사용하면 된다.

~cpp 
class PrintJob {
public:
  PrintJob(const string& whatToPrint);
  ...

};
string buffer;

...                              // 버퍼의 인자를 넣는 코드

thePrinter().reset();
thePrinter().submitJob(buffer);  // 상수 참조(const reference)라서, 
                                 // 임시 객체가 생성되어 실행된다.
하지만 이렇게 구현시에는 thePrinter가 "전역 공간을 사용해야 한다." 것으로 코드를 약하게 만든다. 알다 시피, 전역 공간의 사용은 되도록이면 피해야 하는 방법이며, thePrinter를 Printer클래스 내부에 숨기기를 추천하다. thePrinter를 Printer클래스 내부 메소드로 넣어 버리고, friend를 삭제해 보자.

~cpp 
class Printer {
public:
  static Printer& thePrinter();     // 외부에서 호출 가능하도록 static으로 선언하고
  ...

private:
  Printer();
  Printer(const Printer& rhs);
  ...

};

Printer& Printer::thePrinter()      // friend만 없앴지 마찬가지 방법을 제시한다.
{
  static Printer p;
  return p;
}
그리고 이를 사용하는 클라이언트 측에서는 다음과 같은 방법으로 사용하면 되겠다.

~cpp 
Printer::thePrinter().reset();
Printer::thePrinter().submitJob(buffer);
전역 공간 사용에 대한 문제의 해결책의 또다른 접근 방법이라고 한다면, name space를 사용하는 것이다. 다음과 같이 단순히 PrintingStuff name space로 묶어 버린다.

~cpp 
namespace PrintingStuff {                    // namespace의 시작
  class Printer {                            // namespace로서 이 클래스는 
  public:                                    // PrintingStuff namespace 안에 존재하는 것이다.

    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...
    friend Printer& thePrinter();
  private:
    Printer();
    Printer(const Printer& rhs);
    ...
 };
 Printer& thePrinter()                        // 이 friend 함수 역시 전역이 아닌 
 {                                            // PrintingStuff namespace안에 존재 하는 것이다.
    static Printer p;
    return p;
 }
}                                             // namespace의 끝
그리고 thePrinter를 호출하려면 이제는 이렇게 해야 한다.

~cpp 
PrintingStuff::thePrinter().reset();
PrintingStuff::thePrinter().submitJob(buffer);
물론 using 키워드를 사용해서 해당 이름을 자유롭게 사용할수 잇다.

~cpp 
using PrintingStuff::thePrinter;    // thePrinter를 현재의 namespace안에서 
                                    // 자유로이 쓸수 있게 만든다.                                            
thePrinter().reset();               // 그리고 사용하는 과정
thePrinter().submitJob(buffer);
thePrinter 를 적용할때 두가지 생각해야할 미묘한 문제점이 있다.

첫번째로 만들어지는 객체의 위치이다. 위의 제시된 두가지의 방법에서, Printer 정적(staitc) 객체가 하나는 friend로 클래스의 제어권을 획득한 함수 내부에 있고, 또 하나는 클래스 멤버 메소드 내부에 있다. 함수에 있는 경우에는 정적(static) 객체는 항상 만들어져 있다. 이 의미는 해당 코드의 프로그램이 시작될때 부터 아예 객체가 만들어 진다는 의미이다. 즉, 한번도 그 객체를 사용하지 않아도, 객체는 이미 만들어져 비용을 지출하게 한다. 반면에, 함수 멤버 메소드 내부에 정적(static)객체를 만들 후자의 경우에는 객체를 만드는 역할을 하는 메소드인 Printer::thePrinter 가 제일 처음 호출될때 객체가 생성된다. 이것은 C++에서 "사용하지 않는 객체에 대한 비용은 지불하지 않는다."의 설계 다소 복잡한 이념에 근간을 둔 개념이다. 그리고 이러한 복잡한 개념은 당신을 해깔리게 만든다.

( DeleteMe Translation unit의 해석이 불분명하다.)
또 이 둘의 다른 취약점은 초기화 되는 시간이다. 우리는 함수의 경우에 초기화 시간을 정확히 알수 있다. 아예 처음 이니까 하지만 멤버 메소드로 구현시에는 모호하다. C++는 확실히 특별하게 해석해야 할 부분(단일 객체에서 소스 코드 몸체 부분 따위)은 정적 인자들에 대한 초기화 순서가 보장 된다. 하지만 서로 다른 해석 부분(translation unit)에 있는 정적 객체들의 초기화 순서에 대해서는 말할수가 없다. 이것은 머리를 아프게만 할뿐이다.

두번째로 미묘한 문제라면, inline과 정적 객체의 관게이다. 다음과 같은 비멤버 버전의 thePrinter를 보면

~cpp 
Printer& thePrinter()
{
    static Printer p;
    return p;
}
다음과 같은 코드의 함수는 매우 짧다. 이런 짧은 함수는 함수보다 inline 시켜서 속도를 높이는 것이 더 효과적이다. 하지만 그럴수가 없다. 왜 그런가 하면, inline의 의미는 정확히 해당 함수가 쓰이는 코드를 현재 함수의 몸체로 교체해 버리는 역할이다. 그런게 이렇게 할경우, 위와 같은 함수는 static객체의 처리에서 의문이 생긴다. 해당 함수가 호출된 곳을 위와 같은 함수 몸체로 교체하면, 각 교체 부분은 전부 독립적인 static 인자를 부여 받는 셈이 되어 버린다. 그래서 정적 인자를 쓴 함수는 inline을 시키지 못하며, 이런 정적 인자의 사용에 따라 일어나는 의문을 internal linkage를 가진 문제 라고 한다. DeleteMe) 날림 요약 수정 필요

자, 똑똑한 사람 이라면 당연히, 지금까지의 코드에서 의문점과 문법에 대한 의아함을 일으 킬수 있다. thePrinter는 둘다 내부에 있는 static 객체의 참조를 반환하는데, 이 "static 객체는 해당 함수,메소드의 영역(scop) 내부에서 쓰여야지 외부에서 쓰이면 안되지 않는가?" 라는 의문이 그것이다. 즉, 클라이언트 입장에서 이들 객체는 숨겨져(hidden)있는 존재이고, 이것을 사용하는 것은 잘못된 방법이다. 라고 말할수 있겠는데, 그래서 아마 당신은 다음과 같이 객체의 숫자를 세고, 제한된 객체의 수보다 더 많은 객체를 사용시 예외를 발생시켜서 문제를 해결하는 것이 더 좋은 방법이라 말할 것이다.

~cpp 
class Printer {
public:
  class TooManyObjects{};                   // 너무 많은 객체를 요구하면 
                                            // 이 예외를 발생 시킨다.
  Printer();
  ~Printer();
  ...
private:
  static size_t numObjects;

  Printer(const Printer& rhs);               // 프린터는 1개만 허용하며, 사본도 허용 하지
                                             // 않는다. Item E27
};                                           
이런 아이디어는 numObject를 사용해서 Printer객체의 수를 제한 시켜 버리는 것이다. 위에도 언급하였듯이 객체의 수가 1개를 초과하면, TooManyObject 예외를 발생시킨다.

~cpp 
size_t Printer::numObjects = 0;             

Printer::Printer()
{
    if (numObjects >= 1) {
        throw TooManyObjects();
    }
    보통의 생성 과정을 수행한다.
    ++numObjects;
}

Printer::~Printer()
{
    보통의 파괴 과정을 수행한다.
    --numObjects;
}
이러한 접근 방법은 매력적이며, 단지 객체의 수를 1개에 국한 하지 않고, 특정 숫자로 조정할수 있는 이점이 있다. 사용하기는 귀찮아도 말이다. 하지만 이 것의 문제를 바로 다루어 주겠다.

1.2.2. Context for Object Construction : 객체의 생성을 위한 구문(관계, 문맥, 상황)

방금 위에서 예제로 제시한 방법 역시 문제는 내포하고 있다. 가령 특별한 프린터인 컬러 프린터에 대한 클래스를 작성한다고 가정해 본다.

~cpp 
class ColorPrinter: public Printer {
  ...
};
그리고 다음과 같이 사용한다고 가정해 보자.

~cpp 
Printer p;
ColorPrinter cp;

첫번째 객체 p는 순조로히 생성된다. 하지만 엄연히 다른 프린터를 대상으로 하고 있는 cp는 생성되지 않고, TooManyObjects 예외를 발생 시킨다. 왜 그러는지 모두들 예상 할것이다. 더불어 비슷 또 다른 경우를 생각 해 본다면.

~cpp 
class CPFMachine {                           // copy, print, fax 전부 할수 있는 기계
private:                                     
  Printer p;                                 // 프리터 기능의 인자.
  FaxMachine f;                              // 팩스 기능의 인자
  CopyMachine c;                             // 복사기 기능의 인자.

  ...

};

CPFMachine m1;                               // 여기 까지는 잘된다.

CPFMachine m2;                               // TooManyObjects 예외를 발생 시킨다.

Printer 객체가 존재할수 있는 세가지의 상황에서 이런 문제는 발생 될수 있다. : 그냥 그들 자체를 선언해서 사용하기. 다른 클래스로 유도될때. 좀더 큰 클래스에서 해당 클래스를 인자로 포용할때 이다. 하지만 숫자로 제어하고, 예외를 발생시키는 방법이 아닌 생성자가 사역(private)인자로 들어간 경우에는 해당 클래스에서 유도된 클래스들도 생성하지 못하며, 다른 클래스의 인자로도 들어갈수가 없어서, 이런 문제들이 봉쇄된다.

자, 이런걸로 한가지 재미있는 것을 만들수 있다. 만약 당신이 C++상에서 더이상 상속 되지 않는 클래스를 만들고 싶을때 어떻게 해야 할까?(주:참고로 Java나 C#의 경우 언어 설계 때부터 아예 해당 기능을 수행을 위한 키워드를 제공한다. 하지만 C++는 제공하지 않는다. 이런 방법을 설계자가 생각한건지, 차후 C++의 개발자들이 생각한건지 놀라울 뿐이다. 바로 이전에 나온 가상 복사 생성자의 아이디어와 비슷하다고 해야 할까)

~cpp 
class FSA {
public:
  // 가짜 생성자들
  static FSA * makeFSA();   // 생성자
  static FSA * makeFSA(const FSA& rhs); // 복사 생성자
  ...

private:
  FSA();
  FSA(const FSA& rhs);
  ...
};

FSA * FSA::makeFSA()
{ return new FSA(); }

FSA * FSA::makeFSA(const FSA& rhs)
{ return new FSA(rhs); }
이렇게 생성자가 사역(private)인자로 들어가 버리면, 해당 클래스에서 유도되는 클래스를 만들기란 불가능 하다. 하지만 이 코드의 문제점은 makeFSA를 이용해 생성하면 항상 delete를 해주어야 한다는 점이다. 이전 예외를 다루는 부분에서도 언급했지만, 이는 자원이 세나갈 여지를 남기는 것이다. 이를 위한 STL의 auto_ptr도 참고하자.(Item 9 참고)

~cpp 
// 기본 생성자
auto_ptr<FSA> pfsa1(FSA::makeFSA());

// 복사 생성자
auto_ptr<FSA> pfsa2(FSA::makeFSA(*pfsa1));

...                            // 자 이제 해당 영역을 벗어나면 객체는 자동 파괴된다.

1.2.3. Allowing Objects to Come and Go : 객체가 오고 감을 허용하기?

이제, 단일한 객체 만들기 방법에 관한 디자인 방법은 알수 있을 것이다. 그리고, 객체를 숫자로 제어하는 것은 세가지의 생성 상황에 의해서 폭잡한 상황을 만들어 나간다는 것을 알것이다. 이것을 위해서 생성자의 사역(private)역시 설명했다. 캡슐화된 thePrinter함수는 Printer라는 단일한 객체를 제한하고, 그것을 사용할수 있게 한다. thePrinter가 대안일까. 하지만 결국 thePrinter는 C++의 일반적인 방법인 이러한 디자인의 코드를 불가능하게 한다.

~cpp 
create Printer object p1;

use p1;

destroy p1;

create Printer object p2;

use p2;

destroy p2;
이런 디자인은 단일 Printer객체에 관해서 행하여 질수는 없다. 하지만 서로 다른 프로그램의 서로 다른 부분에서 Printer객체는 이렇게 사용되어 질수 있다. 이것 역시 허용하지 못하게하는 것까지는 필요 없을것 같은데, 아무튼 오직 하나의 프린터 객체만 유지 시킨다는 것에는 벗어나지는 않는 거다. 하지만 여기에 우리가 해왔던 object-counting방법과, 일찍이 쓴 가짜 생성자(pseudo-constructor)를 혼합해서 기능을 구현해 본다.

~cpp 
class Printer {
public:
    class TooManyObjects{};

    static Printer * makePrinter();        // 가짜 생성자(pseudo-constructors)

    ~Printer();
    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...

private:
    static size_t numObjects;
    Printer();
    Printer(const Printer& rhs);         // 다음과 같은 함수는 복사 방지를 위해
                                         // 구현 되지 않도록 한다. ( E27참고)  DeleteMe)검증필요
};
// 클래스의 static의 정의(definition,비슷하게 초기값 구현)은 반드시 해야 한다.
size_t Printer::numObjects = 0;
Printer::Printer()
{
    if (numObjects >= 1) {
        throw TooManyObjects();
    }
    보통의 생성 과정을 수행한다.
    ++numObjects;
}
Printer * Printer::makePrinter()
{ return new Printer; }
다음과 같은 코드는 이렇게 사용해야만 한다.

~cpp 
Printer p1;               // 생성자가 사역(private)이므로
                          // 에러를 발생 시킨다.

Printer *p2 =  Printer::makePrinter();      // 올바르다. 이것이 기본 생성자이다.

Printer p3 = *p2;                           // 에러! 복사 생성자 역시 사역으로 설정되어 있다.

p2->performSelfTest();                      // 객체 사용하듯이 한다.
p2->reset();                                // 마찬가지
...
delete p2;                                  // 자원이 세나가지 않기위해 제거 해준다.
                                            // auto_ptr을 쓰면 이도 필요 없어 진다.
이러한 기술은 어떠한 숫자의 객체의 제한에도 써먹을수 있는데, 다음과 같은 코딩으로 가능하다 다음은 객체를 10개로 제한 시켜버리는 것이다.

~cpp 
class Printer {
public:
    class TooManyObjects{};
    static Printer * makePrinter();   // // 가짜 생성자(pseudo-constructors)
    static Printer * makePrinter(const Printer& rhs);
    ...
private:
    static size_t numObjects;
    static const size_t maxObjects = 10;       // 아래 참고
    Printer();
    Printer(const Printer& rhs);
};
// 클래스의 static의 정의(definition,비슷하게 초기값 구현)은 반드시 해야 한다.
size_t Printer::numObjects = 0;
const size_t Printer::maxObjects;

Printer::Printer()
{
    if (numObjects >= maxObjects) {
        throw TooManyObjects();
    }
    ...
}
Printer::Printer(const Printer& rhs)
{
    if (numObjects >= maxObjects) {
        throw TooManyObjects();
    }
    ...
}

Printer * Printer::makePrinter()
{ return new Printer; }

Printer * Printer::makePrinter(const Printer& rhs)
{ return new Printer(rhs); }
같은 코드 써서 내용만 늘린 것 같다. 하지만 조금더 언급해 본다면. Printer::maxObjects는 클래스 내부에서 10으로 초기화 시켰는데, 이는 컴파일러의 지원 여부에 따라 static const 멤버의 경우 초기화가 가능한 C++의 규칙이다.(주:참고 내용이 있었는데 몇 장인지 기억 안난다.) 그리고 maxObject에 관하여 변하지 않는 값이기에 enum으로도 쓸수 있는데, 다음과 같다.

~cpp 
class Printer {
private:
    enum { maxObjects = 10 };      // 위의 10 제한과 같은 역할을 한다.
};
그리고 만약에 현재 컴파일러가 클래스 내부에서 초기화를 허용하지 않는다면 이렇게 써서 해결하라.

~cpp 
class Printer {
private:
    static const size_t maxObjects;            // 아무런 초기화를 하지 않는다.
    ...

};
// 여기에서 다음과 같이 초기화가 가능하다.
const size_t Printer::maxObjects = 10;
위의 둘다 상단의 코드와 동일한 역할을 하는 것이다. 좋은 컴파일러 쓰시길..

1.2.4. An Object-Counting Base Class : Object-counting에 기본 클래스를 만들어 본다.

이제까지 거쳐왔던 코드들은 어느 정도의 형태가 잡혀 있다. 이것을 라이브러리로 만들어 놓고, 일정한 규칙으로 만들수는 없을까? (참고로 이와 비슷한 기술이 Item 29 reference-counting에 등장한다. 참고 해보자.) template가 이를 가능하게 해준다.

다음과 같은 template를 이용해서

~cpp 
template<class BeingCounted>
class Counted {
public:
    class TooManyObjects{};        // 던질 예외

    static int objectCount() { return numObjects; }

protected:
    Counted();
    Counted(const Counted& rhs);

    ~Counted() { --numObjects; }

private:
    static int numObjects;
    static const size_t maxObjects;

    void init();          // 생성자에 코드 중복을 피한다.(어차피 같은 코드 쓰니까.)
};                                            

template<class BeingCounted>
Counted<BeingCounted>::Counted()
{ init(); }

template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
{ init(); }

template<class BeingCounted>
void Counted<BeingCounted>::init()
{
    if (numObjects >= maxObjects) throw TooManyObjects();
    ++numObjects;
}
해당 클래스는 오직 기본 클래스로만 쓰이도록 설계되어 졌다. 그러므로, 생성자와 파괴자가 모두 protected(보호)인자로 설정되어 있다. 그리고 init로서 object-counting을 구현한다. init는 설정된 객체의 숫자가 넘어가면, TooManyObjects 예외 객체를 발생 시킨다.

이제 위의 template를 바탕으로 글 초반에 누누히 설명한 object-counting에 기반한 Printer클래스를 만들어 본다.

~cpp 
class Printer: private Counted<Printer> {
public:
    static Printer * makePrinter();     // 가짜 생성자(pseudo-constructors)
    static Printer * makePrinter(const Printer& rhs);

    ~Printer();

    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...

    using Counted<Printer>::objectCount;     // 왜 사용하는지 아래 참고
    using Counted<Printer>::TooManyObjects;  // 마찬가지~
private:
    Printer();
    Printer(const Printer& rhs);
};
이해가 안가는 부분은 역시나 using이 쓰인 곳과 private로 상속 되었다는 점일 것이다.

private를 먼저 설명해 한다. 결론부터 이야기하면 위험성 제거와, 크기의 최적화를 위해서 이다. 다른 이가 Counted<Printer> 포인터를 통해서 Printer객체를 제거하려는 시도를 할지도 모른다. 이를 방지하고, private상속은 Item 24에 명백히 나와 있듯이 Counted내의 가상 함수가 존재 하더라도, Printer에서 해당 가상 함수에 대한 vtbl을 생성하지 않으므로서 overhead를 줄인다.

using을 써서 Counted에 존재하는 인자를 참조 가능하게 만드는 이유는 Printer입장에서 다른 인자들은 신경 쓸필요가 없지만, 몇몇 특정한 인자의 참조와, 사용을 하기 위해서 이다. objectCount는 아래의 주석문의 이유이고,

~cpp 
class Printer: private Counted<Printer> {
public:
    ...
    using Counted<Printer>::objectCount;  // 이 함수로 현재 객체의 숫자를 알수 있다.
    ...                                   // 이거 함수다. 착각 마시길. Counted 클래스를 한번 살펴보라.
};

만약, using을 지원하지 않은 컴파일러라면 다음과 같은 옛날 문법을 사용할수 있다.

~cpp 
class Printer: private Counted<Printer> {
public:
    ...
    Counted<Printer>::objectCount;  // 이 함수로 현재 객체의 숫자를 알수 있다.
    ...                                   // 이거 함수다. 착각 마시길. Counted 클래스를 한번 살펴보라.
};
이는 using이 존재 하지 않았을때 사용된 옛날 문법이다. TooManyObjects 클래스 역시 같은 이유로 using을 이용해서 이름을 사용하게 권한을 열었으며, 이렇게 TooManyObjects를 허용해야 지만 해야지만 클라이언트들이 해당 예외를 잡을 수 있다.

생성자에서 감안할 것은 없다. 그냥 일반적인 생성과정을 기술해 주면 무리가 없으며, 필요한 작업은 Counted<Printer>의 생성자에서 하기때문에 Printer의 생성자는 다음고 같다.

~cpp 
Printer::Printer()
{
    일반적인 생성자 과정
}
Printer의 생성자에서는 객체를 점검하는 코드가 들어가서는 안된다. 왜냐하면, Counted<Printer>에서 점검하고 이상 있어서 예외를 던진다면, 다음에 불릴 Printer생성자는 아예 불리지도 않는다. 알겠나?

또 하나는 object-counting시에 초기값이다. 이렇게 하면 초기화가 된다.

~cpp 
template<class BeingCounted>                 
int Counted<BeingCounted>::numObjects;  // 객체의 선언과, 자동으로 0으로 초기화가 된다.
DeleteMe) 이것이 되는 과정도 정확히 이해가 가지 않는다. 되는거였나 여태? 고민된다.

maxObjects의 초기화 역시 문제로 대두 되는데, 다음과 같이 초기화 시킨다.

~cpp 
const size_t Counted<Printer>::maxObjects = 10;
만약 FileDescriptor가 유도된 클래스라면 이렇게 한다.

~cpp 
const size_t Counted<FileDescriptor>::maxObjects = 16;
이는 비록 해당 객체가 private로 상속 받았지만, 유도된 객체가 생성될때 다음과 같이 초기화를 시킬수 있다. 초기화 리스트와 비슷하다고 해야 할까?

1.3. Item 27: Requiring or prohibiting heap-based objects.

  • Item 27: Heap영역을 사용하는 객체 요구하기 or 피하기.

이번 장에서는 Heap영역에서 객체 사용에 관하여 다루어 본다.

때로 자살을 허용해야 하는 객체를 만들때도 있다. 표현이 격한가? "delete this"따위 같이 말이다. 이러한 객체는 heap영역에 배치 되는데, 확실하게 제거만 해준다면 자원이 셀 이유는 없다. 그렇다. 주의 깊게 다루어야 하는 객체들이다. embedded system같이 열악한 환경에서 이들에 잘못다루어서 일어난 실수는 치명적인 손상을 부른다. 이제 하나하나 heap영역에 관련한 내용을 다룬다.

1.3.1. Requiring Heap-Based Objects : Heap 기반 객체들이 필요

우선 객체를 Heap영역 상에서만 생성되고 사용하는 객체로 제한하는 것에 관해서 생각해 보자. Heap영역에 자리를 차지하는 객체는 모두 new를 호출해서 한자리씩 하니, 뭐, 간단히 new를 막아 버리면 되는것이다. 그렇다면 반대로 Heap영역이 아닌 객체들은 모두 new를 호출하지 않고, 자동으로 묵시적 생성되고, 묵시적 파괴되어 진다는 의미가 됙ㅆ다.

Item 26에 다룬것과 같이 생성자와 파괴자를 사역(private)로 묶어 버리는 아이디어가 객체 생성 제한의 시작이 될수 있을 것이다. 하지만, 둘다 묶어 버리는건 너무 과잉이다. 그냥 파괴자만을 사역(private)로 묶어 버리고, 생성자는 공역(public)으로 놓아도 효과는 비슷하다. 다음의 예제를 보자.

~cpp 
class UPNumber {
public:
    UPNumber();
    UPNumber(int initValue);
    UPNumber(double initValue);
    UPNumber(const UPNumber& rhs);

    // 가짜 파괴자(pseudo-destructor) 상수 멤버 메소드, 
    // 상수 객체를 제거 할수 있게 하기 때문에
    void destroy() const { delete this; }
    ...
private:
    ~UPNumber();    // 파괴자가 사역(private)으로 선언되었다.
};
클라이언트가 이렇게 선언된 클래스를 기반한 객체의 사용을 알아 보자면

~cpp 
UPNumber n;                          // 에러! 파괴자 사역(private)이라 작동 할수 없다.
UPNumber *p = new UPNumber;          // 통과
...
delete p;                            // 에러! 역시 파괴자 사역(private) 이유
p->destroy();                        // 통과
자 다음과 같이, UPNumber 클래스는 Heap상에서만 사용할수 있는 객체만을 생성 할수 있다. 이것의 대안으로는 Item 26 마지막에 나온 예제와 같이 모든 생성자 만을 사역(private)화 시키는 것이지만, 이 아이디어의 문제는 많은 생성자가 모두 사역(private)으로 있어야 하고, 그것들을 반드시 기억해야 한다는 점이다. 기본 생성자는 물론, 복사 생성자를 전부 선언해 주어야 한다. 그렇지 않으면 컴파일러는 기본적으로 모두 공역(public)으로 취급하고 지역 변수를 만들수 있다. 결과적으로, 파괴자만을 사역(private)화 시키는 것이 간단하다.

클래스의 생성자와, 파괴자 양쪽을 사역화(private)시켜서 지역 객체(non-heap object)로 만드는걸 막는데도, 약간의 제한 사항이 발생한다. 그 제한사항이란, 상속 관계와 포함(containment:has-a)관계에서 발생한다. 다음의 예제를 보면.

~cpp 
class UPNumber { ... };             // 생성자와 파괴자 모두 사역(private)로 
                                    //선언되었다고 가정한다.

class NonNegativeUPNumber: 
    public UPNumber { ... };        // 에러! 생성자와 파괴자를 컴파일 못한다.

class Asset {
private:
    UPNumber value;                 // 에러! 생성자와 파괴자를 컴파일 못한다.
    ...      
};
이런 문제는 해결하기 어렵지 안하. 상속 관계의 문제는 생성자는 공역(public)으로 유지하고, 파괴자만을 보호(proteced) 관계로 바꾸면 되는것이고, 포함(contain) 관계에서는 해당 포함 인자를 pointer로 바꾸고, 초기화 리스트에서 생성해 버리고, 파괴자에서 약간만 신경써주면 된다. 위의 코드의 해결책은 다음과 같다.

~cpp 
class UPNumber { ... };             // 파괴자를 보호(protected)로 설정한다.
                                    // 대신 생성자는 공역(public)으로 설정한다. 
                                    // 기능상 상관 없으므로

class NonNegativeUPNumber:
    public UPNumber { ... };        // 이제는 유도되는 데는 지장 없다.
                                    // 유도되는 클래스들은 protected인자는
                                    // 접근하는데 문제가 없다.

class Asset {
public:
    Asset(int initValue);
    ~Asset();
    ...
private:
    UPNumber *value;                // 위에서의 객체 선언을 포인터로 바꾸었다.
};

Asset::Asset(int initValue)
: value(new UPNumber(initValue))    // 다음과 같이 초기화 리스트로 객체를 만들고
{ ... }

Asset::~Asset()
{ value->destroy(); }                 // 파괴시에 신경써준다.(resource leak방지)

1.3.2. Determining Whether an Object is On The Heap : Heap에 객체를 올릴지 결정하기.

자, 지금까지 다소 맹목적(?)으로 Heap영역에 객체 올리기에만 열중했다. 그럼 여기에서는 "on the heap"의 의미를 확실한 테스트로서 알아 보도록 하겠다. 앞서 써먹은 NonNegativeUPNumber를 non-heap 객체로 만드는건 뭐 틀리지 않은 것이다.

~cpp 
NonNegativeUPNumber n;                // fine
이것이 허용된다는 것이다. 자, 그럼 지역 객체인 NonNegativeUPNumber의 n에서 UPNumber의 부분은 heap위에 있는 것이 아니다. 맞는가? 이 답변은 클래스의 설계(design)과 적용(implementation)에 기인해야 이해 할수 있을 것이다. 답을 찾아가 보자.UPNumber가 반드시 heap영역에 존재 해야 한다는 것에 관해 not okay를 던지면서 시작해 보자. 어떻게 우리는 이러한 제한 사항을 해결해야 하는 걸까?

이것은 그리 쉽지 않다. UPNumber 생성자가 이것을 유도 받은 모든 객체에게, Heap영역 기반의 객체로 만들어 버리라고 결정 하는것은 가능하지 않다. 기본 클래스로 쓰인 UPNumber의 생성자가 다음의 두 상황을 체크하는 것은 불가능하다.

~cpp 
NonNegativeUPNumber *n1 =  new NonNegativeUPNumber;     // Heap영역
NonNegativeUPNumber n2;                                 // 비 Heap영역
그렇자민 아마 조금 다른 방법의 접근을 할수 있다. 바로 Heap영역에 올라가는 객체는 항상 new를 호출하는것, 그리고 이 new의 호출은 new operator와 operator new와의 상호 작용에서 작업이 이루어 지는 것이다. 자세한 내용은 Item 8을 참고하고, 다음과 같이 UPNumber를 고쳐서 유도되는 객체들 마져 제한을 걸수 있다.

~cpp 
class UPNumber {
public:
    // 만약 non-heap 객체라면 다음의 예외를 발생 시킨다.
    class HeapConstraintViolation {};

    static void * operator new(size_t size);

    UPNumber();
    ...
private:
    static bool onTheHeap;    // 생성자 내부에서 heap영역에 생성되는가 판별인자로 쓴다.
    ...
};

// static 인자의 의 초기화 부분
bool UPNumber::onTheHeap = false;

void *UPNumber::operator new(size_t size)
{
    onTheHeap = true;               // new operator가 불리면 Heap영역을 사용하는것.
    return ::operator new(size);    // operator new로서 메모리를 할당한다.
}
UPNumber::UPNumber()
{
    if (!onTheHeap) {
        throw HeapConstraintViolation();    // 힙이 아니라면 예외를 던진다.
    }
    일반적인 생성 과정을 기술한다.; 
    onTheHeap = false;                    // 객체 생성 플래스를 세팅한다.
}
operator new는 raw메모리 할당을 하고, 해당 메모리에서 생성자를 부르므로서 초기화를 수행한다. operator new에서 onTheHeap을 참으로 설정하여 주면, 생성자에서 이를 검사해서 예외 발생을 하지 않고, 일반 지역 변수로 객체가 선언되면 operator new를 거치지 않으므로, 기본 값인 false인해 생성자에서 예외를 발생시킨다.

하지만 이 코드다 능사가 아닌것이, 다음과 같은 객체의 배열을 선언하면 문제가 된다.

~cpp 
UPNumber *numberArray = new UPNumber[100];
첫째로, 이 경우 operator new가 불리는 것이 아니라. 메모리는 operator new[]로 할당 되기때문에, 문제가 발생하는 것이고, 둘째로 operator new[]에 플래그 값을 주었다고 하더라도, 처음 한번의 operaotr new[]이후에 계속 생성자 100번이 불리면서 첫번째 생성자에서 다시 onTheHeap를 false로 초기화 시키기에, 이후에 불리는 생성자는 전부 onTheHeap이 false값으로 예외를 발생 시켜 버린다.

또 배열이 아니더라도 다음과 같은 경우도 생각해 본다.

~cpp 
new UPNumber(*new UPNumber);
이 경우에는 두가지의 new를 가지고 있다. 그러므로 operator new도 두번 불리고 생성자 역시 두번 불릴 것이다. 프로그래머가 일반적으로 기대하는 다음 순서에서는 아무런 문제가 없다. (Item 8 참고)
  1. operator new가 첫번째 객체에 관해서 불린다.
  2. 첫번째 객체의 생성자가 불린다.
  3. 두번째 객체의 operator new 가 불린다.
  4. 두번째 객체의 생성자가 불린다.
그렇지만 C++언어 상에서 이런 보장에 대한 스펙이 정의 되어 있지 않다 그래서 어떤 컴파일러는 다음과 같이 부를수도 있다.
  1. 첫번째 객체의 operator new가 불린다.
  2. 두번째 객체의 operator new가 불린다.
  3. 첫번째 객체의 생성자가 불린다.
  4. 두번째 객체의 생성자가 불린다. : 여기서 문제가 발생한다.
후자의 순서로 코드가 생성되어도 컴파일러에게 잘못은 전혀 없다. 그렇지만 set-a-bit-in-operator-new 방법을 사용한 상단의 예제는 두번째 순서에서는 실패 할수 밖에 없다.

이런 어려움이 "각 생성자에서 *this가 heap영역에 있는가에 대한 여부를 알아낸다." 라는 아이디어의 근간을 흔드는 것은 아니다. 거기에다가 이런 어려움들은 operator new나 operator new[] 안에서 bit set을 점검해 보는 것이 이런 기본 정보를 결정하는데 신뢰성 있는 방법이 아님을 반증하고 있다. 우리가 필요한 방법을 위해서 한번 생각해 본다.

절망하고 있는가? 그럼 한번 임시로나마 이식성이 떨어지는 영역에서까지 그런 아이디어를 확장해서 생각해 보자. 예를 들어서 많은 시스템 상에서 사실인, 프로그램의 주소 공간은 선형으로 배열되고, 프로그램의 스텍은 위에서 아래로 늘어 난다고 그리고 Heap영역은 밑에서 위로 늘어난다는 사실에 주목해 보자. 그림으로 표현되면 다음과 같은 모습이 된다.


이런 방식으로 구성된 프로그램의 시스템에서 다음과 같은 방법으로 비교를 할수 있지 않을까?

~cpp 
// heap에 대한 여부가 정확하지 않은 점검 방법
bool onHeap(const void *address)
{
    char onTheStack;                // 지역 스택 변수(local stack variable)
    return address < &onTheStack;   // 주소 비교   
}
함수에서 이러한 생각은 참 의미롭다. onHeap함수내에서 onTheStack는 지역 변수(local variable)이다. 그러므로 그것은 스택에 위치할 것이고, onHeap가 불릴때 onHeap의 스텍 프레임은 아마 프로그램 스텍의 가장 위쪽에 배치 될것이다. 스택은 밑으로 증가하는 구조이기에, onTheStack는 방드시 어떠한 stack-based 변수나 객체에 비하여 더 낮은 위치의 메모리에 위치하고 있을 것이다. 만약 address 인자가 onTheStack의 위치보다 더 작다면 스택위에 있을수 없는 것이고, 이는 heap상에 위치하는 것이 되는 것이다.

이러한 구조는 옳다. 하지만 아직 충분히 생각하지 않은 것이 있는데, 그것은 객체가 위치할수 있는 세가지의 위치를 감안하지 않은 근본적인 문제이다. 지역(local) 변수,객체(variable, object)나, Heap영역 상의 객체는 감안해지만 빼먹은 하나의 영역 바로 정적(static)객체의 위치 영역이다.

이들의 위치는 전부 수행되는 시스템에 의존적이다. 하지만 많은 시스템이 stack와 heap이 서로를 향해 증가 하도록 구성되어 있으며, 가장 최하단에 static여역이 자리 잡는 구성으로 되어 있다. 그러므로 앞에 언급한 그림에서 한가지를 추가하면 진짜 메모리의 구성이 된다. 다음과 같이 말이다.:


갑자기 이제 앞서 작성한 onHeap함수가 어떠한 시스템에서도 정확한 수행을 못할 것이 명백해 진다.:heap 객체와 적적(static) 객체를 구별하지를 못한다. 예제에서 보면

~cpp 
void allocateSomeObjects()
{
    char *pc = new char;    // heap 객체: onHeap(pc)
                            // 이것은 true를 반환

    char c;                 // 스택(=:지역) 객체 object: onHeap(&c)
                            // 이 주소는 false 를 반환

    static char sc;         // 정적(static) 객체: onHeap(&sc)
                            // 이 주소는 true 를 반환
  ...
}
이제 heap 객체와 stack 객체를 판별하는 방법에 혼란이 올 것이다. 그리고 이러한 방법은 이식성에도 역시나 문제가 있다. 그래서 결국 compare-the-addresses 방법은 신뢰할수 없는 방법이 된다.

여담이라면, 이러한 방법은 대다수의 시스템에서 사실이지만, 꼭 그렇다고는 볼수 없다. 그러니까 완전히 not portable한게 아닌 semi-portable이라고 표현을 해야 하나. 그래서 앞머리에 잠시나마 생각해 보자고 한것이고, 이러한 시스템 의존적인 설계는 차후 다른 시스템에 이식시에 메모리의 표현 방법이 다르다면, 소프트웨어 자체를 다시 설계해야 하는 위험성이 있다. 그냥 알고만 있자.

우리는 앞쪽에서 "delete this"로 가상 파괴자로 객체가 스스로를 자살 시키는 방법으로 heap객체만을 사용하도록 제한 시키는 방법을 기억할 것이다. 이런 "delete this"식으로의 제거는 추천할 만한 방법이 결코 아니다. ( DeleteMe 모호) 그렇지만, 지우기 위한 객체의 안전성을 아는 것은 heap상에서 포인터가 지칭하는가를 간단히 알아네고자 하는 방법과 같은 것이 아니다. 자, 다시 UPNumber 객체를 가지는 Asset 객체의 관해서 생각해 보자.

~cpp 
class Asset {
private:
    UPNumber value;
    ...

};
Asset *pa = new Asset;
명백히 *pa는 heap상에 위치한 객체이다. 또 명백하게, pa->value의 포인터 역시 delete로 지울수 없다. 왜냐하면 이녀석이 new를 호출해서 만든것이 아니기 떄문이다.

포인터가 지우기 안전한가에 판단은, 포인터가 heap상에 위치하는 객체를 가리키는가를 알아내는 것보다 쉽다. 왜냐하면 우리는 operator new로 인해서 반환되는 주소의 모음으로, 전자의 질문에 관해서 알아 낼수 있기 떄문이다. 여기 예제에 그런 문제에 관한 접근 방식이 기술되어 있다.

~cpp 
void *operator new(size_t size)
{
    void *p = getMemory(size);          // 메모리를 할당하는 어떤 함수를 호출한다.
                                        // 그리고 이것은 out-of-memory 예외를 잡을수 있다.
    할당된 주소를 collection에 저장한다.;
    return p;
}
void operator delete(void *ptr)
{
    releaseMemory(ptr);                 // 메모리를 free한다.

    이번에 지운 주소를 collection에서 제거한다.;
}

bool isSafeToDelete(const void *address)
{
    물어보는 주소가 collection상에 존재하는지 여부를 반환한다.;
}
이러한 것은 간단하다. operator new는 collection에 메모리를 할당하는 주소를 기록하고, operator delete는 그것을 지운다. 그리고 isSafeToDelete는 collection에 해당 주소가 있는지 알려주는 역할을 한다. 만약 operator new와 operator delete가 전역 공간에 있다면 이것은 모든 타입의 작업시에 적용 될것이다.

실제로 세가지 생각이 이러한 디자인을 매달리지 못하게 한다. 첫번째는 전역 공간에 어떤것을 정의하는 극도로 피하려는 경향이다. operator enw나 operator delete같은 미리 정의된 함수에 대하여 특별하게 고친다는 것은 더 그럴 것이다. 둘째로 효율에 관한 문제이다. 모든 메모리 할당에서 overhead가 발생한다는 의미인데, 이것을 유지하겠는가? 마지막으로 걱정되는 것은 평범하지만 중요한 것으로 isSafeToDelete이 모든 수행에 관하여 적용되는 적용하는 것이다. 하지만 이것이 근본적으로 불가능하다고 보이기 때문이다. 조금더 이약 해보자면, 다중 상속이나, virtual base class가 가지는 여러게의 주소들, 이 주소들 때문에 isSafeTo Delete에게 전달되는 주소에 대한 확실한 보장이 없다. 자세한 내용은 Item 24, 31일 참고하라.

자 위의 이유로 이번 아이디어도 쓰레기통으로 들어갈 참이다. 하지만 그 아이디어를 채용해서 C++에서는 abstract base class를 제공할수 있다. 여기서는 abstract mixin base class라고 표현한다.

가상 클래스라고 해석 될수 있는 abstract base 는 초기화 될수가 없다. 덧붙여 말하자면, 이것은 순수 가상 함수를 최소한 하나 이상은 가지고 있어야만 한다. mixin("mix in")클래스는 잘 정의된 기능과 상속되는 어떤 클래스와도 잘 부합되도록 설계되어져 있다. 이런 클래스들은 거의 항상 abstract이다. 그래서 여기에서는 abstract mixin base class는 용도로 operator new로 객체의 할당 여부만 알수 있는 능력만을 유도된 클래스에게 제공한다.

~cpp 
class HeapTracked {                  // mixin class; keeps track of
public:                              // ptrs returned from op. new

    class MissingAddress{};            // 예외 클래스;아래 참고

    virtual ~HeapTracked() = 0;

    static void *operator new(size_t size);
    static void operator delete(void *ptr);

    bool isOnHeap() const;

private:
    typedef const void* RawAddress;
    static list<RawAddress> addresses;
};
이 클래스에서 list데이터 구조체는 C++의 라이브러리에 정의되어 있다.(Item 35참고) list가 하는 일은 예상되는 것과 같이 operator new에서 반환된 주소의 저장이다. 그리고 operator delete는 메모리를 해제하고, list로 부터 해당 주소의 엔트리를 지운다.

HeapTrack 적용은 간단하다. 왜냐하면, operator new와 operator delete가 실제 메모리 할당과 해제를 수행하고 list 클래스는 삽입, 지우기, 그리고 검색 엔진을 포함하기 때문이다. 자 여기 이런 내용의 적용에 관련한 내부 코드를 살피자.

~cpp 
// static 클래스 멤버의 정의는 규칙이다.
list<RawAddress> HeapTracked::addresses;

// HeapTracked의 파괴자는 가상 클래스의 순수 가상 함수이다. (E14참고)
// 그래서 이 파괴자가 구현되어야 만한다.
HeapTracked::~HeapTracked() {}

void * HeapTracked::operator new(size_t size)
{
    void *memPtr = ::operator new(size);  // 메모리 할당

    addresses.push_front(memPtr);         // 해당 주소를 list의 암쪽에 저장
    return memPtr;
}
void HeapTracked::operator delete(void *ptr)
{
    // "iterator"를 얻어서 list에서 검색하는 부분 Item 35참고    
    list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr);

    if (it != addresses.end()) {    // 지울 주소를 찾아서
        addresses.erase(it);        // 해당 엔트리를 지우고
        ::operator delete(ptr);     // 메모리를 해제하고
    } else {                        // 하지만
        throw MissingAddress();     // ptr에 할당이 안되었다면 예외를 던진다.
    }
}

bool HeapTracked::isOnHeap() const
{
  // *this로 할당받은 메모리의 시작점을 얻는 과정;자세한건 밑에 본문
  const void *rawAddress = dynamic_cast<const void*>(this);

  // 주소 list에서 pointer를 찾고 operator new에 의해 반환된다.
  list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), rawAddress);

  return it != addresses.end();      // return whether it was
}                                    // found
이코드는 약간 생소한 list클래스에 대한 것이 보일 것이다. 뭐, 이제는 하도 많이 나와서 별로 안생소 할것 같다. STL이고 Item 35에 모든것이 설명 되어 있다. 하지만 주석을 바탕으로 예제에 관하여 충분히 설명 가능하리라고 본다.

멤버 메소드 isOnHeap에 존재하는 이 구문이 다소 의문이 들것이다.

~cpp 
const void *rawAddress = dynamic_cast<const void*>(this);
( DeleteMe 모호 )
위에서 isSafeToDelete를 구현할때 다중 상속이나 가상 기초 함수으로 여러개의 주소를 가지고 있는 객체가 전역의 해당 함수를 복잡하게 할것이라고 언급했다. 그런 문제는 isOnHeap에서 역시 마찬가지이다. 하지만 isOnHeap는 오직 HeapTracked객체에 적용 시킨 것이기 때문에, dynamic_cast operatror를 활용으로 위의 문제를 제거한다. 간단히 포인터를 dynamic_cast 하는 것은 (혹은 const void* or volatile void* or 알맞는 것으로 맞추어서) 객체의 가장 앞쪽 포인터, 즉, 할당된 메모리의 가장 앞쪽에 주소를 가리키는 포인터를 의미한다. 그렇지만 dynamic_cast는 가상함수를 하나 이상 가지는 객체를 가리키는 포인터에 한해서만 허용 된다. isSafeToDelete함수는 모든 포인터에 관해서 가능하기 때문에 dynamic_cast가 아무런 소용이 없다. isOnHeap는 조금더 선택의 폭이 있어서 this를 const void*로 dynamic_cast하는 것은 우리에게 현재 객체의 메모리 시작점의ㅣ 포인터를 주게 된다. 그 포인터는 HeapTracked::operator new가 반드시 반환해야만 하는 것으로 HeapTrack::operator new의 처음 부분에 있다. 당신의 컴파일러가 dynamix_cast를 지원하면 이러한 기술은 이식성이 높다.

이러한 클래스가 주어지고, BASIC 프로그래머는 이제 heap 에 할당하는 것을 추적할수 있는 능력을 클래스에게 부여 할수 있다. 추적을 원하는 클래스는 반드시 HeapTracked를 상속하도록 만든다. 예를들어서 우리가 Asset객체를 사용할때 해당 객체가 heap-base 객체인지 알고 싶다면 다음과 같이

~cpp 
class Asset: public HeapTracked {
private:
    UPNumber value;
    ...
};
이제 우리는 Asset* 포인터에 관해서 객체의 상태를 물어 볼수 있다.

~cpp 
void inventoryAsset(const Asset *ap)
{
    if (ap->isOnHeap()) {
        ap 는 heap-based asset 이다. 
    }
    else {
        ap 는 non-heap-based asset 이다.
    }
}
이 mixin 객체의 단점이라면 int나 char따위의 built-in 형에는 써먹지를 못하는 것이다. 이것들은 상속을 할수 없기 때문이다.

1.3.3. Prohibiting Heap-Based Objects : Heap영역에 객체 올리기 막기

기나긴 여정이다. 이제 글의 막바지인 주제로 왔다. 여태한 것과 반대로 Heap영역에 객체를 올리는ㄳ을 막을려면 어떻게 해야 할까? 이번에는 이런 반대의 경우에 관해서 생각해 본다.

곧 바로 예제를 보자.

~cpp 
class UPNumber {
private:
    static void *operator new(size_t size);     // 자 이렇게 operator new를
                                                // 사역(private)인자로 만들어 버린다.
    static void operator delete(void *ptr);     // operator delete 역시 마찬가지이다.
    ...
};
클래이언트에서 발생하는 것에 관해 가정하자.

~cpp 
UPNumber n1;                         // 맞다.

static UPNumber n2;                  // 이상 없다.

UPNumber *p = new UPNumber;          // 사역(private)인자인 opreator new가 불려서
                                     // 에러가 발생한다.

26장 부터 꾸준히 잘 봐왔다면 보는 순간 이해가는 예제이다. 그리고 의문까지 제기된다. operator new[]는 어디 있는가? 즉, heap 영역에 배열 선언까지 막기위해서는 operator new[]까지 사역(private)인자로 선언해 주면 된다. (주:분명히 다른 주제인데 지겹다. 너무 같은 것만 반복하는 느낌이 든다.)

흥미롭게 operator new의 사역(private)인자로의 선언은 UPNumber객체를 상속한 객체에서도 같은 능력을 발휘한다는 점이다. 다음의 예제를 보자.

~cpp 
class UPNumber { ... };             // 위와 같은 코드

class NonNegativeUPNumber:          // 이 클래스에서는 operator new에대한 
    public UPNumber {               // 선언이 없다.
    ...
};

NonNegativeUPNumber n1;             // 이상 없다.

static NonNegativeUPNumber n2;      // 역시 이상 없다.

NonNegativeUPNumber *p =            // 사역(private)인자인 opreator new가 불려서
    new NonNegativeUPNumber;        // 에러가 발생한다.
마지막 new를 쓴 부분에서는 NonNegativeUPNumber를 new로 생성할때 부모인 UPNumber부분을 생성할때 private로 제약이 발생되어서 에러가 발생하는 것이다.

반면에 위의 코드가 그대로라면, 다음과 같은 코드 같이 has-a관계의 경우에는 가능하다.

~cpp 
class Asset {
public:
  Asset(int initValue);
  ...

private:
  UPNumber value;
};

Asset *pa = new Asset(100);     // 맞다.
                                // 이경우 Asset::operator new 나 
                                // ::operator new는 불리지만 
                                // UPNumber::operator new는 불리지 않으므로 올바르게 작동한다.
뒷부분은 앞에 했든 heap영역의 주소 기반의 검출과 이식성에 대한 논의이다.

1.4. Item 28: Smart pointers

  • Item 28: 똑똑한 포인터:스마트 포인터

Smart pointer(이하 스마트 포인터)는 객체의 수월한 관리를 위해서 태어난 방법이다. "방법" 이라고 표현한것을 주의하라, 특별히 정해진 것이아니라. 제시된 방법론을 바탕으로, 누구나 구현할수 있다. 이전 내용중 잘 등장하는 auto_ptr STL도 스마트 포인터의 아이디어를 구현한 것이다.

C++의 built-in 포인터로서(다시 말해 dumb(가상정도로 해석, 이하 "더미"로 표현) 포인터) 스마트 포인터 사용한다. 이럴 경우 앞으로 다를 다음과 같은 경우들의 문제들이 논의 된다.
(작성자주:dumb pointer를 덤 포인터 라고 부르지 않고 더미(dummy) 포인터라고 부르는건 의미 호도지만, 능력을 상실한 가짜 포인터의 의미로 사용한다. 특히 Evangelion에서 더미 라는 표현이 괜찮은 어감이기에 차용했다는 후문이.. )
  • Construction and destruction.(생성과 파괴)
    스마트 포인터는 자동으로 0 or null의 초기화로 미 초기화로 인한 문제를 방지하고, 파괴시에도 built-in 타입의 특성으로 자동으로 파괴 시켜준다.
  • Copying and assignment. (복사와 할당)
    스마트 포인터에서 가장 심각한 제약 사항을 일으키는 것이 복사와 할당 문제 인데(차후 논의된다.) 이에 대한 이유와, 옳바른 방향의 제시를 한다.
  • Dereferencing. (역참조)
    클라이언트는 스마트 포인터가 가진 클래스를 어떤 때 어떻게 참조를 해야 할까? 예를들어서 lazy fetching(Item 17참고)를 스마트 포인터 구현하는 것과 같이 여러 경우에 대한 문제를 생각하자.

스마트 포인터는 built-in 포인터와 같이 강한 형 안정성을 가져야 하기 떄문에 template로 구현된다. 대다수 스마트 포인터의 모습은 다음과 같이 보여진다.:

~cpp 
template<class T>                   // 스마트 포인터를 위한 템플릿
class SmartPtr {                    // 포인터 객체
public:
    SmartPtr(T* realPtr = 0);       // 스마트 포인터를 주어진 더미(dumb) 포인터로 
                                    // 초기화 시키는데, 할당되지 않으면 
                                    // 더미(dumb)포인터를 0(null)로 초기화 시킨다.

    SmartPtr(const SmartPtr& rhs);  // 스마트 포인터 복사 생성자

    ~SmartPtr();                    // 스마트 포인터의 파괴자
                                    

    // 스마트 포인터의 값을 할당한다.
    SmartPtr& operator=(const SmartPtr& rhs);

    T* operator->() const;          // 참조 풀기:자료에 접근하도록 포인터를 제공한다.

    T& operator*() const;           // 참조 풀기:마찬가지
 
private:
    T *pointee;                     // 현재 스마트 포인터가 가리키고 있는 것
                                    // 이것이 직접 접근 못하는 더미(dumb) 포인터 이다.
};                                    
현재 복사 생성자와(copy constructor), 할당 연산자(assignment operator)가 특별히 선언되어 있지 않으므로, 둘다 공역(public)인자이다. 문제는 함부로 이를 사용할 경우 가리키는 더미(dumb)포인터가 같아 지므로 built-in 형으로 생성되는 스마트 포인터 둘이 사라지는 영역을 벗어나는 시점에 같은 객체를 delete 하려고 할수 있다. 이에 대한 보완이 필요하다.

그리고, 스마트 포인터를 자세히 다루기에 앞서서, 스마트 포인터가 작성되는 하나의 가상 시나리오를 작성한다. 이유는 점차 스마트 포인터에 관한 자세한 설명이 필요할때, 이 가상 시나리오상에서 발생하거나, 해당 문법에서 감안해야 할 요인들을 논하기 위해 시나리오의 전체 설계를 제시한다.

설정될 시나리오는 분산 시스템상, 분산 DB에서, local 에 있는 객체와, remote에 있는 객체를 다루기 이다. 클라이언트 입장에서는 둘을 다루는 방법이 이원화 되어 있어서, 해당 객체의 프로시저 호출에 일관성이 없을 경우 프로그래밍 환경이 불편하고, 명시성이 떨어지는 등 여러 불리한 점이 있다. 그래서 아예 양쪽의 객체에 사용법에 대한 일관성을 가지고 싶어 한다. 이를 해결 하기 위해서 스마트 포인터로, 해당 객체를 둘러싸서, 프로시저의 일관된 사용 방법을 제공 받고자 한다. 다음은 그것을 구현한 코드들이다.:

~cpp 
template<class T>                   // 분산 DB상에 객체를 가리키는 스마트 포인터
class DBPtr {                       // 템플릿
public:                              

    DBPtr(T *realPtr = 0);          // DB상의 DB객체를 가리키는 더미 포인터를 바타응로
                                    // 스마트 포인터를 생성한다.

    DBPtr(DataBaseID id);           // DB 객체에서 DB ID를 바탕으로 스마트 포인터를
                                    // 생성한다.
    ...                             // DB를 위한 다른 작업들
                                    
};                                   

class Tuple {                       // 데이터 페이스의 tuple를 위한 클래스
public:
    ...
    void displayEditDialog();       // tuple의 수정을 하기 위한 유저의 
                                    // 그래픽 인터페이스(수정이 허락되는 한에서)

    bool isValid() const;           // *this 반환의 유효성 점검
};                                  

// T객체가 수정되어 해당 log를 남기는 역할을 하는 클래스 템플릿
template<class T>
class LogEntry {
public:
    LogEntry(const T& objectToBeModified);
    ~LogEntry();
};

void editTuple(DBPtr<Tuple>& pt)
{
    LogEntry<Tuple> entry(*pt);     // 수정을 위한 해당 log 엔트리를 작성한다.
                                    // 자세한 설명은 아래

    // 유효한 값이 주어지기 까지 수정 dialog를 뛰우기 위한 요구를 계속한다.
    do {
        pt->displayEditDialog();
    } while (pt->isValid() == false);
}
editTuple의 내부에서 수정되어지는 tuple은 아마도 원격(remote)지의 기계에 존재하는 객체이다. 하지만, editTuple을 사용하는 프로그래머는 이러한 사항에 대하여 스마트 포인터 때문에 특별히 상관할 필요가 없다.

editTuple내에 LogEntry객체를 생각해 보자. 수정을 위한 log를 남기기위해서는 displayEditDialog의 시작과 끝에서 매번 불러주면 되는데, 구지 왜 구지 이렇게 했을까? 이에관한 내용은 예외에 관련된 상황 때문인데, Item 9를 참고하면 된다.

당신이 볼수 있는 것처럼 스마트 포인터는 더미(dumb)포인터를 그냥 사용하는 것과 크게 차이점은 없어 보인다. 그것은 캡슐화에 대한 효율성을 논하는 것과 같다. 스마트 포인터를 사용하는 클라이언트는 그냥 더미(dumb)포인터를 사용하는 것처럼 사용하므로서, 스마트 포인터가 어떤 일을 행하는지 특별히 관심을 쏟을 필요가 없게 만드는 것이 관건이다.

1.4.1. Construction, Assignment and Destruction of Smart Pointers : 스마트 포인터의 생성, 할당, 파괴

  • 생성자:Constructor 관련
스마트 포인터의 생성은 특별히 신경 쓸것이 없다. 초기화 되지 않았따면 스마트 포인터의 내부의 있는 더미(dumb)포인터는 null로 세팅 되므로, 차후 파괴시에도 별 문제 될것이 없다.

  • 복사 생성자(copy Constructor), 할당 연산자 (assignment operator) 관련
스마트 포인터의 적용에서 문제시 되는 것이 복사 생성자(copy constructor), 할당 연산자(assignment operator), 파괴자(destuctor)이다. 이들에서 주요한 논점이 되는것은 ownership 즉 소유권의 문제이다. 소유권에 관한 문제는 이 아이템 전반에서 다루는 주제이고, 해결법과 그에 대한 결점이 반복되는 식으로 진행 한다.

auto_ptr 템플릿에 관해서 생각해 보자. Item 9에서 설명한 것과 같이 auto_ptr은 heap-based 객체를 auto_ptr이 파괴 될 때까지 가리키는 역할을 한다. 만약 auto_ptr이 파괴되어지는 경우(존재 지역을 벗어날때) 가리키는 객체는 파괴되어진다. auto_ptr은 다음과 같이 구현되어 있다.

~cpp 
template<class T>
class auto_ptr {
public:
    auto_ptr(T *ptr = 0): pointee(ptr) {}
    ~auto_ptr() { delete pointee; }         // 가리키는 객체를 파괴한다.
    ...

private:
    T *pointee;
};
이렇게 구현되어 있는 상황에서, 만약 auto_ptr을 복사(copy)하거나 할당(assign)하면 어떨게 될까? 다음과 같이 말이다.

~cpp 
auto_ptr<TreeNode> ptn1(new TreeNode);

auto_ptr<TreeNode> ptn2 = ptn1;      // 복사 생성자를 호출했다. 어떤 일이 일어날까?

auto_ptr<TreeNode> ptn3;

ptn3 = ptn2;                         // operator=를 호출했다. 어떤 일이 일어날까?
이 둘(copy, assign)의 의미가, 만약 내부의 더미(dumb) 포인터를 복사 하였다면, 해당 두개의 auto_ptr은 같은 객체를 가리키고 있을 것이다. 그렇다면, 차후에 auto_ptr이 파괴되는 시점에서 하나의 객체를 연속으로 두번 파괴하려고 시도 할것이다. 이것은 잘못 된것이다.

이렇게 제기되는 문제의 대안으로 가리키고 있는 객체에 대하여 복사를 수행한뒤에 가리키도록 만들수 있다. 하지만 이런 구현 상태는 자칫, 많은 copy, assign의 남발시에 new와 delete의 다량의 호출로, 속도 저하와, "같은 것을 같은 것을 가리키고 있다." 라는 의미로 쓴 프로그래머에게 의미를 호도 시켜버릴수 있다.

이런 문제는 auto_ptr이 만약 복사(copying)와 할당(assignment)를 하지 않는다면, 근본적으로 제거 될수 있다. 하지만 auto_ptr 클래스를 간단히 조작해서 문제를 피해본다. 위에서 언급한것과 같이 겉으로는 복사이지만, 내부적으로는 소유권(ownership)를 넘기는 작업을 하도록 구성하는 것이다. 코드는 다음과 같다.

~cpp 
template<class T>
class auto_ptr {
public:
    ...
    auto_ptr(auto_ptr<T>& rhs);         // 복사 생성자
    auto_ptr<T>&  operator=(auto_ptr<T>& rhs);       // 할당(assignment) 연산자
    ...
};

template<class T>
auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs)
{
    pointee = rhs.pointee;              // 소유권(ownership)을 이양하는 작업
    rhs.pointee = 0;                    // rhs의 소유권(ownership)을 박탈한다.
}

template<class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs)
{
    if (this == &rhs)                   // 같은 객체를 가리키는건 1=1 같은 경우밖에 
                                        // 없다. 그러므로 아무런 작업을 하지 않는다.

    delete pointee;                     // 해당 더미(dumb) 포인터가 가리키는 객체를
                                        // 삭제한다.

    pointee = rhs.pointee;              // 소유권(ownership)을 이양한다.
    rhs.pointee = 0;                    // rhs의 소유권(ownership)을 박탈한다.

    return *this;
}
이렇게 복사 생성자(copy constructor)가 불리거나 할당 연산자(assignment operator)를 호출할 경우 소유권(ownership)을 이양한다. 만약 이런 작업이 실패하면, 결코 객체는 지워질수가 없다. 하지만 이런 방식은 치명적인 결과를 불러 올수 있다.

객체의 소유권(ownership)이 auto_ptr의 복사 생성자에 의하여 이양되므로, 만약 auto_ptr을 값에 의한 전달(by value)로 넘겨 버리면 돌아올수 없는 강을 건너는 꼴이 된다.

~cpp 
// 값에 의한 전달(by value)로 auto_ptr을 넘긴다.
void printTreeNode(ostream& s, auto_ptr<TreeNode> p)
{ s << *p; }

int main()
{
    auto_ptr<TreeNode> ptn(new TreeNode);
    ...
    printTreeNode(cout, ptn);          // 값에 의한 전달(pass auto_ptr by value)
    ...
}
printTreeNode에게 auto_ptr<TreeNode> 형인 ptn이 값에 의한 전달(by-value)로 전달 되므로, printTreeNode가 수행되기전에 임시 객체가 생성되고 거기에 ptn이 복제 된다. 이때 불리는 복사 생성자(copy constructor)는 가지고 있는 더미(dumb) 포인터를 넘기는 소유권(ownership) 이양 작업을 하게되고, printerTreeNode가 마친뒤에 해당 객체는 삭제되고, ptn은 0(null)로 초기화 되어 진다. auto_ptr을 잃어 버린 것이다.

그렇다면 값으로의 전달이 아닌 다른 방법이라면? 상수 참조 전달(Pass-by-reference-to-const)로 한다면 되지 않을까? 구현 코드를 보자

~cpp 
// 직관적인 예제이다. auto_ptr<TreeNode>를 상수 참조 전달(Pass-by-reference-to-const)하였다.
void printTreeNode(ostream& s, const auto_ptr<TreeNode>& p)
{ s << *p; }
해당 예제의 상수 참조 전달(Pass-by-reference-to-const)의 인자 p는 객체가 아니다. 그것은 그냥 참조일 뿐이다. 그래서 아무런 생성자가 불리지 않는다. 그래서 소유권(ownership)은 넘긴 인자에 그대로 남아 있으며 값으로의 전달(by-value)시에 일어나는 문제를 막을수 있다.

소유권(ownership)을 전달하는 개념을 다룬건 참 흥미롭지만, 사용자는 결코 재미있지 않다고 생각된다. 이는 복사(copy)나 할당(assign)의 원래의 의미를 손상 시키는 면이라서, 프로그래머는 혼란스러울 뿐이다. 개다가 함수들이 모두 상수 참조 전달(Pass-by-reference-to-const)로 작성된다는 법도 없다. 즉, 쓰지 말라는 소리다.

auto_ptr에 관련한 내용은 이 책의 후반에 다루어 있다.(정확히 291-294쪽) 이번장 여기에 구현된 코드들이 auto_ptr의 100% 코드가 아니므로, STL의 auto_ptr을 사용하기 위해서는 꼭 보기를 바란다. 위에 사실 밑말고 말이다.

  • 파괴자(destuctor)관련
그리고 대게의 스마트 포인터의 파괴자는 다음과 같이 간단히 구현되어 있다.

~cpp 
template<class T>
SmartPtr<T>::~SmartPtr()
{
    if (*this 가 가지고 있는 *pointee) {
        delete pointee;
    }
}
  • 맺음말:차후에 스마트 포인터는 Reference Counting(Item 29)을 구현하는데 적용 된다. 그때 다시 스마트 포인터에 대한 쓰임에 대하여 느껴 보기를 바란다.

1.4.2. Implementing the Dereferencing Operators : 역참조(Dereferencing) 연산자의 적용

  • 작성자주:Dereference라는 표현은 스마트 포인터를 사용시에 내부에 가지고 있는 더미(dumb) 포인터를 통해서 해당 객체에 접근하거나, 그 객체의 참조를 통해서 접근하는 과정을 의미한다. 이것을 Reference의 반대 의미로 Dereference로 쓴다. 사전에는 물론 없는 의미이다. 앞으로 여기에서도 역참조라고 표현한다.

  • 역참조(Dereference)를 하는 연산자는 operator*와 operator-> 이다. 개념적인 구성을 보자.

    ~cpp 
    template<class T>
    T& SmartPtr<T>::operator*() const
    {
        "스마트 포인터"(똑똑한 포인터 기능?)를 수행한다.;
        return *pointee;
    }
    
    처음 해당 함수는 초기화 하거나 pointee가 유효하도록 만든다. 예를 들어서 lazy fetching이라면(Item 17 참고) 해당 함수 내에서 pointee에 해당하는 객체를 만드는 과정을 치를 것이다.

    이 함수의 반환 형은 참조(reference)이다. 객체를 반환하면 어떻게 되는가? 난리도 아닐것이다. 객체의 반환은 객체가 복사로 전달시에 생성, 삭제에서의 비효율은 물론, Item 13에 제시되는 slicing을 발생할 가능성이 있다. 또 Item 13에서도 계속 다룬 가상 함수를 호출할때 역시 객체를 통한 반환은 문제를 발생시킨다. Reference로 전달해라, 이것의 효율성에 관해서는 이미 다루었기에 더 말하지 않는다.

    한가지 더 생각해 보면 더미(dumb) 포인터인 pointee가 null값인 경우에 참조는 어떻게 될까? 안심해라. 이상황은 C++의 설계 단계에서 이미 원하지 않는 것이라 가정 했고, 예외를 발생 시키도록 조치해 두었다. abort(중지)를 원하는가? 그렇게 된다.

    역참조(dereference)의 다른 연산자인 operator->를 생각해 보자. operator*과 비슷한 역할이지만 이것은 더미(dumb) 포인터 그 자체를 반환한다. 앞의 예제중, operator->가 등장한 원격 DB 접근에 관한 예제로 생각해 본다.

    ~cpp 
    {
      LogEntry<Tuple> entry(*pt);
    
      do {
        pt->displayEditDialog();
      } while (pt->isValid() == false);
    }
    
    그러니까. 부분의 구문이

    ~cpp 
    pt->displayEditDialog();
    
    컴파일시 이런 의미로 교체되는 것이라고 생각하면 된다.

    ~cpp 
    (pt.operator->())->displayEditDialog();
    
    생각해 보면 operator->의 의미는 두가지를 가질수 있다. 스마트 포인터 객체 자체의 포인터를 지칭하는 다소 보통의 의미로서 포인터와, 스마트 포인터 객체가 가지고 있는 더미(dumb) 포인터가 그것인데, 스마트 포인터의 사용 용도상 사용자는 더미(dumb)포인터를 반환 받기를 원할 것이다. 그래서 operator->는 다음과 같이 구현되어 있다.

    ~cpp 
    template<class T>
    T* SmartPtr<T>::operator->() const
    {
      "스마트 포인터"(똑똑한 포인터 기능?)를 수행한다.;
    
      return pointee;
    }
    
    해당함수는 올바르게 전달되고, 더미(dumb) 포인터가 가리키는 객체의 가상 함수들도 올바르게 링크 될것이다.

    많은 어플리 케이션에서 스마트 포인터와 같은 기법을 사용하므로 잘 알아 두기를 바란다. 특히나 계속 홍보해서 지겹겠지만 Reference count쪽에서 나온다니까. 주목하기를.

    1.4.3. Testing Smart Pointers for Nullness : 스마트 포인터의 더미(dumb)상의 null 여부를 알고 싶어!

    우리는 스마트 포인터 사용에서 필요한 것들인 생성(create), 파괴(destroy), 복사(copy), 할당(assign), 역참조(dereference)에 관해서 논의했다. 그렇지만 우리가 아직 안한것이 있다. 바로 스마트 포언터의 null여부를 알고 싶은것 바로 그것이다. 지금까지의 지식까지 스마트 포인터를 구현했다고 하고, 다음의 코드를 시전(?) 하고자 할때

    ~cpp 
    SmartPtr<TreeNode> ptn;
    
    ...
    if (ptn == 0) ...                    // 에러!
    if (ptn) ...                         // 에러!
    if (!ptn) ...                        // 에러!
    
    이것 참 무서운 일이지 않은가?

    isNull 함수를 추가하면 쉽게 해결 될것 같다. 하지만 이것은 스마트 포인터가 더미(dumb) 포인터와 같이 쓴다는 개념에 반하는 것이다. 그래서 암시적(implicit) 형변환으로 해결해 본다.


    ~cpp 
    template<class T>
    class SmartPtr {
    public:
        ...
        operator void*();               // 스마트 포인터가 null이면 0를 반환하고returns 0 if the smart
        ...                             // 아니면 0가 아닌 값을 반환한다.
    };                                   
    
    SmartPtr<TreeNode> ptn;
    
    ...
    
    if (ptn == 0) ...                   // 올바르다.
    if (ptn) ...                        // 역시나
    if (!ptn) ...                       // 좋왔어! ^^
    
    이것은 iostream 클래스들이 제공하는 형변환과 비슷하다. 코드는 다음과 같다.

    ~cpp 
    ifstream inputFile("datafile.dat");
    
    if (inputFile) ...                  // inputFile이 잘 열렸는가에 관해서
                                        // 확인해 본다.
    
    모든 형 변환 함수와 마찬가지로 이것도 역시 결점이 있는데, 스마트 포인터간의 비교에서 문제가 발생된다. 다음 예제를 보자.

    ~cpp 
    SmartPtr<Apple> pa;
    SmartPtr<Orange> po;
    ...
    if (pa == po) ...                   // 이게 컴파일이 된단 말이다!
    
    pa와 po는 같은 것이 아니지만, 암시적(implicit) 형변환에 의해서 아주 잘 참으로 컴파일 된다. 이것은 대다수 암시적(implicit) 형변환의 문제점이다.(아직도 난감하면 Item 5를 다시 보자)

    void*의 형변환의(conversion-to-void*) 모호성을 피하기 위해 const void* 형변환이나, 다른 사람들은 bool 형변환을 권한다. 하지만 이런 방법도 형변환들을 섞어 쓰면서 비교하면서(mixed-type comparisons) 발생하는 문제 역시 해결할수 없다.

    한 발작 물러서서, 형변환이 아닌 operator! 경우를 생각해 보자. operator!를 overload시켜 스마트 포인터가 null인경우 true를 반환한다고 해보자.

    ~cpp 
    template<class T>
    class SmartPtr {
    public:
        ...
        bool operator!() const;            // 스마트 포인터가 null일때 returns true if and only
        ...                                // if the smart ptr is null
    };
    
    클라이언트들은 이렇게 적용할 것이다.

    {{{~c
    Valid XHTML 1.0! Valid CSS! powered by MoniWiki
    last modified 2021-02-07 05:23:49
    Processing time 0.2160 sec