Skip to content

17. 템플릿(Template)


1. 키워드

  • 템플릿(Template)
  • 일반화 프로그래밍(Generic Programming)
  • 함수 템플릿(Function Template)
  • templatetypename 키워드
  • 클래스 템플릿(Class Template)
  • 멤버 템플릿(Member Template)과 중첩 클래스 템플릿(Nested Class Template)
  • 명시적 특수화(Explicit Specialization)
  • 부분 특수화(Partial Specialization)
  • 스마트 포인터(Smart Pointer)
  • memory 헤더 파일
  • unique_ptr 스마트 포인터와 make_unique() 함수
  • shared_ptr 스마트 포인터와 make_shared() 함수
  • 순환 참조(Circular Reference)와 weak_ptr 스마트 포인터


2. 함수 템플릿

1) 일반화 프로그래밍

  • C++이 가지는 프로그래밍 언어로서의 특징 중 하나로 일반화 프로그래밍을 들 수 있다.
  • 일반화 프로그래밍은 데이터를 중시하는 OOP와는 달리 프로그램의 알고리즘에 그 중점을 둔다.
  • 이러한 일반화 프로그래밍을 지원하는 C++의 대표적인 기능 중 하나가 바로 템플릿이다.


2) 템플릿

  • 템플릿이란 매개변수의 타입에 따라 함수나 클래스를 생성하는 메커니즘을 의미한다.
  • 템플릿은 타입이 매개변수에 의해 표현되므로, 매개변수화 타입(Parameterized Type)이라고도 불린다.
  • 템플릿을 사용하면 타입마다 별도의 함수나 클래스를 만들지 않고, 여러 타입에서 동작할 수 있는 단 하나의 함수나 클래스를 작성하는 것이 가능하다.


3) 함수 템플릿

  • C++에서 함수 템플릿이란 함수의 일반화된 선언을 의미한다.
  • 함수 템플릿을 사용하면 같은 알고리즘을 기반으로 하면서, 서로 다른 타입에서 동작하는 함수를 한 번에 정의할 수 있다.
  • 임의의 타입으로 작성된 함수에 특정 타입을 매개변수로 전달하면, C++ 컴파일러는 해당 타입에 맞는 함수를 생성해 준다.


  • C++에서 함수 템플릿은 다음과 같은 문법으로 정의한다.


template <typename 타입이름>
함수의원형
{
    함수의본체
}


  • C++98에서 추가된 typename 키워드 이전에는 class 키워드를 사용했다.
  • 따라서 C++ 컴파일러는 템플릿 정의 내의 typename 키워드와 class 키워드를 같은 것으로 간주한다.


  • 위에서 정의된 타입이름함수의원형함수의본체에서 임의의 타입으로 사용할 수 있으며, 보통 T를 지정한다.
  • 이렇게 정의된 함수 템플릿을 호출할 때 매개변수로 int 타입을 전달하면, 함수의원형함수의본체에서 정의된 타입이름은 모두 int 타입으로 바뀌게 된다.


  • 다음의 코드는 여러 타입의 변수의 값을 서로 바꿔주는 Swap() 함수를 함수 템플릿으로 작성한 것이다.


#include <iostream>
#include <string>
using namespace std;

template <typename T>
void Swap(T &, T &);

int main(void) {
    int c = 2, d = 3;
    cout << "c: " << c << ", d: " << d << endl;
    Swap(c, d);
    cout << "c: " << c << ", d: " << d << endl;

    string e = "hong", f = "kim";
    cout << "e: " << e << ", f: " << f << endl;
    Swap(e, f);
    cout << "e: " << e << ", f: " << f << endl;

    return 0;
}

template <typename T>
void Swap(T &a, T &b) {
    T temp;

    temp = a;
    a = b;
    b = temp;
}

// c: 2, d: 3
// c: 3, d: 2
// e: hong, f: kim
// e: kim, f: hong


  • 위의 코드에서 Swap() 함수 템플릿은 정수형 숫자뿐만 아니라 문자열에 대해서도 정상적으로 동작한다.


4) 함수 템플릿의 인스턴스화

  • 함수 템플릿이 각각의 타입에 대해 처음으로 호출될 때, C++ 컴파일러는 해당 타입의 인스턴스를 생성한다.
  • 이렇게 생성된 인스턴스는 해당 타입에 대해 특수화된 템플릿 함수이다.
  • 이 인스턴스는 함수 템플릿에 해당 타입이 사용될 때마다 호출된다.


