17. 템플릿(Template)
1. 키워드
- 템플릿(Template)
- 일반화 프로그래밍(Generic Programming)
- 함수 템플릿(Function Template)
template
과typename
키워드- 클래스 템플릿(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++에서 함수 템플릿은 다음과 같은 문법으로 정의한다.
- 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
타입에 대한 명시적 특수화는 다음과 같다.
- 다음의 코드는
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++에서 클래스 템플릿은 다음과 같은 문법으로 정의할 수 있다.
- 함수 템플릿과 마찬가지로 템플릿 정의 내에서
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++에서 클래스 템플릿은 하나 이상의 템플릿 인수를 가지는 클래스 템플릿을 선언할 수 있다.
- 그리고 클래스 템플릿에 디폴트 템플릿 인수를 명시할 수 있다.
- 마지막으로 클래스 템플릿을 기초 클래스로 하여 상속할 수 있다.
3) 명시적 특수화
- 클래스 템플릿은 함수 템플릿과 마찬가지로 특정 타입이나 값의 템플릿 인수에 대하여 특수화할 수 있다.
- 이렇게 특수화를 명시하면, 해당 타입에 대한 특별한 동작을 정의할 수 있다.
- 컴파일러는 전달된 인수에 정확히 대응하는 특수화된 정의를 발견하면, 더는 다른 템플릿을 찾지 않고 해당 정의를 사용한다.
- 앞서 정의된 클래스 템플릿
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
키워드 다음에 나오는<>
에 특수화하지 않는 타입의 템플릿 인수를 명시한다. - 그리고서 다음에 나오는
<>
에 특수화하는 타입을 명시하면 된다.
- 위의 코드에서 선언된 클래스 템플릿
X
를double
타입에 대해 부분 특수화를 하면 다음과 같다.
- 위의 코드에
T1
타입까지 특수화하게 되면, 앞서 살펴본 완전한 명시적 특수화가 된다.
5) 템플릿을 위한 새로운 이름
- C++11부터는
typedef
키워드를 이용하여 템플릿 특수화를 위한 새로운 이름을 선언할 수 있다.
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_ptr
을make_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_ptr
을make_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
인스턴스 사이의 순환 참조를 제거하기 위해서 사용된다.