C++/기타

[C++] CRTP 패턴.

sseram 2024. 2. 13. 00:21
반응형

 

CRTP [curiously recurring template pattern]

 

모든 언어에서 사용하는 패턴은 아니다. 오직 C++ 에서만 숙어처럼 사용하는 패턴이다.

template을 이용하여 [본인의 자식 타입] 을 typename 으로 가진다.

 

그렇게 사용하니 이름이 curiosuly recurring template pattern. 즉 요상하게 반복되는 template pattern이 되었나 보다.

 

1. CRTP 형태.

2. 사용하는 이유.

3. 예제.

   3 - 1. 일반적인 방식으로 구현.

   3 - 2. CRTP 패턴으로 구현.

4. C++ 에서 실제 활용하는 코드.

 


 

1. CRTP 형태.

 

말로 풀어 쓰면 아래와 같다.

 

- 기반 클래스를 템플릿으로 만들고
- 파생 클래스 만들 때 자신의 클래스 이름을 기반 클래스의 템플릿 인자로 전달

 

그리고 이것을 코드로 풀어 쓰면

#include <iostream>

// 기반 클래스를 템플릿으로 만든다.
template <typename T>
class Bass{
public:
    void some_fn(){
    	// 여기서 T 타입 fn 사용
    }
};


// 파생 클래스 만들 때 자신의 클래스 이름을 기반 클래스의 템플릿 인자로 전달한다
// -->  Base<derived> 를 상속
class derived : public Bass<derived>{
public:
    void fn() {}
};

 

이렇게 쓸 수 있겠다.

 


 

 

2. 사용하는 이유.

 

가장 큰 이유는 효율적으로 사용할 수 있기 때문이다.

CRTP 패턴으로 사용함으로써

 

- base class에 virtual table이 만들어지지 않는다.

- 이에 따라 가상 함수를 호출하지 않다 보니, runtime 시 바로 인라인 함수를 호출하여 오버헤드가 줄어든다.

 


 

3. 예제.

 

한 번 간단한 코드를 구현하여 crtp 패턴의 장점에 대해 알아보도록 하자.

먼저 base_monster class를 만들 것이다. 이 클래스 안에는 encounter라는 함수가 있고, 이 내부에서는 script를 호출한다.

그 후 앞으로 만들어지는 derived class에서는 각각의 몬스터가 치는 대사들을 따로 구현해 줄 것이다.

 

 

 

3 - 1  일반적인 방식

#include <iostream>

class Base_monster{
public:
    void encounter(){
        script();
    } 
    virtual void script() = 0;
};


class slime : public Base_monster{
public:
    virtual void script() override{
        std::cout << "ahhhh" << std::endl;
    }
};


class mushroom : public Base_monster{
public:
    virtual void script() override{
        std::cout << "ooooowh" << std::endl;
    }
};


int main(){
    slime s;
    s.encounter();
    mushroom m;
    m.encounter();
    
    return 0;
}

 

 

Base_monster라는 기본 클래스를 만든 후, 각 몬스터가 해야하는 대사인 script를 구현한 후 각자의 derived class에서 구현해 주었다.

monsters 라는 vector를 만든 후, slime, mushroom을 넣어 준 후 script를 통해 각자의 대사를 출력해 주었다.

 

 

 

기대한 대로 잘 나온다.

다만 바로 실행 파일을 확인하지 않고, virtual table을 잠깐 확인해보면

 

 

이런 식으로 각각의 정보를 담고 있는 virtual table이 만들어지고, 이를 활용하여 script에서 본인의 타입에 맞는 script 함수를 호출하였을 것이다.

이렇게 사용하기 위해 base class를 만들고, derived class를 만든 다음 polymorphism을 사용한 것이다.

 

다만, 정말 조금의 시간이라도 줄여야 하는 상황이라면? 혹은, 저 virtual table의 공간도 아깝거나,  만들어질 derived class들이 너무 많아 table이 너무 커질 것이 우려된다면 어떻게 해야 할까?

 

 

 

 

