Post thumbnail

vtable은 조상님이 만들어줌? CRTP로 virtual 없이 다형성 구현하기

· by 박승재

템플릿(Template)은 C언어에는 없는 C++만의 특별한 문법입니다.

템플릿은 코드를 찍어내는 틀처럼 컴파일 시간에 타입에 맞는 코드를 생성하기 때문에 템플릿을 이용하면 다양한 타입에 대한 코드를 여러 개 만들 필요가 없습니다.

타입에 맞는 코드를 자동으로 생성한다는 점은 다형성 구현에도 이용됩니다.

상속을 통해 구현한 다형성은 런타임(프로그램 실행 시간)에 필요한 코드를 불러오기 때문에 느립니다.

반면, 템플릿을 통해 구현한 다형성은 컴파일 시간에 필요한 코드가 완성된 상태이기 때문에 상속보다 빠르게 동작합니다.

Polymorphism

다형성(Polymorphism)은 poly(여러 개의) + morphism(형태)의 합성어이며, 같은 형태의 코드가 다르게 동작하도록 하는 것을 말합니다.

class Animal {
public:
    virtual void print() const {
        std::cout << "Animal\n";
    }
};
class Cat : public Animal {
    void print() const override {
        std::cout << "Cat\n";
    }
};
class Dog : public Animal {
    void print() const override {
        std::cout << "Dog\n";
    }
};
int main() {
    std::vector<Animal*> v;
    v.push_back(new Cat);
    v.push_back(new Dog);
    v[0]->print(); // Cat
    v[1]->print(); // Dog
    return 0;
}

또한, 다형성은 타입을 일반화해서 코드를 다룰 수 있게 하므로 깔끔한 코드를 작성하기 위해서 중요합니다.

여기서 동적 다형성과 정적 다형성으로 나뉘게 되는데, 동적(런타임) 다형성상속을 통해 구현하며 정적(컴파일 타임) 다형성템플릿을 통해 구현합니다.

Template

template<typename T>
T sum(T a, T b) {
    return a + b;
}
sum<int>(1, 2); // 3
sum<float>(1.1, 2.2); // 3.3

// 함수 이름은 예시일 뿐이며, 실제로 함수 이름이 이렇게 생성되지 않음
int sum_int(int a, int b) {
    return a + b;
}
float sum_float(float a, float b) {
    return a + b;
}
sum_int(1, 2); // 3
sum_float(1.1, 2.2); // 3.3

sum 템플릿 함수는 sum_intsum_float를 찍어내는 틀이 되어 각 타입에 맞는 새로운 코드를 생성합니다.

template<typename T>은 과거에 template<class T> 형태로 작성되었으나, class가 주는 어감(ex. int, char 등은 클래스가 아니지만 T에 들어올 수 있음)으로 인해 typename이 추가되었습니다.

classtypename은 이름만 다를 뿐 완전히 동일한 의미의 키워드로, typename 사용을 권장합니다.

템플릿의 최고 장점은 “빠르게 동작하는 코드를 작성할 수 있다”는 것입니다.

반면, 템플릿의 주요 단점으로는,

  1. 읽고 쓰기 어렵다. (가독성)
  2. 디버깅이 어렵다.
  3. 컴파일 시간이 늘어난다.
  4. 프로그램의 크기가 커진다.
  5. 정의와 구현을 분리할 수 없고, 모두 헤더파일에 들어간다.

무려 5가지나 있네요.

그런데도 우리가 템플릿을 쓰는 이유는 딱 하나, “성능“입니다.

하지만 성능만 보고 템플릿을 남용하면 코드 꼬이기 딱 좋기에, 프로그래밍계의 흑마법이라고도 불리는 문법입니다.

CRTP

CRTPCuriously Recurring Template Pattern의 약자로, 직역하면 기묘한 재귀 템플릿 패턴이란 뜻입니다.

template<class T>
class Base {
};

class Derived : public Base<Derived> {
};

스스로를 인자로 받는 템플릿을 상속하는 클래스 형태이며, virtual을 사용하지 않고 클래스의 메소드를 재사용하기 위해 사용합니다.

template<typename T>
class Animal {
public:
    void print() const {
        (static_cast<const T&>(*this)).print();
    }
};

class Cat : public Animal<Cat> {
public:
    void print() const {
        std::cout << "Cat\n";
    }
};

template<typename T>
void print_animal(const Animal<T>& animal) {
    animal.print();
}

CRTP를 통해 virtual 없이 다형성을 구현한 예제입니다.

위와 같이 템플릿을 이용한 다형성을 정적 다형성(Static Polymorphism)이라 합니다.

virtual을 사용하게되면 성능 손실이 따라오기 때문에, 성능 손실 없이 class마다 동일한 메소드 구현이 필요할 때 CRTP를 사용합니다.

Non-virtual class

CRTP를 사용하지 않고, 그냥 virtual만 지우면 어떻게 동작하는지 알아보겠습니다.

class Animal {
public:
    void print() const { // non-virtual
        std::cout << "Animal\n";
    }
};

class Cat : public Animal {
    void print() const {
        std::cout << "Cat\n";
    }
};

void print_animal(const Animal& animal) {
    animal.print();
}

int main() {
    Cat cat;
    print_animal(cat); // Animal
    return 0;
}

Animal이 나오는 이유는 객체지향 프로그래밍 강의에서 다 배웠죠?