http://www.hanb.co.kr/network/view.html?bi_id=1567



제공: 한빛 네트워크
저자: 최흥배

함수 템플릿

두 값을 비교하는 함수를 만들어야 됩니다.

앞서 제가 하는 일을 이야기했습니다. 네, 온라인 게임을 만들고 있습니다. 게임에서 구현해야 되는 것에는 캐릭터 간에 HP를 비교하는 것이 필요합니다. 그래서 두 개의 int 타입을 비교하는 Max라는 이름의 함수를 하나 만들었습니다.
int Max( int a, int b );
일을 다 끝낸 후 다음 기획서를 보니 캐릭터와 NPC가 전투를 하는 것을 구현해야 되는데 여기에는 경험치를 비교하는 기능이 필요합니다. 구현해야 되는 것은 위에서 만든 Max 함수와 같습니다. 그래서 그것을 사용하였습니다. 

< List 1 >
#include 
using namespace std;

int Max( int a, int b )
{
  return a > b ? a : b;
}

void main()
{
  int Char1_HP = 300;
  int Char2_HP = 400;
  int MaxCharHP = Max( Char1_HP, Char2_HP );
  cout << "HP 중 가장 큰 값은" << MaxCharHP << "입니다." << endl << endl;
  
  float Char1_Exp = 250.0f;
  float Char2_Exp = 250.57f;
  float MaxCharExp = Max( Char1_Exp, Char2_Exp );
  cout << "경험치 중 가장 큰 값은" << MaxCharExp << "입니다." << endl << endl;
}
앗, 체력(HP)을 저장하는 변수의 타입은 int인데, 경험치를 저장하는 변수의 타입은 int가 아닌 float 타입니다. 

그림1 

당연하게 경험치를 비교하는 부분은 버그가 있습니다.
앞에 만들었던 Max와는 다르게 비교하는 변수의 타입이 float인 것이 필요하여 새로 만들었습니다. 

< List 2 >
float Max( float a, float b )
{
  return a > b ? a : b;
}
함수 오버로딩에 의해 경험치를 비교할 때는 int 타입의 Max가 아닌 의 float 타입을 비교하는 Max가 호출되어 버그가 사라지게 되었습니다. 

이제 경험치 비교는 끝나서 다음 기획서에 있는 것을 구현해야 합니다. 이번에는 돈을 비교하는 것이 있습니다. 그런데 돈을 저장하는 변수의 타입은 __int64입니다. __int64는 비주얼 C++에서만 사용할 수 있는 64비트 정수 타입입니다. __int64 타입을 비교하는 것은 앞에서 만든 int 타입의 Max나 float 타입의 Max로 할 수 없습니다. 함수에서 사용하는 변수의 타입만 다를 뿐 똑같은 것을 또 만들어야 됩니다.
__int64 Max(__int64 a, __int64 b )
{
  return a > b ? a : b;
}
현재까지만 하더라도 이미 똑같은 로직으로 구현된 함수를 3개나 만들었는데, 게임에서 사용하는 캐릭터의 정보는 HP, 경험치, 돈 이외에도 더 많습니다. 저는 앞으로 Max 함수를 몇 개 더 만들어야 할지 모릅니다. Max 함수의 구현을 고쳐야 한다면 모든 Max 함수를 찾아야 합니다. 함수 오버로딩은 문제를 해결하지만, 코드가 커지고 유지보수는 어렵게 만듭니다. 

프로그래밍에서 유지보수는 아주 중요합니다. 왜냐하면, 프로그래밍은 언제나 변경이 가해지기 때문입니다. 유지보수를 편하게 하는 가장 간단한 방법은 유지보수 할 것을 줄이는 것입니다.

Max 함수를 하나로 만들고 싶습니다. 어떻게 해야 될까요?

앗, 혹시 모른다고요? 제가 이 앞에 템플릿에 대해 설명을 할 때 이런 말을 하지 않았나요?
'템플릿을 사용하면 타입에 제약을 받지 않는 로직을 기술 할 수 있습니다'
네, 템플릿을 사용하면 됩니다. 

