Post

C++ - Template

C++

C++ - Template

Template

템플릿이란?

템플릿 (Template) : 코드의 재사용성을 높이고, 함수나 클래스를 일반화할 수 있도록, 컴파일 타임에 데이터 타입을 지정할 수 있게 만드는 기능이다.

클래스 템플릿에 인자를 전단해 실제 코드를 생성하는 것을 클래스 템플릿 인스턴화 라고 한다.

이 때, 탬플릿 클래스와 함수는 호출된 모든 타입에 대해 인스턴화 한다. 때문에 컴파일된 코드는 템플릿 인스턴스의 개수만큼 더 커지며, 컴파일을 하는 시간도 증가한다.

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
templateClass
{
  templateClass<T>();
  ~templateClass<T>();
};

templateClass<int> intTemplate;
templateClass<float> floatTemplate;
templateClass<string> stringTemplate;
// int, float, string 총 3개의 인스턴스 생성

템플릿은 특정 타입에 대해 따로 정의를 내릴 수 있고, 이걸 템플릿 특수화 라고 부른다.

1
2
3
4
5
6
template<>
templateClass<int> //인자를 int로 넣으면 이걸로 실행
{
  templateClass<int>();
  ~temolateClass<int>();
};

템플릿은 클래스 말고도 함수에도 사용이 가능하며, 템플릿 함수는 호출 시 인자를 생략해도 컴파일러가 유추 가능하면 호출이 가능하다.

1
2
3
4
5
6
7
template<typename T>
void Func(T t)
{
  cout << t << endl;
}

Func(1); // <int>생략 가능

이걸 템플릿 타입 추론 이라고 한다.

Type/Value

템플릿 인자에는 타입 말고도 다른 인자를 넣을 수 있다.

1
2
3
4
5
6
7
template<int num>
void Func()
{
  cout << num << endl;
}

Func<1>(); //1을 출력

이렇게 타입 대신 값을 넣을 수 있는데, 이걸 일반화 프로그래밍(generic programming) 이라고 부른다.

인자에 값을 넣으면 매개변수처럼 사용이 가능하다.

이 때, 인자가 같은 타입이라도 값이 다르면 다른 타입으로 보며, 인스턴스가 각자 생성된다.

1
2
Func<1>();
Func<2>(); // 둘은 서로 다른 타입으로 인식한다.

때문에 클래스의 static 변수도 마찬가지로, 공유하지 않는다.

참고로 템플릿 인자도 디폴트를 지정할 수 있다.

1
2
3
4
5
6
7
template<int num = 1>
void Func()
{
  cout << num << endl;
}

Func(); //1을 출력

가변 길이 템플릿

C++ 11부터 가변 길이 템플릿 이 도입되었다.

이름처럼 인자의 갯수를 변경할 수 있는 템플릿이고, typename 키워드 뒤에 ...을 붙이기만 하면 된다.

1
2
3
4
5
template<typename T, typename... Args> //가변 길이 인자를 이용한 벡터 생성 함수
vector<T> GetVector(Args... args)
{
	return vector<T>{args...};
}

typename... 키워드를 템플릿 파라미터 팩(Template parameter pack) 이라고 부른다.

가변 길이 인자를 잘 이용하면 재귀함수처럼 사용도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
template<typename A> //반드시 재귀 종료 함수가 필요하다.
void Print(A a)
{
	cout << a << endl;
}

template<typename A, typename... B> //가변 길이 템플릿 함수
void Print(A a, B... b)
{
	cout << a << " ";
	Print(b...);
}

재귀 함수처럼 사용할 때는 반드시 재귀 종료를 위한 함수가 필요하다.

또한 컴파일러가 함수를 컴파일 할 때, 자신보다 먼저 선언된 함수만 볼 수 있기에 종료 함수는 재귀함수보다 앞에 선언되어야 한다.

하지만 C++17부터 Fold라는 기능이 추가되었고, Fold를 사용하면 재귀 종료 함수를 만들 필요가 없다.

1
2
3
4
5
6
7
template<typename T, typename... Args> //종료 함수 없이 가변 길이 인자 템플릿 재귀
T Sum(Args... args)
{
	return (... + args); //return ((((1 + 4) + 2) + 3) + 10); 와 동일해진다.
}

cout << Sum(1, 4, 2, 3, 10) << endl;

