본문 바로가기
C++

C++ 템플릿

by FraisGout 2020. 7. 5.

일반화 프로그래밍(generic programming)

C++이 가지는 프로그래밍 언어로서의 특징 중 하나로 일반화 프로그래밍(generic programming)을 들 수 있습니다.

 

일반화 프로그래밍은 데이터를 중시하는 객체 지향 프로그래밍과는 달리 프로그램의 알고리즘에 그 중점을 둡니다.

 

이러한 일반화 프로그래밍을 지원하는 C++의 대표적인 기능 중 하나가 바로 템플릿(template)입니다.

 

템플릿(template)

템플릿(template)이란 매개변수의 타입에 따라 함수나 클래스를 생성하는 메커니즘을 의미합니다.

 

템플릿은 타입이 매개변수에 의해 표현되므로, 매개변수화 타입(parameterized type)이라고도 불립니다.

 

템플릿을 사용하면 타입마다 별도의 함수나 클래스를 만들지 않고, 여러 타입에서 동작할 수 있는 단 하나의 함수나 클래스를 작성하는 것이 가능합니다.

 

함수 템플릿(function template)

C++에서 함수 템플릿(function template)이란 함수의 일반화된 선언을 의미합니다.

 

함수 템플릿을 사용하면 같은 알고리즘을 기반으로 하면서, 서로 다른 타입에서 동작하는 함수를 한 번에 정의할 수 있습니다.

 

임의의 타입으로 작성된 함수에 특정 타입을 매개변수로 전달하면, C++ 컴파일러는 해당 타입에 맞는 함수를 생성해 줍니다.

 

 

 

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

 

문법

template <typename 타입이름>

 

함수의 원형

 

{

 

// 함수의 본체

 

}

 

 

 

C++98에서 추가된 typename 키워드 이전에는 class 키워드를 사용했습니다.

 

따라서 C++ 컴파일러는 템플릿 정의 내의 typename 키워드와 class 키워드를 같은 것으로 간주합니다.

 

 

 

위에서 정의된 타입 이름은 함수의 원형과 본체에서 임의의 타입으로 사용할 수 있습니다.

 

이렇게 정의된 함수 템플릿을 호출할 때 매개변수로 int형을 전달하면, 함수의 원형과 본체에서 정의된 타입 이름은 모두 int형으로 바뀌게 됩니다.

 

 

 

다음 예제는 여러 타입의 변수의 값을 서로 바꿔주는 Swap() 함수를 함수 템플릿으로 작성한 예제입니다.

 

예제

template <typename T>

 

void Swap(T& a, T& b);

 

 

 

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;

 

}

 

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

 

함수 템플릿의 인스턴스화

함수 템플릿이 각각의 타입에 대해 처음으로 호출될 때, C++ 컴파일러는 해당 타입의 인스턴스를 생성합니다.

 

이렇게 생성된 인스턴스는 해당 타입에 대해 특수화된 템플릿 함수입니다.

 

이 인스턴스는 함수 템플릿에 해당 타입이 사용될 때마다 호출됩니다.

 

명시적 특수화(explicit specialization)

C++의 함수 템플릿은 특정 타입에 대한 명시적 특수화를 제공하여, 해당 타입에 대해 특별한 동작을 정의할 수 있게 해줍니다.

 

컴파일러는 호출된 함수에 정확히 대응하는 특수화된 정의를 발견하면, 더는 템플릿을 찾지 않고 해당 정의를 사용합니다.

 

 

 

앞서 정의된 함수 템플릿 Swapdouble형에 대한 명시적 특수화는 다음과 같습니다.

 

함수 템플릿 원형

template <typename T>

 

void Swap(T& a, T& b);

 

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

 

 

 

double형을 위한 명시적 특수화

template <> void Swap<double>(double&, double&) { ... };

 

 

 

다음 예제는 Swap() 함수 템플릿에서 double형에 대한 동작만을 변경하기 위해 명시적 특수화를 사용한 예제입니다.

 

예제

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

 

{

 

// double형은 값을 서로 바꾸지 않음.

 

}

 

위의 예제에서 Swap() 함수는 double형에 대해서는 더는 값을 서로 바꾸지 않게 됩니다.

 

 

클래스 템플릿(class template)

C++에서 클래스 템플릿(class template)이란 클래스의 일반화된 선언을 의미합니다.

 

앞서 살펴본 함수 템플릿과 동작은 같으며, 그 대상이 함수가 아닌 클래스라는 점만 다릅니다.

 

 

 

클래스 템플릿을 사용하면, 타입에 따라 다르게 동작하는 클래스 집합을 만들 수 있습니다.

 

