스토리텔링 개발자

[More Effective C++] 4. 불필요한 기본 생성자 미제공하기 본문

Effective C++/More Effective C++

[More Effective C++] 4. 불필요한 기본 생성자 미제공하기

김디트 2024. 8. 4. 09:10
728x90

항목 4 : 쓸데 없는 기본 생성자는 그냥 두지 말자

 

 

 

기본 생성자
  • 아무런 인자도 받지 않고 호출될 수 있는 생성자
  • 외부 정보를 하나도 받지 않고 초기화를 한다는 의미이다.
  • 하지만, 외부 정보 없이는 완전한 초기화를 수행할 수 없는 경우도 있다.
    • 예를 들면 주소록의 입력 자료, 회사에서 관리하는 회사 장비의 ID 번호 등.
    • 그렇다면 이 경우엔 기본 생성자를 제공하지 않으면 될 것이나...

 

 

 

기본 생성자가 없는 클래스 제작 시 유의사항 세 가지
// 아래 예제를 통해서 유의사항을 알아본다.
// 회사 장비를 나타내는 클래스
class EquipmentPiece
{
public:
    EquipmentPiece(int IDNumber);
    ...
}

 

 

 

1. 배열을 생성할 때
  • 일반적으로 배열의 요소로 들어가는 객체에 대해서는 생성자 매개변수를 지정할 수 없다.
  • 그래서 EquipmentPiece 객체의 배열을 생성하기란 불가능하다.
// EquipmentPiece의 생성자를 호출할 수 없다!
EquipmentPiece bestPieces[10]; // 컴파일 에러!
EquipmentPiece* bestPieces = new EquipmentPiece[10]; // 컴파일 에러!!
  • 제약을 피해가는 세 가지 방법

1. 배열이 정의된 위치에서 매개변수를 직접 넣어준다.

  • 배열을 힙에 만들지 않을 때에만 가능한 방법이다.
int ID1, ID2, ID3, ..., ID10;
...
EquipmentPiece bestPieces[] = {
    EquipmentPiece(ID1),
    EquipmentPiece(ID2),
    EquipmentPiece(ID3),
    ...,
    EquipmentPiece(ID10)
};

 

2. 포인터의 배열을 사용한다.

  • 하지만, 배열 내의 모든 포인터가 가리키는 객체를 삭제해야 한다는 것을 잊으면 안된다.
  • 포인터 공간에 대한 메모리가 추가로 필요하므로, 결과적으로는 필요한 메모리 사용량이 늘어난다.
typedef EquipmentPiece* PEP;

PEP bestPieces[10]; // 포인터의 배열이므로 생성자가 호출되지 않는다.
PEP* bestPieces = new PEP[10]; // 동일하다.

// 배열에 들어있는 각 포인터들은 다른 EquipmentPiece 객체를 가리킬 수 있다.
for(int i = 0 ; i < 10; ++i)
{
    bestPieces[i] = new EquipmentPiece(ID Number);
}

 

3. 포인터 배열에서 메모리 공간을 줄인 방법

  • 배열에 대해 직접 비가공(raw) 메모리를 할당하고
  • 위치지정 new(항목 8 참조)를 써서 그 메모리 안에 EuipqmentPiece 객체가 생성되기 한다.
// 비가공 메모리 생성
void* rawMemory = operator new[](10*sizeof(EquipmentPiece));

// bestPieces가 EquipmentPiece 배열 첫 요소의 주소가 되도록 셋팅
EquipmentPiece* bestPieces = static_cast<EquipmentPiece*>(rawMemory);

// 위치지정 new를 써서 해당 메모리 안에 EquipmentPiece 객체 10개를 생성한다.
for(int i = 0 ; i < 10 ; ++i)
{
    new (bestPieces + i) EquipmentPiece(ID Number);
}
  • 해당 코드의 단점
    • EquipmentPiece 객체에 생성자 매개변수를 넘겨주어야 하는 것은 여전하다.
    • 대부분의 프로그래머가 위치지정 new를 사용하는 방법에 익숙하지 않다.(유지보수의 문제)
    • 객체를 삭제할 때 각 객체의 소멸자와 비가공 메모리의 operator delete[]를 손으로 직접 호출해야 한다.
for (int i = 9 ; i >= 0 ; --i)
{
	// 소멸자를 직접! 호출해야 한다.
    bestPieces[i].~EquipmentPiece();
}
// 비가공 메모리 해제
operator delete[](rawMemory);

 

 

 

2. 많은 템플릿 기반 컨테이너 클래스의 타입 매개변수로 사용할 수 없다.
  • 대부분의 컨테이너 클래스는 인스턴스화 하기 위해 매개변수로 들어간 타입에게 기본 생성자를 요구한다.
    • 참고) C++은 클래스나 구조체 외에, 원시 타입(Int, double)에 대해서도 기본 생성자를 제공한다.
// Array 클래스에 대한 템플릿은 아마 이런 구성일 것이다.
template<typename T>
class Array
{
public:
    Array(int size);
    ...
private:
    T* data;
};

template<typename T>
Array<T>::Array(int size)
{
    data = new T[size]; // 배열을 생성한다. 기본 생성자가 필수로 필요하다!
    ...
}

 

 

 

3. 가상 기본 클래스(virtual base class)에 기본 생성자가 없으면 사용하기 힘들다.
  • 가상 기본 클래스의 생성자 매개변수를 생성되는 객체의 파생 클래스 쪽에서 제공해야 하기 때문이다.
  • 기본 생성자가 없는 가상 기본 클래스를 상속받는다면?
    • 파생 클래스 쪽에서 기본 클래스의 생성자 매개변수의 리스트와 각 매개변수의 의미를 알고 직접 제공해야 한다.
    • 이는 굉장히 불편한 일이다..

 

 

 

모든 클래스에 기본 생성자를 넣는다?
  • 클래스 사용 제약을 피하기 위해서 모든 클래스에 기본 생성자를 우겨넣을 수 있을 것이다.
// 기본 생성자를 제공하는 버전
class EquipmentPIece
{
public:
    EquipmentPiece(int IDNumber = UNSPECIFIED);
    ...
private:
    static const int UNSPECIFIED;
};

EquipmentPiece e; // 컴파일 성공
  • 문제점 
    • 멤버 데이터가 제대로 초기화 되었는지 보장할 수 없으므로 다른 멤버 함수가 복잡해진다. 
      • 불필요한 검사 코드들이 들어가게 된다.
    • 클래스 효율이 나빠진다.
      • 초기화 검사 시간만큼 실행 속도가 떨어질 것이다.
      • 초기화 검사 코드만큼 실행파일 / 라이브러리의 크기 커질 것이다.
      • 또한 초기화 검사가 실패했을 때에 대한 처리 코드도 생각해야 한다.

 

 

 

결론
  • 쓸데없는 상황에서는 기본 생성자를 피하자.
  • 클래스 사용 제약을 받긴 하겠지만, 그 부분은 충분히 주의하면 괜찮을 것이다.

 

 

 

참조
 

[Effective C++] 6. 암시적으로 생성되는 함수 금지하기

항목 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해버리자.   암시적 생성 함수의 문제컴파일러가 생성하는 함수는 모두 public 멤버가 된다.이처럼 의도하지 않은 인

delightlane.tistory.com

 

728x90
Comments