5) 명시적 특수화

  • C++의 함수 템플릿은 특정 타입에 대한 명시적 특수화를 제공하여, 해당 타입에 대해 특별한 동작을 정의할 수 있게 해준다.
  • 컴파일러는 호출된 함수에 정확히 대응하는 특수화된 정의를 발견하면, 더는 템플릿을 찾지 않고 해당 정의를 사용한다.


  • 앞서 정의된 함수 템플릿 Swap()double 타입에 대한 명시적 특수화는 다음과 같다.


// 함수 템플릿 원형
template <typename T>
void Swap(T &a, T &b);
// double 타입을 위한 명시적 특수화
template <>
void Swap<double>(double &, double &){ ... };


  • 다음의 코드는 Swap() 함수 템플릿에서 double 타입에 대한 동작만을 변경하기 위해 명시적 특수화를 사용한 것이다.


#include <iostream>
using namespace std;

template <typename T>
void Swap(T &, T &);

template <>
void Swap<double>(double &, double &);

int main(void) {
    int c = 2, d = 3;
    cout << "c: " << c << ", d: " << d << endl;
    Swap(c, d);
    cout << "c: " << c << ", d: " << d << endl;

    double e = 1.234, f = 4.321;
    cout << "e: " << e << ", f: " << f << endl;
    Swap(e, f);
    cout << "e: " << e << ", f: " << f << endl;

    return 0;
}

template <typename T>
void Swap(T &a, T &b) {
    T temp;

    temp = a;
    a = b;
    b = temp;
}

template <>
void Swap<double>(double &a, double &b) {
    // double 타입은 값을 서로 바꾸지 않음
}

// c: 2, d: 3
// c: 3, d: 2
// e: 1.234, f: 4.321
// e: 1.234, f: 4.321


  • 위의 코드에서 Swap() 함수는 double 타입에 대해서는 더는 값을 서로 바꾸지 않게 된다.


3. 클래스 템플릿

  • C++에서 클래스 템플릿이란 클래스의 일반화된 선언을 의미한다.
  • 앞서 살펴본 함수 템플릿과 동작은 같으며, 그 대상이 함수가 아닌 클래스라는 점만 다르다.


  • 클래스 템플릿을 사용하면, 타입에 따라 다르게 동작하는 클래스 집합을 만들 수 있다.
  • 즉, 클래스 템플릿에 전달되는 템플릿 인수(Template Argument)에 따라 별도의 클래스를 만들 수 있게 된다.
  • 이러한 템플릿 인수는 타입이거나 명시된 타입의 상숫값일 수 있다.


  • C++에서 클래스 템플릿은 다음과 같은 문법으로 정의할 수 있다.


template <typename 타입이름>
class 클래스템플릿이름
{
    // 클래스 멤버의 선언
}


  • 함수 템플릿과 마찬가지로 템플릿 정의 내에서 typename 키워드 대신에 class 키워드를 사용할 수 있다.
  • 위에서 정의된 타입이름은 클래스의 선언에서 임의의 타입으로 사용할 수 있다.


  • 다음의 코드는 클래스 템플릿을 사용하여 다양한 타입의 데이터를 저장할 수 있는 Data 클래스를 작성한 것이다.


#include <iostream>
#include <string>
using namespace std;

template <typename T>
class Data {
   private:
    T data_;

   public:
    Data(T dt);
    T get_data();
};

int main(void) {
    Data<string> str_data = Data<string>("C++ 수업");
    Data<int> int_data = Data<int>(12);

    cout << "str_data: " << str_data.get_data() << endl;
    cout << "int_data: " << int_data.get_data() << endl;

    return 0;
}

template <typename T>
Data<T>::Data(T dt) {
    this->data_ = dt;
}

template <typename T>
T Data<T>::get_data() {
    return this->data_;
}

// str_data: C++ 수업
// int_data: 12


  • 위처럼 클래스 템플릿으로부터 객체를 생성할 때에는 <>(꺾쇠괄호) 안에 템플릿에 전달된 인수 타입을 명시해야 한다.
  • 전달된 매개변수의 타입을 가지고 컴파일러가 해당 타입에 맞는 함수를 생성해 주는 함수 템플릿과는 달리, 클래스 템플릿은 사용자가 사용하고자 하는 타입을 명시적으로 제공해야 한다.


  • 또한 다음과 같이 포인터 객체를 생성할 수 있다.


#include <iostream>
#include <string>
using namespace std;

template <typename T>
class Data {
   private:
    T data_;

   public:
    Data(T dt);
    T get_data();
};