, 클래스 템플릿에 전달되는 템플릿 인수(template argument)에 따라 별도의 클래스를 만들 수 있게 됩니다.

 

이러한 템플릿 인수는 타입이거나 명시된 타입의 상숫값일 수 있습니다.

 

 

 

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

 

문법

template <typename 타입이름>

 

class 클래스템플릿이름

 

{

 

// 클래스 멤버의 선언

 

}

 

 

 

함수 템플릿과 마찬가지로 템플릿 정의 내에서 typename 키워드 대신에 class 키워드를 사용할 수 있습니다.

 

위에서 정의된 타입 이름은 클래스의 선언에서 임의의 타입으로 사용할 수 있습니다.

 

 

 

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

 

예제

template <typename T>

 

class Data

 

{

 

private:

 

T data_;

 

public:

 

Data(T dt);

 

data(T dt);

 

T get_data();

 

};

 

위처럼 클래스 템플릿으로부터 객체를 생성할 때에는 꺾쇠괄호(<>) 안에 템플릿에 전달된 인수 타입을 명시해야 합니다.

 

전달된 매개변수의 타입을 가지고 컴파일러가 해당 타입에 맞는 함수를 생성해 주는 함수 템플릿과는 달리, 클래스 템플릿은 사용자가 사용하고자 하는 타입을 명시적으로 제공해야 합니다.

 

중첩 클래스 템플릿(nested class template)

C++에서는 클래스나 클래스 템플릿 내에 또 다른 템플릿을 중첩하여 정의할 수 있으며, 이러한 템플릿을 멤버 템플릿(member template)이라고 합니다.

 

멤버 템플릿 중에서도 클래스 템플릿을 중첩 클래스 템플릿(nested class template)이라고 합니다.

 

이러한 중첩 클래스 템플릿은 바깥쪽 클래스의 범위 내에서 클래스 템플릿으로 선언되며, 정의는 바깥쪽 클래스의 범위 내에서뿐만 아니라 범위 밖에서도 가능합니다.

 

 

 

예제

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를 정의하면, 클래스 템플릿의 템플릿 인수와 멤버 템플릿의 템플릿 인수가 둘 다 앞에 명시되어야 합니다.

 

클래스 템플릿의 특징

C++에서 클래스 템플릿은 다음과 같은 특징을 가집니다.

 

 

 

1. 하나 이상의 템플릿 인수를 가지는 클래스 템플릿을 선언할 수 있습니다.

 

예제

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

 

class X

 

{ ... }

 

 

 

2. 클래스 템플릿에 디폴트 템플릿 인수를 명시할 수 있습니다.

 

예제

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

 

class X

 

{ ... }

 

 

 

3. 클래스 템플릿를 기초 클래스로 하여 상속할 수 있습니다.

 

예제

template <typename Type>

 

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

 

{ ... }

 

명시적 특수화(explicit specialization)

클래스 템플릿은 함수 템플릿과 마찬가지로 특정 타입이나 값의 템플릿 인수에 대하여 특수화할 수 있습니다.

 

이렇게 특수화를 명시하면, 해당 타입에 대한 특별한 동작을 정의할 수 있습니다.

 

컴파일러는 전달된 인수에 정확히 대응하는 특수화된 정의를 발견하면, 더는 다른 템플릿을 찾지 않고 해당 정의를 사용합니다.

 

 

 

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

 

double 타입을 위한 명시적 특수화

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

 

 

 

다음 예제는 Data 클래스 템플릿에서 double형에 대한 동작만을 변경하기 위해 명시적 특수화를 사용한 예제입니다.

 

예제

template <> class Data<double> // double형에 대한 명시적 특수화

 

{

 

private:

 

double data_;

 

public:

 

Data(double dt) { data_ = dt; } // 생성자

 

data(double dt) { data_ = dt; }

 

double get_data()

 

{

 

cout << "double형 데이터를 출력합니다!" << endl;

 

return data_;

 

}

 

};

 

부분 특수화(partial specialization)

만약 템플릿 인수가 두 개 이상이고, 그중 일부에 대해서만 특수화를 해야 할 때는 부분 특수화(partial specialization)를 사용할 수 있습니다.

 

부분 특수화 방법은 먼저 template 키워드 다음에 나오는 꺾쇠괄호(<>)에 특수화하지 않는 타입의 템플릿 인수를 명시합니다.

 

그리고서 다음에 나오는 꺾쇠괄호(<>)에 특수화하는 타입을 명시하면 됩니다.

 

 

 

예제

template <typename T1, typename T2>

 

class X

 

{ ... };

 

 

 

위의 예제에서 선언된 클래스 템플릿 Xdouble형에 대해 부분 특수화를 하면 다음과 같습니다.

 

예제

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

 

 

 