3-2 CRTP 패턴 이용

 

이 때 , CRTP 패턴을 사용해 볼 수 있다.

 

#include <iostream>

template<typename T>
class Base_monster{
public:
    void encounter(){
        static_cast<T*>(this)->script();
    }
};


class slime : public Base_monster<slime>{
public:
    void script(){
        std::cout << "ahhhh" << std::endl;
    }
};


class mushroom : public Base_monster<mushroom>{
public:
    void script(){
        std::cout << "ooooowh" << std::endl;
    }
};


int main(){
    slime s;
    s.encounter();
    mushroom m;
    m.encounter();

    return 0;
}

 

 

위의 코드에서 달라졌다고 한 것이라면

 - base_monster class가 template 으로 변경되었다.

- base class에서 script 함수가 사라지고, encounter 내에서 this를 T* 형태로 casting 하여 script 함수를 호출한다.

 

이 역시 출력하면

 

 

이렇게 된다.

 

다만 똑같이 .o 파일을 까 보면

 

 

virtual table 내부에 아무것도 존재하지 않는 것을 볼 수 있다.

 

 

 

 

결국, crtp 패턴을 사용함으로써 인해

 

일반적인 다형성 이용 :

base class -> virtual table에서 현재 derived class의 함수 정보 가져옴 -> 해당 함수 호출

 

CRTP 패턴 이용 :

base class -> 해당 함수 호출

 

의 단계로 줄어들게 되었다!

 


 

4. C++ 에서 실제 사용하는 예시

 

먼저, singleton pattern 의 기반 클래스를 crtp를 사용하여 구현 가능하다.

template<typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }

protected:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

class MySingleton : public Singleton<MySingleton> {
    friend class Singleton<MySingleton>;

public:
    void doSomething() {}

private:
    MySingleton() {}
    ~MySingleton() {}
};

int main() {
    MySingleton& instance = MySingleton::getInstance();
    instance.doSomething();
    return 0;
}

 

 

또 C++ stl에서는 view_interface 가 CRTP 패턴으로 만들어져있다고 한다.

https://en.cppreference.com/w/cpp/ranges/view_interface

 

std::ranges::view_interface - cppreference.com

std::ranges::view_interface is a helper class template for defining a view interface. view_interface is typically used with CRTP: class my_view : public std::ranges::view_interface { public: auto begin() const { /*...*/ } auto end() const { /*...*/ } // em

en.cppreference.com

 

 

 

추가로, 위에서 장점으로 말하진 않았지만, derived class가 Base Class를 각각 가지게끔 강제시킬 수도 있다.

 

아래 두 개의 코드를 한 번 직접 비교해보자.

#include <iostream>

class too_many_monster : public std::exception{};

template<typename T>
class LimitMonster_CRTP{
    inline static int maxcnt = 0;
    public:
     LimitMonster_CRTP() {
        if (++maxcnt > 5 )
            throw too_many_monster();
        }
    ~ LimitMonster_CRTP(){
        maxcnt--;
    }
};

class slime_CRTP : public LimitMonster_CRTP<slime_CRTP>{};

class mushroom_CRTP : public LimitMonster_CRTP<mushroom_CRTP>{};

int main(){
    // NO ERROR IN HERE!
    slime_CRTP a[3];
    mushroom_CRTP b[3];
}

 

 

#include <iostream>

class too_many_monster : public std::exception{};

class LimitMonster{
    inline static int maxcnt = 0;
    public:
     LimitMonster() {
        if (++maxcnt > 5 )
            throw too_many_monster();
        }
    ~ LimitMonster(){
        maxcnt--;
    }
};

class slime : public LimitMonster{};

class mushroom : public LimitMonster{};

int main(){
    // too_many_monster throw IN HERE!
    slime a[3];
    mushroom b[3];
}

 

 

 

 

 

* 잘못된 정보는 덧글로 알려주시면 감사한 마음으로 수정하겠습니다.

반응형