위 예시는 단항 좌측 Fold (Unary left fold) 라고 부른다.

C++ 17에서 지원하는 Fold 형식은 총 4 가지로, 여기서 I는 초기값, E는 요소다.

Fold TypeExpression SyntaxExpanded Form
Unary Right Fold(E op ...)(E₁ op (... op (Eₙ₋₁ op Eₙ)))
Unary Left Fold(... op E)(((E₁ op E₂) op ...) op Eₙ)
Binary Right Fold(E op ... op I)(E₁ op (... op (Eₙ₋₁ op (Eₙ op I))))
Binary Left Fold(I op ... op E)((((I op E₁) op E₂) op ...) op Eₙ)

객체 생성하듯 타입을 생성하기

템플릿은 객체를 생성하지 않고, 타입에 값을 부여할 수 있다.

1
2
3
4
5
6
7
8
template<int num>
struct Int
{
	static const int value = num;
};

using one = Int<1>;
using two = Int<2>;

이렇게 생성한 타입들을 사용하여 템플릿을 통해 연산 결과를 새로운 타입으로 정의할 수 있다.

1
2
3
4
5
template<typename i1, typename i2>
struct Add
{
  using result = Int<i1::value + i2::value>;
};

이 계산은 러닝 타임에 동작하는 것이 아닌, 컴파일 단계에서 계산 된 후 치환되는 것이다.

템플릿 메타 프로그래밍(TMP)

템플릿을 사용하면 객체 생성없이 타입에 값을 부여하고, 연산이 가능하다는 장점을 이용하는 프로그래밍 기법이다.

컴파일 타임에 타입 지정과 연산이 끝나기에 런타임에서 빠른 실행 속도를 가지지만, 디버깅이 복잡하고 가독성이 떨어진다는 단점이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int Factorial(int num) //재귀함수를 이용한 팩토리얼 함수
{
  return n == 1 ? 1 : n * Factorial(n - 1);
}

template<int Num> //템플릿을 이용한 팩토리얼 함수
struct FactorialS
{
	static const int value = Num * FactorialS<Num-1>::value;
};

template<> //종료 함수 겸 특수화
struct FactorialS<1>
{
	static const int value = 1;
};

분모/분자 를 구하는 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
template <int Numerator, int Denominator = 1>
struct Ratio
{
	using type = Ratio<Numerator, Denominator>; //this 처럼 사용 가능
	const static int n = Numerator;
	const static int d = Denominator;
};

template<typename Ratio1, typename Ratio2>
struct Add_Ratio
{
	using type = Ratio<(Ratio1.n * Ratio2.d + Ratio2.n * Ratio1.d), (Ratio1.d * Ratio2.d)>;
};

의존 타입(dependent type)

위에서 템플릿 인자는 타입 또는 값으로 사용할 수 있다고 말했다.

때문에 가끔 모호한 식은 컴파일러에게 오해를 일으킬 수 있다.

예를 들어서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
int func()
{
	T::t* p;
}

class A
{
	const static int t;
}

class B
{
	using t = int;
}

A, B 클래스를 func 에 넣어보자.

A의 경우, t* p는 int와 p의 곱하기 연산이 된다.

하지만 B의 경우 int의 포인터 p를 생성하는 코드로 인식한다.

위처럼 T::t는 인자에 따라 값 또는 타입으로 해석될 수 있고, 인자에 따라 타입이 달라지는 식별자는 의존타입(Dependent Type) 이라 부른다.

반대로 템플릿 인자에 의존하지 않는 타입을 비의존 타입(Non-Dependent Type) 이라고 부른다.

1
2
3
4
5
6
template<typename T>
void func()
{
    int a;       // 비의존 타입
    std::vector<int> vec; // 비의존 타입
}

템플릿 인자가 타입이라는 걸 명시적으로 알리려면 typename을 앞에 붙이면 된다.

기본적으로 컴파일러는 식별자를 보았을 때, 타입이 아닌 값으로 생각하기에 인자가 타입이면 꼭 typename을 붙이는 것이 안전하다.

또한 템플릿 인자 선언 구문에선 오로지 식별자만 사용가능하다.

1
2
3
4
5
template <int N>
struct check_div<N, N / 2> //연산자는 사용 불가능
{
  static const bool result = (N % (N / 2) == 0);
};
This post is licensed under CC BY 4.0 by the author.