int main(void) {
    Data<string> *ptr_str_data = new Data<string>("C++ 수업");
    Data<int> *ptr_int_data = new Data<int>(12);

    cout << "ptr_str_data: " << ptr_str_data->get_data() << endl;
    cout << "ptr_int_data: " << ptr_int_data->get_data() << endl;

    delete ptr_str_data;
    delete ptr_int_data;

    return 0;
}

template <typename T>
Data<T>::Data(T dt) {
    this->data_ = dt;
}

template <typename T>
T Data<T>::get_data() {
    return this->data_;
}

// ptr_str_data: C++ 수업
// ptr_int_data: 12


1) 중첩 클래스 템플릿

  • C++에서는 클래스나 클래스 템플릿 내에 또 다른 템플릿을 중첩하여 정의할 수 있으며, 이러한 템플릿을 멤버 템플릿이라고 한다.
  • 멤버 템플릿 중에서도 클래스 템플릿을 중첩 클래스 템플릿이라고 한다.
  • 이러한 중첩 클래스 템플릿은 바깥쪽 클래스의 범위 내에서 클래스 템플릿으로 선언되며, 정의는 바깥쪽 클래스의 범위 내에서 뿐만 아니라 범위 밖에서도 가능하다.


template <typename T>
class X
{
    template <typename U>
    class Y
    {
        ...
    }
    ...
    }

int main(void)
{
    ...
}

template <typename T>
template <typename U>
X<T>::Y<U>::멤버함수이름()
{
    ...
}


  • 위의 코드처럼 바깥쪽 클래스인 X의 외부에 중첩 클래스 템플릿인 Y를 정의하면, 클래스 템플릿의 템플릿 인수와 멤버 템플릿의 템플릿 인수가 둘 다 앞에 명시되어야 한다.


2) 클래스 템플릿의 특징

  • C++에서 클래스 템플릿은 하나 이상의 템플릿 인수를 가지는 클래스 템플릿을 선언할 수 있다.


// 두 개의 템플릿 인수를 가지는 클래스 템플릿을 선언함
template <typename T, int i>
class X { ... }


  • 그리고 클래스 템플릿에 디폴트 템플릿 인수를 명시할 수 있다.


// 디폴트 템플릿 인수의 타입을 int 타입으로 명시함
template <typename T = int, int i>
class X { ... }


  • 마지막으로 클래스 템플릿을 기초 클래스로 하여 상속할 수 있다.


// 클래스 템플릿 X를 상속받음
template <typename Type>
class Y : public X<Type> { ... }


3) 명시적 특수화

  • 클래스 템플릿은 함수 템플릿과 마찬가지로 특정 타입이나 값의 템플릿 인수에 대하여 특수화할 수 있다.
  • 이렇게 특수화를 명시하면, 해당 타입에 대한 특별한 동작을 정의할 수 있다.
  • 컴파일러는 전달된 인수에 정확히 대응하는 특수화된 정의를 발견하면, 더는 다른 템플릿을 찾지 않고 해당 정의를 사용한다.


  • 앞서 정의된 클래스 템플릿 Xdouble 타입에 대한 명시적 특수화는 다음과 같다.


template <>
class X<double> { ... };


  • 다음의 코드는 Data 클래스 템플릿에서 double 타입에 대한 동작만을 변경하기 위해 명시적 특수화를 사용한 것이다.


#include <iostream>
using namespace std;

template <typename T>
class Data {
   private:
    T data_;

   public:
    Data(T dt);
    T get_data();
};

template <>
class Data<double> {  // double 타입에 대한 명시적 특수화
   private:
    double data_;

   public:
    Data(double dt) { this->data_ = dt; }
    double get_data() {
        cout << "double 타입 데이터를 출력합니다." << endl;

        return this->data_;
    }
};

int main(void) {
    Data<string> str_data("C++ 수업");
    Data<double> double_data(3.14);
    double d_data;

    cout << "str_data: " << str_data.get_data() << endl;

    d_data = double_data.get_data();
    cout << "double_data: " << d_data << endl;

    return 0;
}

template <typename T>
Data<T>::Data(T dt) {
    this->data_ = dt;
}

template <typename T>
T Data<T>::get_data() {
    return this->data_;
}

// str_data: C++ 수업
// double 타입 데이터를 출력합니다.
// double_data: 3.14


