[Advanced C++] 39. Nested Type, destructor, 멤버 함수를 가지는 클래스 템플릿, injected class name, member function template

dev.kelvin·2025년 4월 15일
1

Advanced C++

목록 보기
39/74
post-thumbnail

1. Nested Type

멤버 타입 (중첩 타입)

다음과 같은 코드가 있다고 가정해보자

    enum class PlayerStat
    {
        Attack,
        Die,
        Hurt
    };

    class Archer
    {
    private:
        PlayerStat ps{};
    };

만약 enum class인 PlayerStat이 Archer class와 함께 사용되도록 의도되었다면 위 예시는 class와 enum class가 독립적이기 때문에 둘이 어떻게 연결되는지 추론해야 한다

따라서 멤버타입으로 class 내부에 타입을 정의하는것이 좋다 (단순히 적절한 접근 지정자 밑에 원하는 타입을 정의한다)

    class Archer
    {
    public:
        enum PlayerStatType
        {
            Attack,
            Die,
            Hurt
        };

        Archer(PlayerStatType stat)
            : ps{ stat }
        {

        }

    private:
        PlayerStatType ps{};
    };

    int main()
    {
        Archer Ac{ Archer::Attack };

        return 0;
    }

이렇게 클래스와 독립적이지 않게 내부에 enum을 정의하였다

멤버 타입은 class내부에서 사용되기 전에 완전히 정의되어야 하기 때문에 클래스의 맨 위에 정의하는게 좋다

위에서 PlayerStatType은 public: 접근 지정자에 선언되었기 때문에 외부에서 직접 접근이 가능하다, 따라서 Archer::Attack으로 접근이 가능한 것이다

class 내부에서 해당 enum에 접근할때는 범위지정자 ::없이 그냥 enum 열거자에 접근하면 된다 (같은 범위인 class에 있기 때문)

	PlayerStatType::Attack; //class내부에서는 이렇게 할 필요없이
    
    Attack; //이렇게 바로 접근 가능

enum class를 사용하다가 class 내부에 정의할때는 enum을 사용했는데, 이는 class 자체가 유효 범위 역할을 하기때문에 enum class를 사용하는 목적이 중복된다 (만약 enum class를 사용했다면 Archer::PlayerStatType::Attack 이런식으로 접근해야 한다)

    class Archer
    {
    public:
        enum class PlayerStat
        {
            Attack,
            Die,
            Hurt
        };

        Archer(PlayerStat stat)
            : ps{ stat }
        {

        }

    private:
        PlayerStat ps{};
    };

    int main()
    {
        Archer Ac{ Archer::PlayerStat::Attack };

        return 0;
    }

클래스는 using이나 typedef와 같은 type aliases를 가질 수 있다

	class Archer
    {
    public:
        using ArcherHp = int;

        ArcherHp GetHp() { return hp; }

    private:
        ArcherHp hp{};
    };

    int main()
    {
        Archer Ac{};
        Archer::ArcherHp AcHp = Ac.GetHp();

        return 0;
    }

단 여기서도 마찬가지로 같은 class 내부에서는 범위지정연산자:: 없이 사용이 가능하지만 외부에서는 범위지정연산자::를 사용해서 접근해야 한다 (Archer::ArcherHp)

클래스는 아주 드물지만 다른 클래스를 멤버로 가지고 있을 수 있다

    class Player
    {
    public:
        class Archer
        {
        public:
            void Test(const Player& InPlayer)
            {
                InPlayer.PlayerID;
                
                //Archer class의 this로는 Player 클래스의 멤버에 접근이 불가능함
            }

            int GetArcherDamage() { return ArcherDamage; }
        private:
            int ArcherDamage{};
        };

    private:
        int PlayerID{};
    };

    int main()
    {
        Player P1{};

        Player::Archer A1{};
        A1.GetArcherDamage();
        A1.Test(P1);

        return 0;
    }

이때 Archer class에서의 this 포인터로는 Player class의 멤버에 접근이 불가능하다, 따라서 객체를 전달 받아야 한다

