[기술 질문] #14_템플릿, Template
C++의 템플릿에 대해 알아보겠습니다.
Overview
- 개념
- 템플릿 변수
- 템플릿 함수
- 템플릿 non-type 파라미터
- 템플릿 클래스
- 이중 템플릿
#0. 개념
1. 정의
- C++의 '템플릿'은 함수나 클래스가 특정한 데이터 형식이 유형에 귀속되지 않고, 일반화되어 여러 데이터 형에 대해 동작할 수 있도록 하는 '제네릭 프로그래밍'을 제공합니다. 따라서, 템플릿은 서로 다른 유형의 클래스 혹은 함수의 인스턴스 생성을 위한 청사진을 제공하며, 컴파일 시점에 그 상세 유형이 결정됩니다.
2. 특징
- [ 일반화 ] : 템플릿은 특정한 데이터 형식에 의존하지 않고 여러 종류의 데이터 형식에 대해 동작할 수 있도록 합니다. 이러한 특징은 사용자로 하여금 코드 작성의 유연성과 재사용성을 높여줍니다.
- [ 템플릿 인스턴스화 ] : 템플릿은 컴파일 시점에 그 상세 유형이 결정됩니다. 이때, 컴파일러는 해당 데이터 형식에 맞는 코드를 생성하여 사용합니다.
- [ 디버깅 ] : 템플릿 사용 시 특정 데이터 유형에 특수화된 코드가 생성되므로, 에러 발생 시에 디버깅이 다소 복잡합니다.
- [ 장점 ] : 템플릿 활용은 코드 작성의 유연성과 유지보수성 향상의 장점이 있습니다.
3. 일반화 프로그래밍(Generic Programming)
- [ 정의 ] : 일반화 프로그래밍은 데이터 형식으로부터 독립적이며, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 일반적이며, 재사용성 높은 코드 작성을 강조하는 프로그래밍 패러다임입니다. 일반화 프로그래밍의 목표는 코드의 재사용성과 유지 보수성 향상입니다.
4. 기본 문법
template<typename T>
or
template<class T>
Details
- 템플릿은 데이터 타입을 파라미터로 전달 받습니다.
- typename 키워드는 어떤 이름이 변수 혹은 함수가 아니라, 템플릿 내 의존 이름이 데이터 유형임을 가리킵니다.
- 주의할 점은 템플릿 클래스 혹은 함수를 실제 사용할 특정 데이터 타입으로 인스턴스화한 결과물로 지칭해야 합니다.
5. 컴파일러의 템플릿 코드 처리
- 컴파일러가 템플릿 정의 코드를 만나면 문법 검사만 수행하고, 실제 컴파일은 수행하지 않습니다.
- 컴파일러는 실제 사용할 특정 데이터 타입으로 정의한(eg. myClass <int>) 템플릿 인스턴스화 코드를 만나면 새로운 정의 코드를 생성합니다. 따라서, 아직 인스턴스화되지 않은 템플릿 코드는 컴파일되지 않습니다.
#1. 템플릿 변수
1. 기본 문법
template<typename T>
constexpr T pi = T(3.14...);
int main()
{
float piToFloat = pi<float>;
double piToDouble = pi<double>;
}
#2. 템플릿 함수
1. 호출
- [ 타입 연역(Type Deduction) ] : 하나는 "타입 연역(Type Deduction)"을 통해 기존의 호출방법을 사용하는 방법입니다. SomeFunc() 방식으로 동작합니다.
- [ 명시적 타입 지정 ] : 다른 하나는 명시적으로 타입을 지정해 템플릿 함수를 호출하는 방법입니다. SomeFunc<int>() 처럼, 명시적으로 타입을 지정해 템플릿 함수를 호출하는 방법입니다.
2. 예제 1
#include <iostream>
#include <vector>
static const size_t NOT_FOUND = (size_t)(-1);
template<typename T>
size_t Find(T& val, T* arr, size_t size)
{
for (size_t i = 0; i < size; i++)
{
if (arr[i] == val)
return i;
}
return NOT_FOUND;
}
int main()
{
size_t arrSize = 5;
int a[arrSize] = { 1, 2, 3, 4, 5 };
int b = 2;
size_t Idx1 = Find<int>( b, a, arrSize );
size_t Idx2 = Find( b, a, arrSize );
cout << Idx1 << endl;
cout << Idx2 << endl;
return 0;
}
3. 예제 2
#include <iostream>
// Template function to find the maximum of two values
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
int main() {
int x = 5, y = 10;
std::cout << "max(" << x << ", " << y << ") = " << max(x, y) << std::endl;
double a = 3.14, b = 2.71;
std::cout << "max(" << a << ", " << b << ") = " << max(a, b) << std::endl;
return 0;
}
4. 특수화
// 원본 템플릿 함수
template<typename T>
size_t Find(T& val, T* arr, size_t size)
{
for (size_t i = 0; i < size; i++)
{
if (arr[i] == val)
return i;
}
return NOT_FOUND;
}
// string에 대한 특수화
template<>
size_t Find<string>(string& val, string* arr, size_t size)
{
for (size_t i = 0; i < size; i++)
{
if (arr[i] == val)
return i;
}
return NOT_FOUND;
}
Details
- 일반 템플릿 함수는 특정 데이터 타입에 대한 특수화 작업을 수행할 수 있습니다.
5. 템플릿 함수 오버로딩
#include <iostream>
#include <vector>
using namespace std;
static constexpr size_t NOT_FOUND = (size_t)(-1);
// 2. Find<T>() 메서드를 오버로딩한 함수
void Find(string& val, vector<string> arr, size_t size)
{
cout << "오버로드 함수" << endl;
}
// 1. 원본 템플릿 함수
template<typename T>
void Find(T& val, vector<T> arr, size_t size)
{
cout << "템플릿 함수" << endl;
}
int main()
{
vector<string> v = { "One", "Two"};
size_t arrSize = v.size();
string findString = "Two";
Find(findString, v, arrSize);
return 0;
}
Details
- 템플릿 함수 또한 일반 함수처럼 오버로딩이 가능합니다.
- 주의할 점은 "타입 연역", 즉 <>를 활용해 특정 데이터 유형을 명시적으로 작성해 템플릿 함수를 호출하지 않을 경우 오버로딩한 함수가 템플릿 함수의 이름을 가릴 수 있습니다.
#3. 템플릿 non-type 파라미터
1. 개념
- [ 정의 ] : 템플릿 코드를 작성하기 위해 우리는 <> 안에 파라미터 목록들을 작성합니다. 템플릿 코드의 파라미터는 원하는 개수만큼 작성이 가능하며, 꼭 데이터 타입이 아니어도 됩니다.
- [ 특징 ] : (1) 메모리 할당 시점 : non-type 파라미터를 받는 템플릿 코드는 컴파일 시점에 파라미터 값에 대한 메모리 할당이 이루어져, 코드 최적화가 가능합니다. (2) 특정 값에 대한 최적화 : non-type 파라미터를 받는 템플릿 코드 작성은 기존의 템플릿 코드의 일반화 특성을 유지하며, 특정 값에 대한 최적화가 컴파일 시점에 이루어져 성능 향상을 기대할 수 있습니다. 왜냐하면, 런 타임에 메모리 할당과 이루어지는 일반 함수의 파라미터 혹은 지역 변수들과 달리, 템플릿 함수의 non-type 파라미터는 컴파일 시점에 상수 값으로 결정되어 메모리 할당이 이루어집니다. 따라서, 런 타임에 발생하는 메모리 할당 작업이 컴파일 시점으로 앞당겨져 런 타임 오버헤드를 방지합니다.
- [ 주의할 점 ] : 서로 다른 non-type 파라미터로 인스턴스화된 서로 다른 두 객체는 동일한 클래스 유형임에도 불구하고, 서로 다른 타입으로 취급됩니다. 예를 들면, MyClass<int, 10, 10>Class 1 과 MyClass<int, 11, 11>Class2 는 동일한 클래스 유형이지만, "10, 10"과 "11, 11" 처럼 서로 다른 non-type 파라미터를 받아 인스턴스화되었기 때문에, 다른 유형으로 취급됩니다. 따라서, 서로 간 복사 생성자 혹은 복사 대입 연산은 불가능합니다.
2. non-type 파라미터
template<typename T, size_t WIDTH, size_t HEIGHT>
class MyClass
{
public:
MyClass();
virtual ~MyClass();
void setElementAt(size_t x, size_t y, const T& inElement);
const T& getElementAt(size_t x, size_t y) const;
// 템플릿 파라미터 목록 중 HEIGHT와 WIDTH를 반환하는 메서드
size_t getHeight() const { return HEIGHT; }
size_t getWidth() const { return WIDTH; }
private:
T cells[WIDTH][HEIGHT];
};
Details
- 템플릿 코드의 <> 안에 작성된 "WIDTH"와 "HEIGHT"는 데이터 유형이 아니지만, 코드는 정상적으로 작동됩니다.
- 이처럼, 클래스의 생성자 혹은 메서드에서 데이터 값을 결정하지 않고, 템플릿 코드의 파라미터로 전달받아 그 크기를 컴파일 타임에 결정할 수 있기 때문에, 최적화의 이점을 갖고 있습니다.
- 주의할 점은 파라미터로 전달받은 서로 다른 "데이터 타입"을 통해 템플릿으로 작성된 원본 클래스 혹은 함수들이 서로 다른 유형으로 취급받는 것처럼, 전달받은 서로 다른 비 데이터 타입의 파라미터들 또한 다른 타입으로 취급됩니다. 예를 들면, MyClass <int, 10, 10>과 MyClass<int, 11, 11>은 서로 다른 타입이 됩니다
3. non-type 파라미터의 디폴트 값
template< typename T = int, size_t WIDTH = 10, size_t HEIGHT = 10 >
class MyClass
{
//...
};
MyClass <> myClass; // 문제 없이 컴파일됩니다!
MyClass<int> myClass2; // 역시, 문제 없습니다!
MyClass<int, 5> myClass3; // 이것 또한 문제없습니다!
Details
- 물론, non-type 파라미터들도 일반 함수와 같이 디폴트 값을 지정할 수 있습니다.
#4. 템플릿 클래스
1. 템플릿 클래스
template<typename T>
class myClass
{
//...
}
Details
- [개념] : C++의 템플릿을 활용해 여러 데이터 타입에서 작동할 수 있는 일반환된 템플릿 클래스를 정의할 수 있습니다.
2. 선언부와 정의부 파일 나누기
// #!. 하나의 헤더파일에 템플릿 클래스의 선언부와 정의부를 모두 작성하는 방법
#pragma once
template<class T>
class TestClass
{
public:
void SetVal(T _val);
T GetVal();
};
template <typename T>
void TestClass<T>::SetVal(T _val)
{
//...
}
//...
Details
- [개념] : 헤더 파일 1 : 클래스 정의와 메서드 정의를 하나의 헤더 파일에 모두 하는 방법
// #2. 헤더파일1(.h)에 클래스의 선언부 작성, 헤더파일2(.hpp/.tpp)에 클래스의 정의부 작성
// TestClass.h 파일
#pragma once
template <class T>
class TestClass
{
private:
T val;
public:
void SetVal(T _val);
T GetVal();
};
#include "TestClass.hpp"
// TestClass.hpp 파일
template<typename T>
void TestClass<T>::SetVal(T _val)
{
val = _val;
}
template<typename T>
T TestClass<T>::GetVal()
{
return val;
}
Details
- [개념] : 헤더 파일1(.h) + 헤더 파일2(.hpp) : 클래스 정의를 하나의 헤더 파일에, 그리고 메서드 정의를 다른 하나의 헤더 파일에 정의하고 클래스 정의부에서 해당 헤더 파일을 #include 합니다.
- [주의할 점] : (1)파일 나누기 : 헤더파일2는 ".tpp" 혹은 ".hpp"로 생성한는 것이 일반적입니다. ".tpp"의 경우 인텔리센스가 작동하지 않아 불편하기 때문에, 헤더파일 형식으로 생성하고 확장명을 ".hpp"로 변경해 활용하는 것이 편합니다. (2) #include : 헤더파일2는 헤더파일1을 #include 하지 않습니다. 따라서, 함수를 정의할 때 "TestClass" 같이 클래스 명이 인식되지 않는 오류가 나오는데, 컴파일하면 정상적으로 동작합니다.
3. 템플릿 클래스 메서드 정의
// #1. 헤더파일1에 선언부와 정의부 모두 작성할 경우
#pragma once
#include <iostream>
#include <vector>
using namespace std;
template<typename T>
class myClass
{
public:
myClass(T a, T b);
public:
T GetA();
T GetB();
private:
T myValA;
T myValB;
};
template<typename T>
inline myClass<T>::myClass(T a, T b)
{
}
template<typename T>
inline T myClass<T>::GetA()
{
return myValA;
}
template<typename T>
inline T myClass<T>::GetB()
{
return myValB;
}
// #2. 헤더파일1(.h)과 헤더파일2(.hpp)로 나눌 경우
// myClass.h 파일 내부
#pragma once
#include <iostream>
#include <vector>
using namespace std;
template<typename T>
class myClass
{
public:
myClass(T a, T b);
public:
T GetA();
T GetB();
private:
T myValA;
T myValB;
};
#include "myClass.hpp"
// myClass.hpp 파일 내부
template<typename T>
myClass<T>::myClass(T a, T b)
{
myValA = a;
myValB = b;
}
template<typename T>
T myClass<T>::GetA()
{
return myValA;
}
template<typename T>
T myClass<T>::GetB()
{
return myValB;
}
Details
- [개념] : 템플릿 클래스의 메서드 정의부는 위에서 설명한 "파일 나누기" 항목을 살펴보면 됩니다. 템플릿 클래스의 메서드를 정의할 때 범위 지정 연산자를 통해 "myClass <T>"처럼, 특정 데이터 타입으로 인스턴스화된 결과물로 지정해야 합니다.
4. 템플릿 인스턴스화 대상 타입 제한
// 헤더파일(.h)
#include <iostream>
#include <vector>
using namespace std;
template<typename T>
class myClass
{
public:
myClass(T a, T b);
public:
T GetA();
T GetB();
private:
T myValA;
T myValB;
};
// 헤더파일2(.hpp)
template<typename T>
myClass<T>::myClass(T a, T b)
{
myValA = a;
myValB = b;
}
template<typename T>
T myClass<T>::GetA()
{
return myValA;
}
template<typename T>
T myClass<T>::GetB()
{
return myValB;
}
template class myClass<int>;
template class myClass<double>;
template class myClass<vector<int>>;
// 메인 문
#include "myClass.h"
int main()
{
myClass<int> classA(1, 2); // 오케이!!
myClass<string> classB("One", "Two"); // 에러!!
return 0;
}
Details
- [개념] : 정의한 템플릿 클래스에 대해 인스턴스화 대상 타입들을 제한할 수 있습니다. 인스턴스화 대상 타입을 제한하기 위해 템플릿 클래스의 메서드를 정의한 소스파일(.hpp)의 마지막에 "template class myClass <대상타입>"을 작성합니다. 헤더파일1(.h)에 인스턴스화 대상 타입 제한 코드를 작성해도 무방합니다.
5. 특수화
// 헤더 파일
template<typename T>
class myClass
{
//...
}
template<>
class myClass<string>
{
//...
}
Details
- [개념] : 템플릿 클래스 특수화는 기존의 템플릿 코드가 특정 데이터 타입에 대해 일반적으로 동작하지 않는 경우, 그 데이터 타입에 대한 별도의 코드를 작성하여 해당 데이터 타입에 대해 템플릿 클래스가 특수한 동작을 하도록 합니다.
- [특징] : 템플릿 클래스 특수화는 일반화된 속성들 혹은 기능들을 유지하며 특정 데이터 타입들에 대해 더욱 최적화된 혹은 특수한 행동들을 구현합니다.
- [장점&단점] : (1) 장점: 템플릿 클래스 특수화의 장점은 사용자 입장에서 해당 버전의 특수화 클래스가 가려져 캡슐화의 장점이 존재합니다. 더불어, 특정 데이터 타입에 대한 최적화가 가능해 성능 향상도 기대할 수 있습니다. (2) 단점 : 템플릿 클래스 특수화 버전은 기존의 일반화 버전보다 선택적 우위를 가지며, 컴파일러에게 혼란을 줄 수 있습니다. 더불어, 클래스 전체를 재작성해야 하는 불편함과 코드의 유지보수성을 해칠 수 있습니다.
- [주의할 점] : 주의할 점은 템플릿 특수화는 어떠한 코드도 상속받지 않기 때문에, 클래스 전체를 재작성해야 합니다.
6. 상속
// 슈퍼 클래스(.h)
template<typename T>
class SuperClass
{
//...
}
// 서브 클래스(.h)
template<typename T>
class SubClass : public SuperClass<T>
{
public:
SubClass( size_t inWidth = SuperClass<T>::DefaultWidth, size_t Height = SuperClass<T>::DefaultHeight)
void Move( size_t srcX, size_t srcY, size_t destX, size_t destY );
};
// 서브 클래스 메서드의 정의부(.cpp)
template<typename T>
SubClass<T>::SubClass(size_t inWidth, size_t inHeight )
{
//...
}
template<typename T>
void SubClass<T>::Move(size_t srcX, size_t srcY, size_t destX, size_t destY)
{
//...
}
Details
- [개념] : 템플릿 클래스 또한 일반 클래스처럼 상속 기능을 제공합니다.
- [특징] : 이때, 템플릿 부모 클래스를 상속하는 자식 클래스는 부모 클래스와 같이 "템플릿" 형식으로 작성해야 합니다.
7. 특수화 vs 상속
- 코드 재사용 : 상속의 경우 코드 재작성이 필요가 없습니다. 하지만, 특수화의 경우 코드를 모두 재작성해야 합니다.
- 이름 재사용 : 상속의 경우 부모 클래스의 이름과 자식 클래스의 이름이 다릅니다. 하지만, 특수화의 경우 부모 클래스의 것과 같습니다.
- 다형성 지원 : 상속의 경우 "is-a" 관계가 성립됩니다. 하지만, 특수화의 경우 같은 클래스라도 서로 다른 타입으로 인스턴스화된 클래스 객체들이 되며 서로 다른 타입으로 취급됩니다.
- 결론 : 다형성의 장점을 활용하기 위해 우리는 템플릿 상속을 사용하며, 특정 데이터 타입에 대한 특수화를 통해 일반화 프로그래밍의 장점과 더불어 캡슐화의 장점을 취하기 위해 템플릿 클래스 특수화를 활용해 볼 수 있겠습니다.
#5. 이중 템플릿
1. 이중 템플릿
- [개념] : C++는 템플릿 클래스의 개별 메서드에 대한 이중 템플릿화를 지원합니다.
- [특징] : 동일한 클래스 유형과 다른 데이터 타입으로 인스턴스화된 서로 다른 두 객체는 복제 생성자 혹은 복제 대입 연산자의 활용이 불가능합니다. 따라서, 우리는 이중 템플릿을 활용해 템플릿 클래스 내 복제 생성자 혹은 대입 연산자를 템플릿 코드로 재정의하여 같은 클래스로부터 생성된 서로 다른 유형의 객체들 간의 대입 연산 혹은 복제 생성을 수행할 수 있게 됩니다!!
- [주의할 점] : (1) 가상 멤버 함수와 소멸자 : 가상 멤버 함수와 소멸자는 템플릿 코드로 작성할 수 없습니다. (2) 문법 : template<typename T, typename E>로 작성하면 안 됩니다!!!
2. 예제
// MyClass.h 파일 내부
template<typename T, size_t WIDTH, size_t HEIGHT>
class MyClass
{
public:
MyClass();
virtual ~MyClass();
// 복제 생성자의 템플릿화
template<typename E>
MyClass(const MyClass<E>& src);
// 대입 연산자의 템플릿화
template<typename E>
MyClass<T>& operator=(const MyClass<E>& rhs);
void setElementAt(size_t x, size_t y, const T& inElement);
const T& getElementAt(size_t x, size_t y) const;
size_t getHeight() const { return HEIGHT; }
size_t getWidth() const { return WIDTH; }
private:
T cells[WIDTH][HEIGHT];
};
#include "MyClass.tpp"
// MyClass.tpp 파일 내부
// template<typename T, typename E> -> XXXX 안됩니다!!
template<typename T>
template<typename E>
MyClass<T>& operator=(const MyClass<E> rhs)
{
//...
}
Details
- 같은 템플릿 클래스로부터 생성된 서로 다른 타입의 객체들 간의 복제 혹은 대입 연산이 가능해졌습니다!
- 정리하자면, 템플릿 클래스는 전체적으로 typename T에 대해 템플릿화 되어있으며, 개별적으로 사용자 정의 복제 생성자와 대입 연산자는 template<typename E>로 템플릿화 하여 같은 템플릿 클래스로부터 서로 다른 데이터 타입으로 인스턴스화된 객체들 간의 복제 혹은 대입 연산이 가능합니다.
'언어 > 기술 질문' 카테고리의 다른 글
[기술 질문]#16_RTTI, 런타임 타입 정보 (0) | 2023.03.22 |
---|---|
[기술 질문]#15_가상 함수 (0) | 2023.03.20 |
[기술 질문]#13_6가지 디폴트 멤버 메서드 (0) | 2023.03.05 |
[기술 질문]#12_함수 포인터, Function Pointer (0) | 2023.02.22 |
[기술 질문]#11_허상 포인터(Dangling Pointer) (2) | 2023.02.18 |