이번에는 변수 템플릿을 활용해, 클래스 템플릿을 개선하는 방법을 정리해본다.

먼저 변수 템플릿이란, 컴파일 타임에 상수값을 결정하고, 타입에 따라 달라지는 상수를 정의하기 위한 기능이다.


constconstexpr

우선 변수 템플릿으로 사용 할 수 있는 constconstexpr에 대해서 알아보자.

const (상수 변수)

  • const 키워드는 변경할 수 없는 변수를 선언할 때 사용.
  • const 변수는 런타임에 상수로 결정될 수도 있고, 컴파일 타임에 결정될 수도 있다.
  • 기본적으로 const 변수는 정적(static) 바인딩되지 않으며, 컴파일러가 항상 상수로 최적화하지 않을 수도 있다.
const int x = 10;  // 컴파일 타임에 결정됨 (최적화 가능)
int y = 20;
const int z = y;   // 런타임에 결정됨 (컴파일러가 상수로 확정할 수 없음)
  • x컴파일 타임 상수이므로 최적화될 가능성이 높음.
  • 하지만 zy의 값을 기반으로 초기화되므로 런타임 상수일 수도 있음. → 즉, const가 항상 컴파일 타임 상수는 아님!

constexpr 과 컴파일 타임 평가

  • constexpr컴파일 타임에 반드시 평가되는 상수를 의미한다.
  • constexpr을 사용하면 컴파일 타임 최적화가 가능하다.
  • 함수에도 적용할 수 있어, 컴파일 타임에 실행 가능한 연산을 수행할 수 있다.
constexpr int square(int x) {
    return x * x;
}

constexpr int result = square(5);  // 컴파일 타임에 평가됨
int value = 10;
int runtime_result = square(value); // 가능하지만, 런타임에 평가됨
  • square(5)컴파일 타임에 평가되어 최적화될 수 있음.
  • 하지만 square(value)value런타임 변수이므로 런타임 연산이 수행됨.

언제 const vs constexpr을 써야 할까?

  • 컴파일 타임에 결정될 값이면 constexpr, 런타임 값이 될 수도 있다면 const.
  • 함수를 컴파일 타임에도 실행하고 싶다면 constexpr 함수 사용.
  • 가능한 경우 constexpr을 사용하여 최적화를 극대화하는 것이 좋다.

컴파일 타임

근데 컴파일 타임은 뭘까?

컴파일 타임은 소스 코드가 실행 파일로 변환되는 과정을 뜻한다.

즉, 소스 코드를 다른 프로그램이나 기계(하드웨어)가 처리하기 용이한 형태로 변환하는 과정이다.

컴파일 타임에 수행되는 작업들

  • 문법 검사(Syntax Checking)
    • ; 누락, {} 범위 오류, 타입 불일치 등 코드의 문법 오류 검사
  • 타입 체크(Type Checking)

    • 정수와 문자열을 잘못된 방식으로 연산하는 경우 오류 발생

    • 예: "string" + 5;컴파일 오류 발생

  • 최적화(Optimization)

    • 불필요한 연산 제거 (x * 00)

    • 함수 인라인 처리, 루프 전개 등의 성능 최적화 수행

  • 템플릿 인스턴스화(Template Instantiation)
    • std::vector<int> 같은 템플릿 클래스의 구체적인 코드 생성
  • 컴파일 타임 상수 계산(Constant Evaluation)

    • constexpr을 사용한 연산 수행

    • 예: constexpr int square(int x) { return x * x; }

    • constexpr int result = square(5);25로 변환됨

컴파일 타임 연산이 중요한 이유

  • 성능 최적화: 컴파일 시 계산되면 불필요한 연산이 제거되므로 실행 속도가 빨라짐.
  • 오류 예방: 컴파일 타임에 오류를 잡을 수 있어 안정적인 코드 작성 가능.
  • 가독성 향상: 복잡한 런타임 조건문을 줄일 수 있음.

변수 템플릿

