콘텐츠로 건너뛰기

C++에서 템플릿(Template)

1. 함수 템플릿

템플릿을 사용하면 함수가 변수를 매개변수로 전달받아 사용하듯이, 템플릿이 타입을 받아들여서 사용할 수 있게된다.

  • 범용성 (Generic Programming): 하나의 코드로 다양한 데이터 타입을 처리하여 코드 중복을 줄임. 여러 데이타 타입을 처리하기 위해서 함수를 여러개 만들지 않아도 됨을 말함.
  • 효율성과 안정성 (Efficiency & Safety): 컴파일 타임타입을 검사하여 오류를 줄이고, 런타임 오버헤드 없는 고성능 코드를 생성함. 템플릿이 여러 타입을 받아들이게 되어서 동일한 함수명을 이용해서 서로 다른 타입을 가진 매개변수를 처리할 수 있다. 컴파일 타임에 여러 함수를 만들지 않게 되어서 오버헤드를 줄일 수 있다는 장점이 생긴다는 것이다.

문법 설명:

  • 함수는 함수명과 함께 “괄호(())”를 명시적으로 붙여야한다. 다시말하면 ” 괄호()”가 없으면 함수가 아니다.
  • 이와 마찬가지로 템플릿은, “꺽쇠 괄호(<>)”를 명시적으로 동반하여야 한다.
  • 그러므로 타입을 함수의 매개 변수 “꺽쇠 괄호(<>)” 안에 넣어서 사용하면 된다.
  • 왜 꺽쇠? 괄호(())를 사용했나? 대괄호(“[]”), 중괄호(“{}”) 모두 사용 중이라 생각하고 넘어가면 될 것 같다.
  • ‘T’는 Type(자료형)의 약자이다.
#include <iostream>

template <typename T> //typename T -> 타입이 T란 의미이다.
T add(T a, T b) {
    return a+b;
}

int main() {
    std::cout << std::showpoint; // 이후 출력되는 모든 실수에 소수점을 표시함
    std::cout << add(10,20) << std::endl;
    std::cout << add(10.0f,20.0f) << std::endl;
    std::cout << add(10.0,20.0) << std::endl;
    
    std::cout << "\n--- Type Check ---" << std::endl;
    std::cout << "Float result type:  " << typeid(add(10.0f, 20.0f)).name() << std::endl;
    std::cout << "Double result type: " << typeid(add(10.0, 20.0)).name() << std::endl;

    return 0;
}

// std::showpoint에 대한 설명은 본 포스팅의 주제와 다르지만
// 중요한 내용이라 다른 포스팅에서 다루도록 할 예정이다.

2. 클래스 템플릿

클래스 템플릿은 객체를 생성할 때 여러 타입을 처리할 수 있는 객체를 찍어 내듯이 생성할 수 있도록 해준다. std::vector를 예로 들면 std::vector는 int, float, double.. 등등의 여러 자료형을 하나의 클래스(std::vector)를 통해서 처리할 수 있다는 의미이다. 이 클래스 템플릿의 장점은 아래와 같다.

  • 자료구조의 범용성 확보 (Generic Data Structures)
  • 명시적 타입 안정성 (Type Safety)
  • 유지보수의 효율성 (Maintainability)

클래스가 객체를 찍어 내는 설계도와 같이 템플릿을 이용해서 만들어진 템플릿 클래스도 객체를 찍어 낸다. 단, 클래스 내부의 타입을 변경해서 찍어 낸다는 것이 일반 클래스와 가장 큰 다른 점이라고 이해하면 될 것이다.

아래는 클래스 템플릿의 일반적인 형태이다.

template <typename T>
class Box {
private:
    T data;
public:
    Box(T d) : data(d) {}
};

아래는 템플릿 함수(Add())를 클래스 템플릿(template <typename T> class Calculator {})으로 변경한 코드다.

#include <iostream>
#include <typeinfo>

// 클래스 템플릿 정의
template <typename T>
class Calculator {
private:
    T val1;
    T val2;

public:
    // 생성자: 멤버 변수 초기화
    Calculator(T v1, T v2) : val1(v1), val2(v2) {}

    // 덧셈 수행 함수
    T add() {
        return val1 + val2;
    }

    // 현재 T의 타입을 확인하기 위한 헬퍼 함수
    const std::string getTypeName() {
        return typeid(T).name();
    }
};