이러한 경우는 보통 STL에서 iterator가 이런 방식으로 구현되어 있다
(std::string::iterator는 std::string class의 멤버 클래스로 구현되어 있음)

이러한 멤버 클래스는 class 내부에서 전방선언하고 외부에서 정의가 가능하다

	class Player
    {
    public:
        class Archer;

    private:
        int PlayerID{};
    };

    class Player::Archer
    {
    public:
        void Test(const Player& InPlayer)
        {
            InPlayer.PlayerID;
        }

        int GetArcherDamage() { return ArcherDamage; }
    private:
        int ArcherDamage{};
    };

단 이때 멤버 클래스 전방선언이 있는 outer 클래스가 멤버 클래스 정의보다 위에 선언되어야 한다

	class Player::Archer
    {
    public:
        void Test(const Player& InPlayer)
        {
            InPlayer.PlayerID;
        }

        int GetArcherDamage() { return ArcherDamage; }
    private:
        int ArcherDamage{};
    };

    class Player
    {
    public:
        class Archer;
        
    private:
        int PlayerID{};
    };
    
    //compile error

2. destructor

소멸자

여러개 데이터를 객체가 소멸하기 전에 send하는 기능의 함수를 가진 클래스를 생각해보자

만약 데이터를 추가만 해당 클래스 타입 객체가 소멸되기 전에 send를 하지 않는다면 의도치 않은 동작이 발생하게 된다

물론 객체가 소멸되기 전에 반드시 send를 하자! 라고 기억할 수 있지만 휴먼 에러가 발생하기 쉽다
(또한 다른 경로에 의해 예상보다 일찍 객체가 소멸될 수 있음)

정리하면 객체가 소멸되기 전에 특정한 명령을 수행하고 싶을 수 있는데 이럴때 소멸자를 사용하여 쉽게 처리할 수 있다

생성자는 보통 객체의 멤버 변수를 초기화하고 객체가 사용 준비되도록 하는데 필요한 다른 설정 작업을 수행한다, 반대로 소멸자는 클래스 타입 객체가 소멸되기 전에 필요한 정리 작업을 수행한다

소멸자 이름 규칙

  • 소멸자도 생성자와 마찬가지로 클래스명과 동일해야 하고 앞에 ~가 붙는다
  • 소멸자는 인자를 받을 수 없다
  • 소멸자는 반환형이 없다
  • 클래스는 단 하나의 소멸자만 가질 수 있다
	class Archer
    {
   	public:
    	~Archer();
    };

생성자와 마찬가지로 소멸자를 명시적으로 호출해서는 안된다 (어차피 객체가 소멸될 때 자동으로 호출됨)

소멸자는 객체의 소멸 전에 실행되므로 멤버 함수를 안전하게 호출할 수 있다

컴파일러는 생성자와 마찬가지로 프로그래머가 명시적으로 선언한 소멸자가 없다면 empty body를 가진 암시적 소멸자를 생성하고 호출한다

만약 std::exit()을 사용하면 프로그램이 즉시 종료되고, 이때는 로컬변수 객체의 소멸자가 호출되지 않는다 (소멸자에 의존적인 멤버 함수 호출이 있다면 std::exit() 사용을 주의해야 한다)

처리되지 않은 예외(unhandled exception)또한 프로그램 크래시가 발생할 수 있으며 이때 stack이 풀리지 않으면 (stack unwinding) 소멸자가 호출되지 않을 수 있다
(stack unwinding은 예외가 throw되었을 때 스택을 거슬러 올라가면서 모든 지역객체의 소멸자를 호출하여 리소스를 안전하게 해제하는게 목적이다)


3. 멤버 함수를 가지는 클래스 템플릿

함수 템플릿과 클래스 템플릿을 다시 정리해보면 다음과 같다

	//함수 템플릿
    template <typename T>
    T max(T a, T b)
    {
        return (a < b) ? b : b;
    }

    int main()
    {
        std::cout << max<int>(10, 20) << std::endl;

        return 0;
    }
	//클래스 템플릿
    template <typename T>
    struct Foo
    {
        T A{};
        T B{};
    };

    template <typename T>
    Foo(T, T) -> Foo<T>; //추론 가이드

    int main()
    {
        Foo<int> f1{ 10, 20 };
        Foo<double> f2{ 10.5, 20.5 };

        return 0;
    }