함수 템플릿 Max를 만들자 

아래의 코드는 템플릿을 사용하여 Max 함수를 구현 한 것입니다. 

< List 3 >
#include 
using namespace std;

// 템플릿으로 만든 값을 비교하는 Max 함수
template 
T Max(T a, T b )
{
  return a > b ? a : b;
}


void main()
{
  int Char1_HP = 300;
  int Char2_HP = 400;
  int MaxCharHP = Max( Char1_HP, Char2_HP );
  cout << "HP 중 가장 큰 값은" << MaxCharHP << "입니다." << endl << endl;
  
  float Char1_Exp = 250.0f;
  float Char2_Exp = 250.57f;
  float MaxCharExp = Max( Char1_Exp, Char2_Exp );
  cout << "경험치 중 가장 큰 값은" << MaxCharExp << "입니다." << endl << endl;
}
실행한 결과는 다음과 같습니다. 

그림2 

네 이번에는 경험치 비교가 정확하게 이루어졌습니다.
템플릿을 사용하게 되어 이제는 불필요한 Max 함수를 만들지 않아도 됩니다.. 

List 3 코드에서 template으로 만든 함수를 '함수 템플릿'이라고 합니다. 

함수 템플릿을 정의하는 방법은 아래와 같습니다. 

그림3 

템플릿을 사용하면 Generic Programming을 할 수 있다
라고 앞서 이야기 했는데 위의 Max 함수 템플릿을 보고 좀 이해를 하셨나요?
혹시나 해서 그림으로 조금만 더 설명하겠습니다. 

그림4 

암소를 총칭(Generic)화하면 동물이라고 할 수 있습니다.
Max 함수 템플릿에서는 함수의 반환 값과 함수 인자인 a 와 b의 타입인 int 나 float를 T로 Generic화 하였습니다. 

그림5

함수 템플릿과 컴파일

하나의 Max 함수 템플릿을 만들었는데 어떻게 int 타입의 Max와 float 타입의 Max를 사용할 수 있을까요? 비밀은 컴파일하는 과정에 있습니다. 컴파일할 때 템플릿으로 만든 것은 템플릿으로 만든 함수를 호출하는 부분에서 평가합니다. 가상 함수처럼 실행시간에 평가하는 것이 아닙니다. 

컴파일을 할 때(compile time) 함수 템플릿을 평가하므로 프로그램의 성능에 해가 되는 것은 없습니다. 

컴파일할 때 평가를 하면서 문법적으로 에러가 없는지 검사합니다. 만약 에러가 있다면 컴파일 에러를 출력합니다. 에러가 없다면 관련 코드를 내부적으로 생성합니다. 

List 3을 예로 들면, void main()의 다음 부분을 컴파일하면 Max를 호출할 때 사용한 인자의 변수의 타입이 Max에서 정의 한 문법에 틀리지 않는지 체크한 후 int 타입을 사용하는 Max 함수의 코드를 만듭니다.
int MaxCharHP = Max( Char1_HP, Char2_HP );
이후 다음 부분에서 Max를 만나면 이번에도 위의 int 때와 같이 문법 체크를 한 후 에러가 없다면 float를 사용하는 Max 함수 코드를 만듭니다.
float MaxCharExp = Max( Char1_Exp, Char2_Exp );
Max가 만들어지는 과정을 나타내면 아래와 같습니다. 모든 타입에 대해 Max 함수를 만드는 것은 아닙니다. 코드에서 사용한 타입에 대해서만 Max 함수가 만들어집니다. 

그림6 

참고로 이렇게 만들어지는 코드는 소스 코드에 만들어지는 것이 아니고 프로그램의 코드 영역에 만들어집니다. 컴파일 타임에 함수 템플릿을 평가하고 관련 코드를 만들기 때문에 템플릿을 많이 사용하면 컴파일 시간이 길어질 수 있으며, 각 타입에 맞는 코드를 만들어내므로 실행 파일의 크기도 커질 수 있습니다.

Max 함수 템플릿에 개선점이 없을까요?