위의 예제에 T1 타입까지 특수화하게 되면, 앞서 살펴본 완전한 명시적 특수화가 됩니다.

 

예제

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

 

템플릿을 위한 새로운 이름

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

 

예제

typedef X<double, 3.14> DoubleX;

 

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

 

 

스마트 포인터(smart pointer)

C++ 프로그램에서 new 키워드를 사용하여 동적으로 할당받은 메모리는, 반드시 delete 키워드를 사용하여 해제해야 합니다.

 

C++에서는 메모리 누수(memory leak)로부터 프로그램의 안전성을 보장하기 위해 스마트 포인터를 제공하고 있습니다.

 

스마트 포인터(smart pointer)란 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제해 줍니다.

 

스마트 포인터의 동작

보통 new 키워드를 사용해 기본 포인터(raw pointer)가 실제 메모리를 가리키도록 초기화한 후에, 기본 포인터를 스마트 포인터에 대입하여 사용합니다.

 

이렇게 정의된 스마트 포인터의 수명이 다하면, 소멸자는 delete 키워드를 사용하여 할당된 메모리를 자동으로 해제합니다.

 

따라서 new 키워드가 반환하는 주소값을 스마트 포인터에 대입하면, 따로 메모리를 해제할 필요가 없어집니다.

 

스마트 포인터의 종류

C++11 이전에는 auto_ptr이라는 스마트 포인터를 사용하여 이 작업을 수행하였습니다.

 

하지만 C++11부터는 다음과 같은 새로운 스마트 포인터를 제공하고 있습니다.

 

 

 

1. unique_ptr

 

2. shared_ptr

 

3. weak_ptr

 

 

 

스마트 포인터는 memory 헤더 파일에 정의되어 있습니다.

 

auto_ptrC++11 표준부터 삭제되었습니다.

unique_ptr

unique_ptr은 하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록, 객체에 소유권 개념을 도입한 스마트 포인터입니다.

 

 

 

이 스마트 포인터는 해당 객체의 소유권을 가지고 있을 때만, 소멸자가 해당 객체를 삭제할 수 있습니다.

 

unique_ptr 인스턴스는 move() 멤버 함수를 통해 소유권을 이전할 수는 있지만, 복사할 수는 없습니다.

 

소유권이 이전되면, 이전 unique_ptr 인스턴스는 더는 해당 객체를 소유하지 않게 재설정됩니다.

 

 

 

예제

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

 

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) // 기초 클래스 생성자의 정의

 

{

 

name_ = name;

 

age_ = age;

 

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

 

}

 

 

 

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

 

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

 

make_unique() 함수를 사용하기 위해서는 여러분의 C++ 컴파일러가 C++14를 지원해야 합니다.

shared_ptr

shared_ptr은 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지를 참조하는 스마트 포인터입니다.

 

이렇게 참조하고 있는 스마트 포인터의 개수를 참조 횟수(reference count)라고 합니다.

 

참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하며, 수명이 다할 때마다 1씩 감소합니다.

 

따라서 마지막 shared_ptr의 수명이 다하여, 참조 횟수가 0이 되면 delete 키워드를 사용하여 메모리를 자동으로 해제합니다.

 

 

 

예제

shared_ptr<int> ptr01(new int(5)); // intshared_ptrptr01을 선언하고 초기화함.

 

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() 함수를 통해 생성하는 예제입니다.

 

예제

shared_ptr<Person> hong = make_shared<Person>("길동", 29);

 

cout << "현재 소유자 수 : " << hong.use_count() << endl; // 1

 

auto han = hong;

 

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

 

han.reset(); // shared_ptrhan을 해제함.

 

cout << "현재 소유자 수 : " << hong.use_count() << endl; // 1

 

weak_ptr

weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만, 소유자의 수에는 포함되지 않는 스마트 포인터입니다.

 

 

 

shared_ptr은 참조 횟수(reference count)를 기반으로 동작하는 스마트 포인터입니다.

 

만약 서로가 상대방을 가리키는 shared_ptr를 가지고 있다면, 참조 횟수는 절대 0이 되지 않으므로 메모리는 영원히 해제되지 않습니다.

 

이렇게 서로가 상대방을 참조하고 있는 상황을 순환 참조(circular reference)라고 합니다.

 

weak_ptr은 바로 이러한 shared_ptr 인스턴스 사이의 순환 참조를 제거하기 위해서 사용됩니다.

'C++' 카테고리의 다른 글

C++ STL컨테이너  (0) 2020.07.05
C++ STL반복자  (0) 2020.07.05
C++ OOP다형성  (0) 2020.07.05
C++ OOP상속성  (0) 2020.07.05
C++ OOP캡슐화  (0) 2020.07.05

댓글