template class 내부에서 정의하는 함수에는 해당 템플릿 타입 적용이 가능하다

    template <typename T>
    class Player
    {
    public:
        Player(const T& InHp, const T& InMp) //template 타입 T 사용 가능, aggregate가 아니기 때문에 생성자를 이용한 초기화 필요
            : hp{InHp}, mp{InMp}
        {

        }

        bool IsEqualPlayer(const Player<T>& InPlayer); //template 타입 T 사용 가능

    private:
        T hp{};
        T mp{};
    };

	//클래스 외부에서 멤버 함수 템플릿 정의 시 template <typename T>를 다시 선언해야 한다 (다시 선언하지 않으면 함수 정의가 클래스 템플릿 정의와 분리되어 있기 때문에 컴파일러가 T가 무엇인지 알 수 없다)
    template <typename T>
    bool Player<T>::IsEqualPlayer(const Player<T>& InPlayer)
    {
        return InPlayer.hp == hp && InPlayer.mp == mp;
    }

    int main()
    {
        Player p1{ 10, 20 }; //CTAD로 Player<int>타입 추론

        p1.IsEqualPlayer(Player{ 30, 40 });

        return 0;
    }	

template type으로 전달해야 할 경우 T가 굉장히 큰 값이 될 수도 있기 때문에 const &로 전달하는게 좋다
(const T& InHp)

만약 멤버 함수 템플릿 정의를 class 내부에서 한다면 template < typename T >를 다시 선언할 필요가 없다, 암시적으로 클래스 템플릿의 type T를 사용하게 된다

위와 같은 non-aggregate 클래스에서 CTAD가 작동하기 위해 추론 가이드는 필요하지 않다, 일치하는 생성자가 컴파일러에게 초기화자로부터 템플릿 타입을 추론하는데 필요한 정보를 제공한다

멤버 함수 템플릿 정의를 외부에서 할 때는 반드시 템플릿 타입 (template < typename T >) 를 다시 선언해주고 클래스이름< T >::함수이름()으로 정의해야 한다

ex) bool Player::IsEqualPlayer() 이게 아니라 bool Player< T >::IsEqualPlayer() 이런식으로

injected class name

생성자의 이름은 클래스의 이름과 반드시 일치해야 한다고 정리했지만 위의 코드에서는 Player< T >의 생성자는 Player< T >가 아닌 Player로 지정했지만 잘 동작한다

클래스의 유효범위 (scope)에서 Player< T >와 같이 정규화가 되지 않은 이름 (Player)들을 injected class name이라고 한다

이러한 injected class name은 정규화된 이름의 약칭 역할을 한다, 결론적으로 Player라고 사용해서 Player < T >로 컴파일러가 처리한다는 것이다

따라서 위의 클래스 외부에서 정의한 멤버 함수 템플릿도 다음과 같이 사용이 가능하다

	template <typename T>
    bool Player<T>::IsEqualPlayer(const Player& InPlayer) //Player로 사용했지만 Player<T>의 injected class name이기 때문에 컴파일러는 Player<T>로 처리한다
    {
        return InPlayer.hp == hp && InPlayer.mp == mp;
    }

클래스 템플릿의 멤버 함수 템플릿을 클래스 정의 외부에서 정의할때는 클래스 정의 바로 아래에 정의하는게 좋다(무조건.h에 정의해야 함) (컴파일러가 클래스 정의를 볼 수 있는곳이라면 바로 아래의 멤버 함수 템플릿의 정의도 볼 수 있기 때문)

이는 만약 .h에 클래스가 정의되었다면 그 클래스의 멤버 함수 템플릿도 헤더파일에 정의해야 함을 의미한다

헤더파일에 정의된 멤버 함수 템플릿은 암시적으로 inline처리가 되고 linker가 중복을 제거할 것이기 때문에 전혀 문제가 되지 않는다

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글