본문 바로가기

컴퓨터공학/C, C++

C,C++> 템플릿

함수 템플릿

타임만 다른 함수들

C++는 여러 가지 갭라 방법을 지원하는 멀티 패러다임 언어라고 한다. 

1. 구조적 프로그래밍 :

함수 위주 프로그래밍을 작성할 수 있다.

2. 객체 지향 프로그래밍 :

캡슐화, 추성화를 통해 현실 세계를 모델링할 수 있으며 상속과 다형성을 지원하기 위해 여러 가지 언어 장치를 제공한다. 

3. 일반화 프로그래밍 :

임의 타입에 대해 동작하는 함수나 클래스를 작성할 수 있다. 객체 지향보다 재사용성과 편의성이 더 우수하다. 

 

일반화 프로그래밍은 주로 C++ 템플릿에 의해 지원된다. C++ 표준 라이브러리가 일반화의 좋은 예이다. 

 

복사 후 수정 작업을 컴파일러가 대신 하는 문법 장치가 함수 템플릿이다. 

원하는 함수 모양을 템플릿으로 등록하면 함수를 만드는 나머지 작업은 컴파일러가 한다. 

 

함수 템플릿을 정의할 때 키워드 template 다음에 <> 괄호를 쓰고 괄호 안에 템플릿으로 전달될 인수 목록을 나열한다. 

 

template <typename T>

T Function(T a, T b) {

...

}

 

다음은 템플릿 예시 코드이다. 

template <typename T>
void Swap(T &a, T &b) {
	T t;
    t=a; a=b; b=t;
}

 

템플릿 인수 목록에 키워드 typename 대신 class를 쓸 수 있다. 구형 컴파일러는 이 자리에 class를 사용했었다. 이렇게 되면 클래스 타입만 가능한 것처럼 보여 오해의 소지가 있다. 그래서 typename이라는 키워들 사용하는 것이 좋다. class 키워드는 클래스를 정의할 때만 쓰자. 

 

매크로 함수는 치환될 때마다 코드가 반복되므로 프로그램이 커지는 문제가 있다.

매크로 함수는 전처리기기가 처리하지만 템플릿은 컴파일러가 직접 처리한다. 

전처리기기는 지시대로 소스를 재구성할 뿐이다. 개발자가 필요한 타입에 일일이 매크로를 전개해야 해서 수동이다.

반면 템플릿은 호출만 하면 컴파일러가 알아서 함수를 만드는 자동식이다. 

 

함수는 궁한 대로 매크로 함수를 사용할 수 있지만 클래스를 정의하는 매크로 함수는 만들 수 없다.

함수는 이름이 달라도 타입이 다르면 오버로딩할 수 있지만 클래스는 오버로딩이 안되기 때문이다. 

##연산자를 쓰면 가능하겠지만 타입에 따라 클래스 이름이 매번 달라지므로 쓰기 불편하다. 

 

흔하지 않지만 템플릿 인수 목록에서 두 개이상 타입을 전달받을 수 있다. 이 때 원하는 만큼 typename을 반복하되 각 타입의 이름은 구분할 수 있게 다르게 작성해야 한다. 

 

함수 템플릿 정의는 함수 호출부보다 먼저 나와야 한다. 

 

구체화 

함수 템플릿은 그 자체가 함수인 것은 아니다. 컴파일러는 함수 템플릿 정의문으로 앞으로 만들어질 함수 모양만 기억하며 실제 함수가 호출될 때 타입에 맞는 함수를 작성한다. 함수 템플릿으로 함수를 만드는 과정을 구체화 또는 인스턴스화(Instantiation)이라고 한다. 호출에 의해 구체화되어야 실제 함수가 만들어진다. 존재하는 모든 타입에 함수를 미리 만들어 놓은 것은 아니다. 

이 때 함수 템플릿으로부터 만들어지는 함수를 템플릿 함수라고 한다. 

함수 템플릿은 함수를 만들기 위한 템플릿이고 템플릿 함수는 템플릿으로부터 만들어지는 함수이다. 

 

이미 한 번 말했지만 템플릿으로 함수를 구체화하는 건 사람이 일일이 하는 것과 동일하다. 

호출부 인수를 보고 템플릿에 이 재료를 집어넣어 함수를 찍어내는 것이다. 

사람이 하는 잡다한 일을 컴파일러가 대신하는 것 뿐이다. 

템플릿만 정의하고 함수를 호출하지 않으면 아무런 일이 일어나지 않는다. 

템플릿 자체는 메모리를 소모하지 않는다. 

호출을 하여 템플릿이 구체화되어 실제 함수가 될 때 프로그램 크기가 늘어난다. 

 

 

