스토리텔링 개발자

[More Effective C++] 26. 클래스 인스턴스 개수 제한 본문

개발/More Effective C++

[More Effective C++] 26. 클래스 인스턴스 개수 제한

김디트 2024. 9. 5. 11:43
728x90

항목 26. 클래스 인스턴스의 개수를 의도대로 제한하는 방법

 

 

 

개수 제한의 예시
  • 프린터. 프린터 객체는 갯수 제한을 할 수밖에 없을 것이다.

 

 

객체 개수를 0개로 제한
  • 클래스의 생성자를 private 선언한다.
class CanBeInstantiated
{
private:
    CanBeInstantiated();
    CanBeInstantiated(const CantBeInstantiated&);
    ...
};

 

 

 

객체 개수를 1개로 제한
  • 객체를 생성자 함수 안에 그냥 넣는다.
class PrintJob;
class Printer
{
public:
    void submitJob(const PrintJob& job);
    void reset();
    void performSelfTest();
    ...
    frient Printer& thePrinter(); // friend 함수이므로 private 함수 접근 가능
prviate:
    // 생성자가 private이므로 생성 불가
    Printer();
    Printer(const Printer& rhs);
    ...
};

Printer& thePrinter()
{
    // 프린터 객체는 하나다.
    // private 함수인 생성자에 접근 가능하므로 생성 성공
    // static이므로 객체 개수가 1개임을 보장하게 된다.
    static Printer p;
    return p;
}
  • 다른 구현 방법을 채택해도 된다.
    1. thePrinter() 함수를 클래스 내부 static 함수로 둔다.
      • class Printer
        {
        public:
            static Printer& thePrinter(); // 내부 정적 함수로
            ...
        private:
            Printer();
            Printer(const Printer&  rhs);
            ...
        }
        
        // 대신 사용 시 코드가 조금 더 길어진다.
        Printer::thePrinter().reset();
    2. Printer와 thePrinter를 전역 유효범위에서 특정 네임스페이스로 옮긴다.
      • 네임스페이스(namespace)는 어떤 타입의 이름 충돌을 막는 상위 개념의 범주이다.
      • 클래스와 매우 흡사하지만, 제한자가 없다. 즉, public 뿐이다.
      • namespace PrintingStuff
        {
            class Printer
            {
            public:
                ...
            friend Printer& thePrinter();
            private:
                Printer();
                Printer(const Printer& rhs);
                ...
            };
            
            Printer& thePrinter()
            {
                static Printer p;
                return p;
            }
        }
        
        // 앞에 네임스페이스를 붙여서 사용한다.
        PrintingStuff::thePrinter().reset();
        
        // 하지만 using 선언으로 축약할 수 있다.
        using PrintingStuff::thePrinter;
        
        thePrinter().reset();
  • 앞의 코드의 미묘한 특징
    • Printer 객체가 클래스 정적 객체가 아니라 함수의 정적 객체로 선언되었다.
      • 클래스에 넣으면 사용하든 하지 않든 생성되지만, 함수에 넣으면 사용할 때에서야 생성된다.
      • 또한 클래스에 넣으면 언제 초기화 될지, 초기화 시점을 특정할 수 없다.
    • 함수 안에 정의된 정적 객체와 인라인 사이의 관계
      • // 이렇게 짧고 간단한 함수인데 왜 인라인 함수로 지정하지 않았을까?
        Printer& thePrinter()
        {
            static Printer p;
            return p;
        }
      • inline의 의미
        • 함수가 호출된 부분 대신 그 함수의 몸체를 끼워넣고 즉시 처리해라.
        • 허나, 비멤버 함수에서는 다른 의미를 가진다.
          • 이 함수는 내부 연결(internal linkage)를 가진다는 뜻.
          • (하지만, 1996년 7월에 인라인 함수 연결 형태를 외부 연결로 변경해서 아래 이슈는 더 이상 문제가 아니다.)
          • 내부 연결을 가진 함수는 한 프로그램 안에서 중복될 수 있다.
          • 즉, 내부 연결을 가진 함수의 코드가 프로그램의 목적 코드 안에 두 개 이상 나타날 수 있다.
          • 결론적으로, 정적 객체가 여러 개 만들어질 수 있다는 뜻이다.

 

 

 

객체 개수를 n개로 제한
  • 생성된 객체의 개수를 직접 세어서, 일정한 개수가 넘어갔을 때 예외를 일으키는 방법
class Printer
{
public:
    class TooManyObjects(); // 너무 많은 객체가 요구될 때 사용할 예외 클래스
    
    Printer();
    ~Printer();
    ...
private:
    static size_t numObjects; // Printer 객체의 개수를 센다.
    Printer(const Printer& rhs); // 복사는 금지
};

size_t Printer::numObjects = 0;

Printer::Printer()
{
    if(numObjects >= n) // n개 이상이 되면 예외 발생
        throw TooManyObjects();
    
    ++numObjects;
}