힌트를 드린다면 Max의 두 인자 값은 함수 내부에서 변경되지 않습니다. 그리고 인자의 타입은 C++의 기본형뿐만이 아닌 크기가 큰 타입을 사용할 수도 있습니다. 

생각나셨나요? C++ 기초 공부를 차근차근 쌓아 올린 분이라면 알아차렸으리라 생각합니다. 

정답은 Max 함수 템플릿을 만들 때 템플릿의 인자에 const와 참조를 사용하는 것입니다. Max 함수는 함수의 내부에서 함수의 인자를 변경하지 않습니다. 그러니 함수에 const를 사용하여 내부에서 변경하는 것을 명시적으로 막고 Max 함수를 사용하는 사람에게 알리는 역할을 합니다. 

C++에서 함수 인자의 전달을 빠르게 하는 방법은 참조로 전달하는 것입니다. 위의 Max 함수는 int나 float 같은 크기가 작은 타입을 사용하였기 때문에 참조로 전달하는 것이 큰 의미는 없지만, 만약 구조체나 클래스로 만들어진 크기가 큰 변수를 사용할 때는 참조로 전달하는 것이 훨씬 빠릅니다. 앞에 만든 Max 함수 템플릿을 const와 참조를 사용하는 것으로 바꾸어 보았습니다. 

< List 4 >
template 
const T& Max(const T& a, const T& b )
{
  return a > b ? a : b;
}

class T 라는 것을 본적이 있나요?

함수 템플릿을 만들 때 'typename'을 사용했습니다. 그러나 좀 오래된 C++ 책에서 템플릿에 대한 글을 본 적이 있는 분은 'class'를 사용한 것도 본 적이 있을 것입니다. 

< List 5 >
template 
const T& Max(const T& a, const T& b )
{
  return a > b ? a : b;
}
typename과 class는 기능적으로 다른 것이 아닙니다. 템플릿이 표준이 되기 전에는 'class'를 사용했습니다. 그래서 표준화 이전이나 조금 지난 뒤에 나온 책에서는 'class'로 표기했습니다. 그리고 예전에 만들어진 C++ 컴파일러도 템플릿 인자 선언으로 'class'만 지원했습니다. 만약, C++ 표준화 전후에 만들어진 컴파일러에서는 'class'를 사용해야 합니다. 

현재의 컴파일러에서는 'class', 'typename' 둘 다 지원합니다. 하지만, 'class'보다 프로그래머에게 '타입'을 추상화한 것이라는 의미 전달을 명확하게 하는 typename을 사용합니다. class만 지원하는 오래된 C++ 컴파일러에서 컴파일 해야 하는 것이 아니면 꼭 'typename'을 사용하세요.

이제 Max 함수 템플릿에는 문제가 없을까요?

위에서 Max 함수 템플릿에 대해서 const와 참조로 개선을 했는데 이제 문제가 없을까요? 

그럼 아래의 코드는 문제가 없이 컴파일이 잘 될까요? 

< List 6 >
// List3의 Max 함수 템플릿을 사용합니다.
void main()
{
  int Char1_MP = 300;
  double Char1_SP = 400.25;
  double MaxValue1 = Max( Char1_MP, Char1_SP );
  cout << "MP와 SP 중 가장 큰값은" << MaxValue1 << "입니다." << endl << endl;
  
  double MaxValue2 = Max( Char1_SP, Char1_MP );
  cout << "MP와 SP 중 가장 큰값은" << MaxValue2 << "입니다." << endl << endl;
}
List 6을 컴파일 하면 다음과 같은 에러가 출력됩니다.
max.cpp
max.cpp(16) : error C2782: 'const T &Max(const T &,const T &)' 
        : 템플릿 매개 변수 'T'이(가) 모호합니다.
        max.cpp(6) : 'Max' 선언을 참조하십시오.
        'double'일 수 있습니다.
        또는      'int'
max.cpp(19) : error C2782: 'const T &Max(const T &,const T &)' 
        : 템플릿 매개 변수 'T'이(가) 모호합니다.
        max.cpp(6) : 'Max' 선언을 참조하십시오.
        'int'일 수 있습니다.
        또는      'double'