컴파일러에 의해 구체화된 함수는 실행 파일에 실제로 존재하며 컴파일  단계에서 미리 만들어져서 실행 시 부담이 없다. 함수가 호출될 때 만들어지는 것이 아니다. 대신 매 타입마다 함수들이 새로 만들어진다. 구체화된 만큼 실행 파일 용량이 늘어난다. 템플릿은 크기를 포기한 대신 속도를 얻는 방식이다. 

 

템프릿을 사용하면 반복되는 내용이 통합되어 소스 길이가 짧아지고 수정할 때 템플릿만 수정하면 된다.

관리하기 편하며 임의 타입에 관한 함수를 새로 구체화하는 것은 컴파일러가 알아서 하므로 확장성이 좋다. 

 

명시적 인수 지정

template <typename T> 
T Max(T a, T b) {
	return (a > b) ? a:b;
}

위 예시에서 Max(3, 4)는 두 인수가 정수형이라서 Max(int, int) 함수를 구체화하여 호출할 것이다.

그러나 Max<double>(3, 4)로 호출하면 실인수 3, 4가 정수형 상수지만 산술 변환되어 Max(double, double) 함수가 호출된다. 

 

리턴 타입이나 인수로 직접 사용되지 않는 타입을 가지는 함수를 호출하기 위해서는 명식적으로 템플릿의 인수 타입을 지정해야 한다. 

컴파일러가 함수 호출문만으로 구체화할 함수를 결정할 수 없으므로 어떤 타입의 템플릿 함수를 원하는지 분명히 밝혀야 한다. 

예시를 보며 이해하자.

template <typenameT>
T cast(int s) {
	return (T)s;
}

void func(void){
	T v;
    cin >> v;
    cout << v;
}

void main(){
	unsigned i = cast<unsigned>(1234);
    double d = cast<double>(5678);
    ...
}

예제를 보면 unsigned cast(int)와 double cast(int) 두 버전의 함수가 구체화된다. 

이 함수 이름은 동일하고 인수도 같아서 오버로딩 조건에 맞지 않는다. 

리턴 타입만 다른데 템플릿에 의해 각각 따로 구체화될 수 있지만 호출할 때 어떤 함수를 호출할 지 밝혀야 한다.

예를 들어 cast(1)로 적으면 어떤 함수를 원하는지 알 수 없으므로 에러 처리된다. 

 

두 번째 함수 func는 내부 처리를 위해 T형 지역변수 v를 선언하여 사용한다. 

func은 인수도 리턴값도 없으므로 호출문만 봐서 어떤 함수를 구체화할 지 결정할 수 없다. 

따라서 func()이라고 쓰면 컴파이러는 뭘 원하는지 모를 것이다. 따라서 func<int>() 처럼 v 타입을 명시적으로 전달해야 한다. 

 

명시적 구체화

함수 호출부를 보고 컴파일러가 템플릿 함수를 알아서 만드는 걸 암시적 구체화라고 한다. 

개발자가 원하는 타입으로 함수를 호출하면 나머지는 컴파일러가 알아서 하며 호출하지 않는 타입은 구체화하지 않는다. 

만약 특정 타입 템플릿 함수를 강제로 만들고 싶다면 이때는 명식적 구체화(Explicit Instantiation)이라고 한다. 

template void Swap<float>(float, float);

템플릿이 어떤 모양인지 알아야 컴파일러가 이런 함수를 만들 수 있으므로 명시적 구체화 명령은 템플릿 선언보다 뒤에 와야 한다. 

 

함수가 당장 필요하지 않아도 일단 만들어 놓고 싶다면 명시적 구체화로 강제 생성을 지시할 수 있다. 예를 들어 지금 소스에는 필요하지 않지만 컴파일된 라이브러리를 배포하고 싶다면 명시적 구체화를 할 필요가 있다. 

명시적 구체화로 자주 사용할만한 타입에 관해서 일련의 함수 집합을 미리 생성한다. 

이 라이브러리 사용자는 개발자가 명시적으로 구체화해 놓은 함수만 사용할 수 있을 것이다. 

명시적 구체화는 컴파일 속도에도 긍정적인데 미리 필요한 함수를 생성해 놓으면 컴파일러가 어떤 함수를 생성할 건지 판단하는 시간을 조금 절약할 수 있기 때문이다. 

 

동일한 알고리즘 조건 

사용하는 알고리즘이 달라진다면 함수는 같은 템플릿으로 통합될 수 없다. 

 

임의 타입 지원 조건

함수 템플릿 본체 코드는 임의 타입에 동일하게 동작해야 하므로 타입에 종속적인 코드는 사용할 수 없다. 