Printer~Printer()
{
    --numObjects;
}
  • 직관적이고 단순하다.
  • 객체의 개수를 유동적으로 변경할 수 있다.
  • 하지만 직관적이지 않은 생성법들에 의해서 문제가 발생할 수 있다.(상속 / 합성)
// 문제 상황 1 : 상속된 경우
class ColorPrinter : public Printer { ... };

Printer p;
ColorPrinter cp;
// 이 경우 Printer 객체는 2개가 소비된다.
// 1개로 제한되어 있는 경우 예외가 발생할 것이다.

// 문제 상황 2 : 합성된 경우
class CPFMachine
{
private:
    Printer p;
    FaxMachine f;
    CopyMachine c;
    ...
};
CPFMachine m1;
CPFMachine m2;
// 역시 Printer 객체가 2개가 소비된다.
// 1개로 제한되어 있는 경우 역시나 예외가 발생한다.

 

 

 

객체 개수를 n개로 제한 2
  • 생성자가 private로 선언된 클래스는 상속 / 합성에 불가능하다.
  • 생성자를 대신할 함수를 만드는 방법.
class FSA
{
public:
    // 사용하기 힘든 버전
    static FSA* makeFSA();
    static FSA* makeFSA(const FSA& rhs);
    
    // 사용이 편한 버전
    static unique_ptr<FSA> makeFSA();
    static unique_ptr<FSA> makeFSA(const FSA& rhs);
    ...
private:
    FSA();
    FSA(const FSA& rhs);
    ...
}

// new를 호출한다?
// 받는 쪽에서 delete를 해줘야 한다는 문제가 있다.
FSA* FSA::makeFSA() {return new FSA()}
FSA* FSA::makeFSA(const FSA& rhs) {return new FSA(rhs); }

// 스마트 포인터를 리턴해 버리자.
unique_ptr<FSA> FSA::makeFSA() {return unique_ptr<FSA>(new FSA()); }
unique_ptr<FSA> FSA::makeFSA(const FSA& rhs) {return unique_ptr<FSA>(new FSA(rhs)); }

 

 

 

한 개로 제한하되, 런타임 중에 소멸도 시키고 싶다
  • static 변수로 처리한 경우 아래 상황이 불가능하다.
Printer 객체를 생성;
객체 사용;
객체 소멸;

Printer 객체를 재생성;
객체 사용;
객체 소멸;
  • 방금 전의 생성자를 대신할 함수와 카운팅 코드를 모두 합쳐버리면 된다.
class Printer
{
public:
    class TooManyObjects{};
    
    static unique_ptr<Printer> makePrinter();
    ~Printer();
    ...
private:
    static size_t numObjects;
    Printer();
    Printer(const Printer& rhs);
};

size_t Printer::numObjects = 0;

Printer::Printer()
{
    if(numObjects >= 1)
        throw TooManyObjects();
        
    ++numObjects;
}

unique_ptr<Printer> Printer::makePrinter()
{
    return unique_ptr<Printer>(new Printer);
}
  • 1개 제한은, 간단히 일반화 가능하므로 범용적이다.

 

 

 

인스턴스 카운팅(Object-Counting) 기능을 가진 기본 클래스
  • 위의 코드에서 인스턴스 카운팅 하는 코드들을 일반화 해볼 수 있을 것 같다.
  • 인스턴스 카운팅을 상속받을 수 있도록 기본 클래스를 만든다?
    • 하지만 이보다 좀 더 나은 방법이 있을 것 같다.
  • 아래 템플릿 클래스의 인스턴스를 합성하여 사용한다.
template<typename BeingCounted>
class Counted
{
public:
    class TooManyObjects {};
    static size_t objectCount() { return numObjects; }
    
protected:
    Counted() { init(); }
    Counted(const Counted& rhs) { init(); }
    ~Counted() { --numObjects; }

private:
    static size_t numObjects;
    static const size_t maxObjects;
    void init()
    {
        if(numObjects >= maxObjects) throw TooManyObjects();
        ++numObjects;
    }
};
  • 이를 private 상속 받거나, 멤버 변수로 포함시킨다.
// private 상속
class Printer : private Counted<Printer>
{
public:
    ...
    using Counted<Printer>::objectCount; // 이 함수를 외부 사용자가 볼 수 있게 한다.
    using Counted<Printer>::TooManyObjects; // 이 예외 클래스를 외부 사용자가 볼 수 있게 한다.
private:
	...
};
  • 마무리해야 할 부분이 남아있다.
    • Counted 안에 선언되어 있는 정적 상수 멤버의 초기화
// numOjbects를 구현 파일에 정의하면, 자동으로 0으로 초기화횐다.
template<typename BeingCounted>
size_t Counted<BeingCounted>::numObjects;

// maxObjects 초기화는 사용자에게 맡긴다.
const size_t Counted<Printer>::maxObjects = 10;
728x90
Comments