그럼 다시 원점으로 돌아와 변수 템플릿 왜 사용 하는 것일까?

  • 타입 의존 상수 정의 변수 템플릿은 템플릿 매개변수를 활용해 타입에 따라 다른 상수를 정의할 수 있게 해준다. 예를 들어, 여러 타입에 대해 서로 다른 정밀도의 파이 값을 정의할 때 유용하다.

    template <typename T>
    constexpr T pi = T(3.1415926535897932385);
      
    double d = pi<double>; // double 타입의 파이 값
    float f = pi<float>;   // float 타입의 파이 값
    
  • 코드 재사용성과 가독성 향상 변수 템플릿을 사용하면 동일한 상수 또는 계산 결과를 타입에 따라 여러 번 정의하지 않고도 재사용할 수 있다.

  • 컴파일 타임 상수와의 결합 constexpr와 함께 사용하면, 타입에 따른 상수 값이 컴파일 타임에 결정되므로, 템플릿 기반 메타프로그래밍이나 복잡한 상수 계산 시 유용하게 활용할 수 있다.

부분특수화

변수 템플릿을 활용하면, 부분 특수화(Partial Specialization)를 사용 할 수 있다.

템플릿이 특정 조건을 만족하면, 일반 템플릿과는 다른 구현을 제공하는 기능이다.

기본 템플릿이 모든 타입을 다루도록 정의되어 있지만, 특정 타입 조합에 대해서는 별도로 특수한 버전을 정의할 수 있다.

부분 특수화의 필요성

  • 특정 타입에 대해 최적화된 코드 제공
    • 일반적인 템플릿은 범용적으로 동작하지만, 특정 타입 조합에서는 더 효율적인 구현이 가능
  • 특정 타입에 대해 다르게 동작하도록 설정
    • 특정 타입에 대해 다른 동작이 필요할 때 부분 특수화를 사용하면 코드 가독성과 유지보수에 이점이있다.

변수 템플릿의 부분 특수화

// 변수 템플릿 기본 정의
template<typename T1, typename T2>
constexpr bool mybool = false;

// 부분 특수화: T1과 T2가 같은 경우 true
template<typename T1>
constexpr bool mybool<T1, T1> = true;
  • mybool<T1, T2>의 기본 값은 false
  • 하지만, mybool<T1, T1> 즉, 두 타입이 같을 경우 true가 되도록 부분 특수화되었다.
  • 예) mybool<int, float>false, mybool<int, int>true

  • 기본 제공하는 부분 특수화: std::is_same_v<T1, T2>

부분 특수화 활용

template<typename T>
ASSET_TYPE GetAssetType()
{
    if constexpr (mybool<T, CMesh>)
        return ASSET_TYPE::MESH;

    if constexpr (mybool<T, CComputeShader>)
        return ASSET_TYPE::COMPUTE_SHADER;

    if constexpr (mybool<T, CGraphicShader>)
        return ASSET_TYPE::GRAPHICS_SHADER;
}

이 코드에서 부분 특수화가 중요한 이유

  • 컴파일 타임에 타입을 결정하고 if constexpr을 활용하여 불필요한 조건문을 제거함.
  • mybool<T, SomeType>true가 되는 경우만 코드가 컴파일되므로, 실행 속도가 최적화됨.
  • typeid()를 사용한 런타임 타입 체크 방식보다 훨씬 빠르고 안전한 방식.

클래스 템플릿에서의 부분 특수화

조금 더 활용하면 변수 템플릿뿐만 아니라, 클래스 템플릿에서도 부분 특수화를 사용할 수 있다.

// 기본 템플릿
template<typename T1, typename T2>
class MyClass {
public:
    static void print() { std::cout << "Generic template\n"; }
};

// 부분 특수화: 두 타입이 같을 경우
template<typename T>
class MyClass<T, T> {
public:
    static void print() { std::cout << "Specialized template for same types\n"; }
};

int main() {
    MyClass<int, double>::print();  // "Generic template"
    MyClass<int, int>::print();     // "Specialized template for same types"
}
  • 기본적으로 MyClass<T1, T2>어떤 타입이든 사용할 수 있는 일반적인 템플릿.
  • MyClass<T, T> 부분 특수화를 통해 T1과 T2가 같을 경우 다른 동작을 수행하도록 설정.

명시적 특수화

부분 특수화와 다른 명시적 특수화도 있다.

// 기본 템플릿
template<typename T>
class MyClass {
public:
    static void print() { std::cout << "Generic template\n"; }
};

// 명시적 특수화: int 타입에 대해 완전히 다른 코드 제공
template<>
class MyClass<int> {
public:
    static void print() { std::cout << "Specialized for int\n"; }
};