4) 부분 특수화

  • 만약 템플릿 인수가 두 개 이상이고, 그중 일부에 대해서만 특수화를 해야 할 때는 부분 특수화를 사용할 수 있다.
  • 부분 특수화 방법은 먼저 template 키워드 다음에 나오는 <>에 특수화하지 않는 타입의 템플릿 인수를 명시한다.
  • 그리고서 다음에 나오는 <>에 특수화하는 타입을 명시하면 된다.


template <typename T1, typename T2>
class X { ... };


  • 위의 코드에서 선언된 클래스 템플릿 Xdouble 타입에 대해 부분 특수화를 하면 다음과 같다.


template <typename T1>
class X<T1, double> { ... };


  • 위의 코드에 T1 타입까지 특수화하게 되면, 앞서 살펴본 완전한 명시적 특수화가 된다.


template <>
class X<double, double> { ... };


5) 템플릿을 위한 새로운 이름

  • C++11부터는 typedef 키워드를 이용하여 템플릿 특수화를 위한 새로운 이름을 선언할 수 있다.


typedef X<double, 3.14> DoubleX;

...

DoubleX double_x;  // double_x는 X<double, 3.14> 타입임.


4. 스마트 포인터

  • C++ 프로그램에서 new 키워드를 사용하여 동적으로 할당 받은 메모리는, 반드시 delete 키워드를 사용하여 해제해야 한다.
  • C++에서는 메모리 누수로부터 프로그램의 안전성을 보장하기 위해 스마트 포인터를 제공하고 있다.
  • 스마트 포인터란 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제해 준다.


1) 스마트 포인터의 동작

  • 보통 new 키워드를 사용해 기본 포인터(Raw Pointer)가 실제 메모리를 가리키도록 초기화한 후에, 기본 포인터를 스마트 포인터에 대입하여 사용한다.
  • 이렇게 정의된 스마트 포인터의 수명이 다하면, 소멸자는 delete 키워드를 사용하여 할당된 메모리를 자동으로 해제한다.
  • 따라서 new 키워드가 반환하는 주소값을 스마트 포인터에 대입하면, 따로 메모리를 해제할 필요가 없어진다.


2) 스마트 포인터의 종류

  • C++11 이전에는 auto_ptr이라는 스마트 포인터를 사용하여 이 작업을 수행했다.
  • 하지만 C++11부터는 다음과 같은 새로운 스마트 포인터를 제공하고 있다.


1] unique_ptr

2] shared_ptr

3] weak_ptr


  • 스마트 포인터는 memory 헤더 파일에 정의되어 있으며, auto_ptr은 C++11 표준부터 삭제되었다.


(1) unique_ptr

  • unique_ptr은 하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터이다.
  • 이 스마트 포인터는 해당 객체의 소유권을 가지고 있을 때만, 소멸자가 해당 객체를 삭제할 수 있다.
  • unique_ptr 인스턴스는 move() 멤버 함수를 통해 소유권을 이전할 수는 있지만, 복사할 수는 없다.
  • 소유권이 이전되면, 이전 unique_ptr 인스턴스는 더는 해당 객체를 소유하지 않게 재설정된다.


unique_ptr<int> ptr01(new int(5));  // int 타입 unique_ptr인 ptr01을 선언하고 초기화함

auto ptr02 = move(ptr01);           // ptr01에서 ptr02로 소유권을 이전함

// unique_ptr<int> ptr03 = ptr01;   // 대입 연산자를 이용한 복사는 오류를 발생시킴

ptr02.reset();                      // ptr02가 가리키고 있는 메모리 영역을 삭제함
ptr01.reset();                      // ptr01가 가리키고 있는 메모리 영역을 삭제함


  • 위의 코드에서 주석 처리된 구문처럼 스마트 포인터에 대한 =(대입 연산자)를 이용한 복사는 오류를 발생시킬 것이다.
  • 보통의 C++ 객체에 대해 스마트 포인터가 필요한 상황에서는 주로 unique_ptr을 사용하면 된다.


  • C++14 이후부터 제공되는 make_unique() 함수를 사용하면 unique_ptr 인스턴스를 안전하게 생성할 수 있다.
  • make_unique() 함수는 전달 받은 인수를 사용해 지정된 타입의 객체를 생성하고, 생성된 객체를 가리키는 unique_ptr을 반환한다.
  • 이 함수를 사용하면, 예외 발생에 대해 안전하게 대처할 수 있다.


  • 다음의 코드는 Person 객체를 가리키는 hong이라는 unique_ptrmake_unique() 함수를 통해 생성하는 것이다.


#include <iostream>
#include <memory>
using namespace std;

class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    ~Person() { cout << "소멸자가 호출되었습니다." << endl; }
    void ShowPersonInfo();
};