예를 들면 printf 함수처럼 타입에 따라 서식을 미리 결정해야 하는 함수는 함수 템플릿에서 쓰지 않는 것이 좋다. 

또는 템플릿 내용 중에 +, -, = 등과 같은 연산자가 있다면 기본 타입은 적용되겠지만 +, -, = 등이 연산자가 오버로딩이 안되어 있는 클래스는 사용할 수 없다. 

클래스에서 = 오버로딩을 하지 않는다면 얕은 복사가 될 것이고 

템플릿 인수로 사용될 클래스는 템플릿 본체가 요구하는 모든 기능을 지원해야 한다. 

 

 

특수화 

특정 타입은 다르게 동작하고 싶으면 특수화(Specialization) 기법을 사용한다. 

컴파일러는 템플릿 함수 호출 구문이 있으면 항상 특수화된 정의에 우선권을 준다. 

특수화 함수를 표기하는 방법은 여러가지 있다. 

1. template <> void Swap<double>(double &a, double &b)

2. template <> void Swap<>(double &a, double &b)

3. template <> void Swap(double &a, double &b)

4. void Swap<double>(double &a, double &b)

5. void Swap<>(double &a, double &b)

6. void Swap(double &a, double &b)

 

클래스 템플릿

타입만 다른 클래스들

클래스 템플릿은 함수 템플릿과 비슷하지만 찍는 대상이 클래스라는 것만 다르다. 

클래스 선언문 앞에 template <typename T>를 붙이고 타입에 종속적인 부분만 T를 사용하면 된다. 

이렇게 정의한 클래스 타입 객체를 생성할 때 클래스 이름 다음 <> 괄호 안에 원하는 타입을 밝혀야 한다.

템플릿으로 만들어지는 클래스를 템플릿 클래스라고 하는데 템플릿 클래스 타입 명에는 <> 괄호가 항상 따라 다닌다. 

int 형이 들어가는 클래스는 PosValue<int>이고 char는 PosValue<char>이다.

생성자를 정의할 때는 <T> 괄호가 있거나 없거나 상관없다. 보통 생성자에서 <T>를 붙이지 않는다. 

 

클래스 템플릿 멤버 함수를 선언문 외부에서 작성할 때 템플릿에 속한 멤버 함수임을 밝히기 위해 소속 클래스 이름에도 <T>를 붙여야 하며  T가 템플릿 인수임을 명시하기 위해 template <typename T>가 먼저 와야 한다. 

클래스 선언문 내부에서 인라인으로 함수를 선언할 때는 클래스 선언문 앞에 T 설명이 있으므로 쓸 필요 없다. 

 

생성자를 호출하기 전 메모리 할당을 하기 위해서 객체 크기를 먼저 계산해야 하므로 클래스 이름에 타입이 적혀 있어야 한다. 

 

함수처럼 클래스 템플릿도 단순 선언에 불과하며 컴파일러는 이 템플릿 모양을 기억했다가 객체 생성 시 클래스 정의를 구체화한다. 

클래스 템플릿 선언만 있고 객체 생성을 하지 않는다면 템플릿은 무시된다. 

 

템플릿 위치

클래스 선언문과 멤버 함수 정의까지 모두 헤더 파일에 작성되어야 한다. 

 

헤더 파일에 클래스 템플릿을 두면 최종 사용자에게 이 클래스 코드를 숨길 수 없는 보안상 문제가 있다.

그래서 cpp 파일에서 export 키워드를 사용하면 구현 파일에서 정의된 멤버함수가 외부로 알려지게 한다. 

템플릿 선언 앞에 export를 붙이면 된다.

export template <typename T>

void PosValue<T>::OutValue() { ... }

하지만 이 기술을 채택하지 않은 컴파일러가 있을 수 있으니 주의하자.

 

비타입 인수

템플릿 인수 목록에 전달되는 건 보통 타입인데 타입이 아닌 상수를 템플릿 인수로 전달할 수 있다.

이를 비타입 인수(Nontype Argument)라고 한다. 

 

실행 중 크기를 결정하기 힘든 상수에 관해서 템플릿과 비타입 인수를 사용할 수 있다. 

예를 들면 배열 크기를 정해서 주는 경우이다. 

동적 할당을 이용하면 필요한 만큼 할당할 수 있고 실행 중 크기를 마음대로 바꿀 수 있어서 신축성이 있다.

그러나 동적할당은 생성자와 파괴자가 필요하고 복사 생성자, 대입 연산자를 정의해야 하고 상속 관계를 고려하여 대부분 멤버 함수는 가상 함수가 되어야 한다.