이유는 컴파일러는 사람이 아니어서 서로 다른 타입의 인자가 들어오면 템플릿의 파라메터 T를 사용한 함수의 인자 a와 b의 타입을 int로 해야 할지, double로 해야 할지 판단할 수가 없기 때문입니다. 이 문제는 어떻게 해결 해야 될까요?

typename을 하나가 아닌 복수 개 사용하면 됩니다.

위의 문제는 Max 함수를 정의할 때 typename을 하나만 사용해서 타입을 하나만 선언했습니다. 이제 typename을 여러 개 사용하면 위의 문제를 풀 수 있습니다. 

< List 7 >
template 
const T1& Max(const T1& a, const T2& b )
{
  return a > b ? a : b;
}
List 7의 함수 템플릿을 사용하면 Max 함수의 인자 타입을 int와 double 혹은 double과 int 타입을 사용해도 컴파일이 잘 됩니다. 그럼 제대로 실행 되는지 실행을 해 볼까요? 

그림7 

앗, 실행 결과에 오류가 있습니다.
int Char1_MP = 300;
double Char1_SP = 400.25;
double MaxValue1 = Max( Char1_MP, Char1_SP );
이 코드는 300과 400.25를 비교합니다. 결과는 400.25가 나와야 하는데 400이 나와버렸습니다. 

이유는 List 7의 함수 템플릿의 반환 값으로 T1을 선언했기 때문에 int 타입과 double 타입을 순서대로 함수 인자에 사용하면 반환 값의 타입이 int형으로 되어 버리기 때문입니다. 이렇게 서로 다른 타입을 사용하는 경우에는 반환 값을 아주 조심해야 합니다. 그리고 위의 예에서는 함수 템플릿의 파라메터로 typename을 2개 사용했지만 그 이상도 사용할 수 있습니다. 

위의 Max 함수 템플릿 만족스럽나요?
저는 왠지 아직도 좀 불 만족스럽습니다.
Max( int, double);
실수를 하면 찾기 힘든 버그가 발생할 확률이 높습니다.
이것을 어떻게 풀어야 될까요?

함수 템플릿의 전문화 라는 것이 있습니다.

Max(int, double)을 사용하면 Max 함수 템플릿이 아닌 이것에 맞는, 특별하게 만든 함수를 사용하도록 합니다. 함수 템플릿의 전문화(Specialization)라는 특별한 상황에 맞는 함수를 만들면 함수 오버로드와 같이 컴파일러가 상황에 맞는 함수를 선택하도록 합니다. 

< List 8 >
#include 
using namespace std;

// 템플릿으로만든값을비교하는Max 함수
template 
const T1& Max(const T1& a, const T2& b )
{
  cout << "Max(const T& a, const T& b) 템플릿 버전 사용" << endl;
  return a > b ? a : b;
}

// 전문화시킨Max 함수
template <> 
const double& Max(const double& a, const double& b)
{
  cout << "Max(const double& a, const double& b) 전문화 버전 사용" << endl;
  return a > b ? a : b;
}

void main()
{
  double Char1_MP = 300;
  double Char1_SP = 400.25;
  double MaxValue1 = Max( Char1_MP, Char1_SP );
  cout << "MP와 SP 중 가장 큰 값은" << MaxValue1 << "입니다." << endl << endl;
  
  int Char2_MP = 300;
  double Char2_SP = 400.25; 
  double MaxValue2 = Max( Char2_MP, Char2_SP );
  cout << "MP와 SP 중 가장 큰 값은" << MaxValue2 << "입니다." << endl << endl;
}
위 코드를 실행한 결과는 아래와 같습니다. 

그림8 

컴파일러는 프로그래머의 생각을 완전히 이해하지는 않습니다. 그래서 컴파일러가 어떠한 것을 선택할지 이해하고 있어야 됩니다. List 8은 double에 전문화 된 Max 함수를 만든 예입니다. 

