성공할 게임개발자

[Effective c++] 19. 클래스 설계는 타입 설계와 똑같이 취급하자 본문

C++

[Effective c++] 19. 클래스 설계는 타입 설계와 똑같이 취급하자

fn000 2025. 6. 2. 09:40

c++ 개발자는 클래스 설계자이자 타입type) 설계자이다. 함수와 연산자 오버로딩, 메모리 할당 및 해제, 객체 초기화 및 종료처리 정의 등등 강력한 기능들을 컨트롤한다.

 

좋은 타입이란 문법이 자연스럽고 의미구조가 직관적이며 효율적 구현이 한 가지 이상 가능해야한다. 

 

아래는 효과적인 클래스 설계시 고려사항이다.

 

 

 

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

이 부분에 따라 클래스 생성자 및 소멸자의 설계가 바뀐다. 또 메모리 할당 함수(new, new[], delete, delete[])를 직접 작성할 경우 영향을 미친다.

 

 

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

생성자와 대입 연산자의 동작 및 둘 사이의 차이점을 결정짓는 요소이다. 초기화와 대입을 햇갈리지 않는 것이 가장 중요하다. 각각 해당되는 함수 호출이 다르기 때문. 항목4

 

 

 

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

어떤 타입에 대해 "값에 의한 전달"을 구현하는 쪽은 복사생성자이다.

예를들어,

void foo(Vector v);  // <- 여기서 복사 생성자 호출됨 (값으로 전달됨)

Vector bar() {
    Vector v;
    return v;  // <- 여기서 복사 생성자 또는 이동 생성자 호출됨 (값으로 반환됨)
}

 

 이런식으로 값에 의한 전달이 수행될 때 복사생성자가 호출된다는 소리이다.

 

 

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

클래스 데이터 멤버의 몇 가지 조합 값만큼은 반드시 유효해야한다. 이것을 클래스의 불변속성(invariant)라고 한다. 클래스 차원에서 지켜줘야하고 생성자, 대입연산자, 쓰기함수는 불변속성에 많이 좌우된다.

예를 들면, Rectangle 클래스

규칙(불변속성) : width, height는 항상 0보다 커야한다.

#include <iostream>
#include <stdexcept>

class Rectangle {
public:
    Rectangle(int w, int h) {
        setSize(w, h);  // 생성자에서도 불변성 검사
    }

    void setSize(int w, int h) {
        if (w <= 0 || h <= 0)
            throw std::invalid_argument("가로, 세로는 0보다 커야 합니다.");

        width = w;
        height = h;
    }

    int getWidth() const { return width; }
    int getHeight() const { return height; }
    int area() const { return width * height; }

private:
    int width;
    int height;
};
int main() {
    try {
        Rectangle r(10, 5);
        std::cout << "면적: " << r.area() << std::endl;

        r.setSize(3, 0);  // ❌ 예외 발생: 높이가 0
    }
    catch (const std::exception& e) {
        std::cout << "오류: " << e.what() << std::endl;
    }

    return 0;
}

이런식으로 반드시 유효해야하는 데이터 멤버가 불변속성을 위반했기 때문에 클래스차원에서 방지해준다.

 

 

5. 기존의 클래스 상속 계통망(inheritance graph)에 맞출 것인가?

기존 클래스에서 상속시킨다고 하면 설계는 기본 클래스에 의해 제약을 받게 된다. 특히 멤버 함수가 가상인가 비가상인가의 여부가 가장 큰 요인이다. 설계한 클래스를 기본클래스로 쓰기로 결정했다면 멤버함수의 가상 함수 여부가 결정된다. 특히 소멸자가 그렇다. 항목7

 

 

6. 어떤 종류의 타입 변환을 허용할 것인가?

어떤 타입과 다른 타입 사이에 변환은 필수적이다. T1 타입의 객체를 T2 타입의 객체를 암시적으로(implicitly) 변환되도록 만들고 싶다면, T1클래스에 타입 변환 함수를 넣어두던가 인자 한개로 호출될 수 있는 비명시호출 생성자를 T2클래스에 넣어두어야한다. 명시적(explicit) 타입변환만 허용하고 싶으면 해당 변환을 맡는 별도 이름의 함수를 만들되 타입 변환 연산자 혹은 비명시호출 생성자는 만들면 안된다. 항목15 

 

기본문법

class ClassName {
public:
//operator뒤에 반환하고자하는 타입이름을 붙인다
    //반환타입은 없다
    operator TargetType() const;
};

 

 

 

예제1 Length 클래스 -> double변환

class Length {
public:
    explicit Length(double meters) : m_meters(meters) {}

    operator double() const { return m_meters; }  // 암시적 변환

private:
    double m_meters;
};
Length len(3.5);
double d = len;        // ✅ operator double() 호출됨

 

 

예제2 explicit 키워드로 암시적 변환 막기

class Length {
public:
    explicit operator double() const { return m_meters; }

private:
    double m_meters;
};
Length len(3.5);
double d = static_cast<double>(len);  // ✅ 명시적 변환만 허용됨
double x = len;  // ❌ 컴파일 오류 (암시적 변환 금지)

 

반환형이 없고 함수이름이 곧 반환형이다.

 

 

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

클래스 안에 선언할 함수가 여기서 결정된다. 어떤 것들은 멤버 함수로 적당할 것이고 몇몇은 그렇지 않을 것이다. 항목23, 항목24, 항목46

 

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

private로 선언해야하는 함수가 여기에 해당된다. 항목6

 

 

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

어떤 클래스 멤버를 public, protected, private 영역에 결정에 도움을 줄 질문이다. 프렌드로 만들어야 할 클래스 및 함수 클래스 중첩에 대한 결정도 포함된다.

 

10. "선언되지 않은 인터페이스"로 무엇을 둘 것인가?

내가 만든 타입이 제공할 보장이 무엇인가로 보장할 수 있는 부분은 수행 성능 및 예외 안전성(항목29) 그리고 자원 사용 이다. 이렇게 보장하는 것은 클래스 구현에 있어서 제약으로 작용하게 된다.

 

 

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

타입하나가 아니라 동일 계열의 타입군 일 수도 있다. 만약 그렇다면 새로운 클래스가 아니라 클래스 템플릿을 정의해야 한다.

 

12. 정말로 꼭 필요한 타입인가?

기존의 클래스에 대해 기능 몇개가 아쉬워 파생클래스를 새로 뽑고 있다면, 차라리 간단하게 비멤버 함수라던지 템플릿을 몇개 더 정의하는게 낫다.

 

 

중요포인트!

클래스 설계는 타입 설계이다. 새로운 타입을 정의하기 전에 이번 항목의 고려사항을 점검해보자