코드가 져야 할 부담이 많아서 동적 할당 대신 필요한 크기만큼 요소를 가지는 클래스 만들어 쓰면 속도가 빠르고 위험하지 않고 단순해서 좋다. 

 

함수도 비타입 인수를 전달할 수 있다. 비타입 인수는 함수 본체에서만 사용해야 하며 함수 호출문에 템플릿 인수를 명시적으로 지정해야 한다. 

 

디폴트 템플릿 인수

함수 디폴트 인수는 함수 호출 시 인수가 생략되는 경우에 적용되는 기본값을 말한다.

클래스 템플릿에도 이것과 비슷한 개념이 디폴트 템플릿 인수이다. 

객체 선언문에 인수를 생략하면 템플릿 선언문에 지정한 디폴트 타입이 적용된다.

 

클래스 템플릿은 디폴트 인수를 줄 수 있지만 함수 템플릿은 디폴트를 정의할 수 없다. 

 

특수화

특정 타입에 관해서 미리 클래스 선언을 하고 싶다면 명시적 구체화할 수 있다.

template<> class  클래스명<특수타입>

특수화된 클래스 멤버 함수를 외부에서 정의할 때 template<>를 붙이지 않아도 상관없다.

 

특수화를 하면 객체 선언을 하지 않아도 자동으로 구체화된다. 

클래스 정의가 만들어지고 멤버 함수들은 컴파일되어 실행 파일에 포함된다.

따라서 특수화 클래스 정의는 일반적인 템플릿 클래스와 달리 헤더 파일에 작성해서 안되며 구현 파일에 작성해야 한다. 

 

컨테이너

TDArray

컨테이너(Container)란 객체 집합을 다룰 수 있는 객체이다. 

배열이나 연결 리스트같은 것들을 컨테이너라고 한다.

동일 타입(또는 호환되는 타입) 객체를 저장하며 이런 객체들을 관리할 수 있는 기능을 가지는 또 다른 객체이다. 

동적으로 크기를 변경할 수 있는 동적 배열 기능을 캡슐화한 클래스가 있다고 치자.

이 클래스도 컨테이너이다. 하지만 매크로로 배열 요소 타입을 결정해야 한다. 그래고 클래스를 사용하기 전에 원하는 타입으로 바꿔야 하고 int 배열과 double 배열을 동시에 사용할 수 없어서 활용성이 떨어진다. 

이 문제를 해결하는 방법은 템플릿이다. 

필요한 알고리즘만 템플릿에 작성한다.  이후 타입만 바꾸면 이 타입을 요소로가지는 동적 배열클래스를 만드는 작업은 컴파일러가 알아서 할 것이다. 

 

템플릿 중첩

주의사항은 중첩 템플릿 선언문을 쓸 때 다음처럼 작성하면 안된다. 

TStack<PosValue<int>> sPos(10);

닫는 괄호 >가 연속으로 쓰면 >>오른쪽 쉬프트 연산자로 해석하게 되므로 띄어서 써야 한다. 

 

템플릿 클래스 인수 

템플릿은 클래스를 만드는 선언문일 뿐 그 자체가 타입이 될 수 없으며 인수를 밝혀야 타입이 될 수 있다. 

템플릿을 사용하는 프로그래밍 방법을 일반화(Generic) 프로그래밍이라고 한다. 

 

 


모든 언어나 개발툴 중 C가 어렵고 익숙해지는데 시간이 오래 걸린다. 

C 이후 C++나 윈도우즈 프로그래밍, MFC, COM, 네트워크, DB 등 공부하는 사람들의 문제점은 해당 주제를 모르는 것보다 C를 잘 몰라서 어려워하는 경우를 많이 볼 수 있다. 간단한 루프에 익숙하지 못해 제어 구조가 엉망이고 프로그램이 원하는 대로 동작하지 않는 것이다. 조건문이나 연산에 뒤통수를 맞으며 간단한 배열이면 해결할 문제를 클래스 계층을 만드는 사람도 있다. 

C를 제대로 공부하지 않은 사람은 이후 어떤 과목도 공부하기 어렵다. 반면 C를 능수능란하게 다룰 수 있는 사람은 아무리 어려운 과목이라도 수월하게 기술을 습득할 수 있다. C언어의 관심을 버려서는 안되며 시간이 날 때마다 자료구조와 알고리즘에 투자해야 한다. 

 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 타입 정보  (0) 2022.09.02
C,C++> 예외 처리  (0) 2022.09.01
C,C++> 다형성  (0) 2022.08.22
C,C++> 상속  (0) 2022.08.19
C,C++> 연산자 오버로딩  (0) 2022.08.18