int main() {
    MyClass<double>::print();  // "Generic template"
    MyClass<int>::print();     // "Specialized for int"
}
  • 특정 타입에 대해 완전히 다른 구현을 제공하는 것
  • 특정 타입 하나만 완전히 다른 동작을 하도록 변경
  • 즉, 특정 타입 (int)에 대해 완전히 다른 구현을 제공하는 것이 명시적 특수화
  • 반면, MyClass<T, T>처럼 타입 패턴을 기준으로 특수화하는 것이 부분 특수화

if constexpr vs typeid().hash_code() 비교

그럼 부분 특수화를 사용하면 내 코드에서 어떤 개선이 가능할까?

기존 코드 (typeid().hash_code())

if (info.hash_code() == typeid(CComputeShader).hash_code())
    return ASSET_TYPE::COMPUTE_SHADER;
if (info.hash_code() == typeid(CGraphicShader).hash_code())
    return ASSET_TYPE::GRAPHICS_SHADER;
  • 런타임 비용이 발생

    • typeid(T).hash_code()는 런타임에 평가되므로, 실행 중 비교 연산이 필요.

    • 최적화되지 않으면 성능 저하가 발생할 수 있다.

  • 컴파일 타임 최적화 불가

    • switch-case 같은 최적화 기법이 불가능하며, 분기가 많아질수록 성능이 떨어질 수 있다.

    • if문이 많아지면 O(n)의 비교 연산이 발생하므로 성능 저하가 될 수 있다.

  • 가독성과 유지보수성이 떨어짐

    • 새로운 타입이 추가될 때마다 if 문을 계속 추가해야 하므로, 유지보수가 어렵다.

템플릿과 if constexpr을 사용한 방식

template<typename T>
ASSET_TYPE GetAssetType()
{
    if constexpr (std::is_same_v<T, CComputeShader>)
        return ASSET_TYPE::COMPUTE_SHADER;
    else if constexpr (std::is_same_v<T, CGraphicShader>)
        return ASSET_TYPE::GRAPHICS_SHADER;
}
  • 컴파일 타임 평가로 성능 향상

    • if constexpr컴파일 타임에 결정되므로 런타임 비용이 완전히 사라짐

    • 불필요한 조건문이 아예 컴파일되지 않음 → 최적화 효과 극대화

  • 더 나은 가독성과 유지보수성

    • 새로운 타입을 추가할 때 if constexpr 분기만 추가하면 됨.

    • typeid().hash_code() 비교보다 직관적이며, 실수를 줄일 수 있음.

  • 안전한 타입 체크

    • std::is_same_v<T, U>을 사용하면, 타입이 정확하게 일치할 때만 조건을 만족하므로, 더 안전하게 사용 가능.

언제 어떤 방식을 써야 할까?

  • 컴파일 타임에 타입이 결정될 수 있다면
    • if constexpr을 사용하여 성능 최적화유지보수 용이성을 확보하 는 것이 좋다.
  • 타입이 런타임에 동적으로 결정되는 경우
    • typeid().hash_code()를 사용해야 하지만, 가능하면 std::variantstd::unordered_map 등의 대안을 고려하는 것이 좋다.

즉, 가능한 경우 if constexpr을 사용하여 컴파일 타임 최적화를 활용하는 것이 가장 좋은 방법이다.

변수 템플릿과 부분 특수화를 정리하게 된 이유

파이썬만 사용하다가 C++에서 포인터 개념을 처음 접했을 때의 혼란감을 아직도 기억한다. 런타임이 아닌 컴파일 타임에 연산이 수행된다는 개념이 직관적으로 와닿지 않아 이해하는 데 어려움을 겪었다. 특히, constexprconst의 차이, 그리고 템플릿의 부분 특수화(Partial Specialization) 개념은 처음 봤을 때 또 다른 장벽처럼 느껴졌다.

하지만 이 개념을 조금 이해하고 나니, C++에서의 성능 최적화와 유지보수의 중요성을 깨닫게 되었다. 이번 정리를 통해 템플릿을 활용한 코드 최적화 방식을 아카이브하고자 한다.

중요) 같은 고민을 할 미래의 나에게 도움이 되었으면 한다.

태그: ,

카테고리:

업데이트:

댓글남기기