int main() {
    std::cout << std::showpoint; // 실수 소수점 표시 설정

    // 1. int 타입 인스턴스화
    Calculator<int> intCalc(10, 20);
    std::cout << "Int Add: " << intCalc.add() << std::endl;

    // 2. float 타입 인스턴스화
    Calculator<float> floatCalc(10.0f, 20.0f);
    std::cout << "Float Add: " << floatCalc.add() << std::endl;

    // 3. double 타입 인스턴스화 (C++17부터는 <double> 생략 가능 - CTAD)
    Calculator<double> doubleCalc(10.0, 20.0);
    std::cout << "Double Add: " << doubleCalc.add() << std::endl;

    std::cout << "\n--- Type Check ---" << std::endl;
    // 클래스 내부에서 T가 무엇으로 결정되었는지 확인
    std::cout << "FloatCalc T is:  " << floatCalc.getTypeName() << std::endl;
    std::cout << "DoubleCalc T is: " << doubleCalc.getTypeName() << std::endl;

    return 0;
}

---------------실행결과--------------
Int Add: 30
Float Add: 30.0000
Double Add: 30.0000

--- Type Check ---
FloatCalc T is:  f
DoubleCalc T is: d

위 코드를 읽어보면, 클래스 내부에 타입에 따른 함수를 여러개 만들지 않고, 타입에 따른 객체를 만들어서 객체명으로 매개변수의 타입을 유추할 수 있도록 만들어짐을 볼 수 있다.

3. 템플릿 특수화

특수 타입에 대해서만 템플릿을 다르게 처리하려고 할 때 사용한다. 먼저 아랫 경우에는 템플릿을 그대로 적용할 수 없게된다. 이유는 s1과 s2가 번지이기 때문이다.

template <typename T>
bool isEqual(T a, T b) {
    return a == b;
}

int main() {
    char s1[] = "hello";
    char s2[] = "hello";
    isEqual(s1, s2); // 결과는 항상 0
}

// isEqual(char* s1, char* s2)로 받아들인 후
// return s1 == s1 하기 때문이다.
// 즉, 주소값이 같은지를 비교하기 때문이다.

그러므로 isEqual 대신 문자열을 비교하려면, strcmp를 사용해서 아래와 같이 작성해야 한다.

// 예외 템플릿, 특수 템플릿

#include <iostream>
#include <cstring>

template <typename T>
bool isEqual(T a, T b) {
    return a == b;
}
// char* 전용 특수화(꼭 추가가 되어야 됨)
template <>
bool isEqual<char*>(char* str1, char* str2) {
    // strcmp를 사용하여 두 문자열이 같으면 true를 반환해야 함
    return strcmp(str1, str2)==0; //strcmp는 같을 때 0을 반환하기 때문
}

int main() {

    char s1[] = "hello";
    char s2[] = "hello";
    std::cout << isEqual(s1, s2) << std::endl; // 결과는 true(1)
    
    char s3[] = "gdbye";
    std::cout << isEqual(s1, s3) << std::endl; // 결과는 false(0)

    return 0;
}

4. 요약

함수 템플릿과 클래스 템플릿의 공통점은 함수 및 메소드 함수는 자료형에 따른 여러 함수를 하나의 템플릿으로 만들도록 한 것이다. 또한 여기에 추가하여, 클래스 템플릿은 내부 자료(property)의 타입도 객체 생성 시, 자유롭게 변경할 수 있도록 한 점이 큰 차잇점이 된다.

그러므로 클래스 템플릿으로 설계를 해 놓으면, 다양한 타입을 이용해서 객체를 생성할 수 있게된다. 이 클래스 템플릿 중, 자주 사용할만한 것이 std:: 라이브러리, 즉 STL(Standard Template Library) 라이브러리가 되었다.

위에서도 언급했지만, 이 중 std::vector가 대표적인데 이는 동적 배열(힙에 존재)을 타입에 구애받지 않고 객체로 바로 바로 만들어 낼 수 있는 훌륭한 도구임을 알고 있을 것이다. 아래에서 std::vector 사용법을 확인 할 수 있다.

#include <iostream>
#include <vector>

int main() {
    // 1. 선언 및 초기화 (C++11 초기화 리스트 사용)
    std::vector<int> v = {10, 20, 30};

    // 2. 요소 추가 (push_back)
    // 메모리가 부족하면 자동으로 재할당(Reallocation)이 일어남. 메모리 블록을 옮긴다는 의미.
    v.push_back(40); 
    v.push_back(50);

    // 3. 요소 접근 및 수정 (배열과 동일)
    // 시간 복잡도: O(1)
    v[0] = 999; 

    // 4. 크기 확인
    std::cout << "Size: " << v.size() << "\n"; // 출력: 5

    // 5. 순회 (Range-based for loop)
    std::cout << "Elements: ";
    for (int num : v) {
        std::cout << num << " ";
    }
    std::cout << "\n";

    return 0;
}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다