int main(void) {
    unique_ptr<Person> hong = make_unique<Person>("길동", 29);

    hong->ShowPersonInfo();

    return 0;
}

Person::Person(const string &name, int age)  // 기초 클래스 생성자의 정의
{
    this->name_ = name;
    this->age_ = age;

    cout << "생성자가 호출되었습니다." << endl;
}

void Person::ShowPersonInfo() {
    cout << this->name_ << "의 나이는 " << this->age_ << "살입니다." << endl;
}

// 생성자가 호출되었습니다.
// 길동의 나이는 29살입니다.
// 소멸자가 호출되었습니다.


  • 위의 코드에서 Person 객체를 가리키는 unique_ptr 인스턴스인 hong은 일반 포인터와는 달리 사용이 끝난 후에 delete 키워드를 사용하여 메모리를 해제할 필요가 없다.


(2) shared_ptr

  • shared_ptr은 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지를 참조하는 스마트 포인터이다.
  • 이렇게 참조하고 있는 스마트 포인터의 개수를 참조 횟수(Reference Count)라고 한다.
  • 참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하며, 수명이 다할 때마다 1씩 감소한다.
  • 따라서 마지막 shared_ptr의 수명이 다하여 참조 횟수가 0이 되면, delete 키워드를 사용하여 메모리를 자동으로 해제한다.


shared_ptr<int> ptr01(new int(5));  // int 타입 shared_ptr인 ptr01을 선언하고 초기화함
cout << ptr01.use_count() << endl;  // 1

auto ptr02(ptr01);                  // 복사 생성자를 이용한 초기화
cout << ptr01.use_count() << endl;  // 2

auto ptr03 = ptr01;                 // 대입을 통한 초기화
cout << ptr01.use_count() << endl;  // 3


  • 위의 코드에서 사용된 use_count() 멤버 함수는 shared_ptr 객체가 현재 가리키고 있는 리소스를 참조 중인 소유자의 수를 반환해 준다.


  • 위와 같은 방법 이외에도 make_shared() 함수를 사용하면 shared_ptr 인스턴스를 안전하게 생성할 수 있다.
  • make_shared() 함수는 전달 받은 인수를 사용해 지정된 타입의 객체를 생성하고, 생성된 객체를 가리키는 shared_ptr을 반환한다.
  • 이 함수를 사용하면, 예외 발생에 대해 안전하게 대처할 수 있다.


  • 다음의 코드는 Person 객체를 가리키는 hong이라는 shared_ptrmake_shared() 함수를 통해 생성하는 것이다.


#include <iostream>
#include <memory>
using namespace std;

class Person {
   private:
    string name_;
    int age_;

   public:
    Person(const string &name, int age);  // 기초 클래스 생성자의 선언
    ~Person() { cout << "소멸자가 호출되었습니다." << endl; }
    void ShowPersonInfo();
};

int main(void) {
    shared_ptr<Person> hong = make_shared<Person>("길동", 29);
    cout << "현재 소유자 수 : " << hong.use_count() << endl;  // 1

    auto han = hong;
    cout << "현재 소유자 수 : " << hong.use_count() << endl;  // 2

    // shared_ptr인 han을 해제함
    han.reset();
    cout << "현재 소유자 수 : " << hong.use_count() << endl;  // 1

    return 0;
}

Person::Person(const string &name, int age)  // 기초 클래스 생성자의 정의
{
    this->name_ = name;
    this->age_ = age;
    cout << "생성자가 호출되었습니다." << endl;
}

void Person::ShowPersonInfo() {
    cout << this->name_ << "의 나이는 " << this->age_ << "살입니다." << endl;
}

// 생성자가 호출되었습니다.
// 현재 소유자 수 : 1
// 현재 소유자 수 : 2
// 현재 소유자 수 : 1
// 소멸자가 호출되었습니다.


(3) weak_ptr

  • weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만, 소유자의 수에는 포함되지 않는 스마트 포인터이다.
  • shared_ptr은 참조 횟수를 기반으로 동작하는 스마트 포인터이다.
  • 만약 서로가 상대방을 가리키는 shared_ptr을 가지고 있다면, 참조 횟수는 절대 0이 되지 않으므로 메모리는 영원히 해제되지 않는다.
  • 이렇게 서로가 상대방을 참조하고 있는 상황을 순환 참조라고 한다.
  • weak_ptr은 바로 이러한 shared_ptr 인스턴스 사이의 순환 참조를 제거하기 위해서 사용된다.

References