[질문] Max(10.1, 20.4)를 호출한다면 Max(T, T)가 호출 될까요? 아님 Max(double, double)가 호출 될까요? 

답을 빨리 알고 싶을 테니 뜸 들이지 않고 결과를 바로 보여드리겠습니다. 

그림9 

전문화 버전이 호출 되었습니다. 이유는 호출 순서에 규칙이 있기 때문입니다(최선에서 최악으로). 호출 순서는 다음과 같습니다.
  1. 전문화된 함수와 맞는지 검사한다.
  2. 템플릿 함수와 맞는지 검사한다.
  3. 일반 함수와 맞는지 검사한다.
위의 순서를 잘 기억하고 전문화 함수를 만들어야 합니다. 잘못하면 찾기 힘든 버그를 만들 수가 있습니다. 이제 함수 템플릿에 대한 이야기는 거의 다 끝난 것 같습니다. 

아... 하나 더 있습니다. 

이때까지 한 것들은 타입만을 템플릿 파라메터로 사용했는데 꼭 타입만 함수 템플릿에 사용할 수 있는 것은 아닙니다.

난-타입(non-type) 함수 템플릿

온라인 게임에서는 특정한 이벤트가 있을 때는 캐릭터의 HP, 경험치, 돈을 이벤트 기념으로 주는 경우가 있습니다. HP와 경험치, 돈의 타입은 다르지만 추가 되는 값은 int 상수로 정해져 있습니다. 위와 같이 타입은 다르지만 상수를 더 한 값을 얻는 함수를 만들려면 어떻게 해야 될까요? 

이런 문제도 함수 템플릿으로 해결할 수 있습니다. 

함수 템플릿의 파라메터로 꼭 typename만이 아닌 값을 파라메터로 사용할 수도 있습니다. 

아래의 코드는 캐릭터의 HP, 경험치, 돈을 이벤트에서 정해진 값만큼 더 해주는 것을 보여줍니다. 

< List 9 >
#include <iostream>
using namespace std;

// 지정된 값만큼 더해준다.
template <typename T, int VAL>
T AddValue( T const& CurValue)
{
  return CurValue + VAL;
}

const int EVENT_ADD_HP_VALUE  = 50;    // 이벤트에 의해 추가 될 HP 값
const int EVENT_ADD_EXP_VALUE  = 30;    // 이벤트에 의해 추가 될 경험치
const int EVENT_ADD_MONEY_VALUE  = 10000;    // 이벤트에 의해 추가 될 돈

void main()
{
  int Char_HP = 250;
  cout << Char_HP <<"에서 이벤트에 의해" << AddValue<int, 
       EVENT_ADD_HP_VALUE>(Char_HP) << " 로 변경" <<endl;

  float Char_EXP = 378.89f;
  cout << Char_EXP <<"에서 이벤트에 의해" << AddValue<float, 
       EVENT_ADD_EXP_VALUE>(Char_EXP) << " 로 변경" <<endl;

  __int64 Char_MONEY = 34567890;
  cout << Char_MONEY <<"에서 이벤트에 의해" << AddValue<__int64,   
       EVENT_ADD_MONEY_VALUE>(Char_MONEY) << " 로 변경" <<endl;
}
실행 결과는 다음과 같습니다. 

그림10 

앞에서 사용했던 함수 템플릿 사용 방법과 좀 틀려서 생소할 수도 있겠네요. 

제가 위에 예로든 것은 난 타입 함수 템플릿을 사용해야 되는 당위성이 좀 떨어질 수도 있다고 생각합니다만 설명을 위해서 간단하게 예를 보여주기 위해서 라고 변명해 봅니다. ^^;; 

난 타입을 사용하는 템플릿은 다음 회에 이야기 할 클래스 템플릿에서도 또 다시 이야기 할 예정이니 잘 기억하고 있으시기를 바랍니다. 또 난 타입을 잘 사용하면 템플릿 메타 프로그래밍을 할 때 큰 도움이 됩니다. 템플릿 메타 프로그래밍에 대해서는 다음에 설명해 드리겠습니다.

반응형

+ Recent posts