Generic Programming in C++, Part 2
함수 인자를 사용하는 일반화, 그리고 template
함수 인자를 사용하는 일반화 #
accumulate는 함수 인자 binop 덕택에, 적용되는 연산에 독립적으로 수열의 결과치를 얻어낼 수 있는 매우 일반적인 함수가 되었다. 그러나 앞에서 잠시 언급한 바와 같이 accumulate는 여전히 심한 제약이 있다. 다음의 두 가지 문제를 풀어야 하는 경우에 accumulate는 전혀 재사용이 불가능하다.
- 수열의 각 항목을 제곱하여 더하거나 곱하는 경우
- 수열이 1씩 증가하지 않는 경우
즉 accumulate는 반드시 1씩 증가하는 정수를 열거하고 있고 열거된 각 수는 주어진 binop에 있는 그대로 적용될 수 밖에 없다. 그러므로 제어구조가 완전히 동일함에도 불구하고 accumulate는 전혀 재사용될 수가 없는 것이다. 그러므로 우리는 accumulate를 제외한 다른 프로그램을 손대지 않고 일반화하는 프로그래밍 기법이 필요한데 binop의 경우처럼 함수 인자와 default argument를 사용해서 매우 간단하게 해결할 수 있다.
int add(int x, int y) { return x + y; }
int succ(int x) { return add(x, 1); }
int id(int x) { return x; }
int accumulate(int (*binop)(int, int), int idelem, int begin, int end,
int (*step)(int)=succ,
int (*term)(int)=id)
{
int sum = idelem;
for (int next = begin; next <= end; next = succ(next))
{
sum = binop(sum, id(next));
}
return sum;
}
이제는 만일 4씩 증가하는 정수열의 제곱의 곱이나 합을 구하는 함수를 작성해야 한다면 다음과 같이 프로그램하면 된다.
int square(int x) { return x*x; }
int plus4(int x) { return add(x, 4); }
int square_accumulate_by_step4( int (*binop)(int, int), int idelem, int begin, int end) {
return accumulate( binop, idelem, begin, end, plus4, square);
}
그럼 위와 같은 일반화 과정을 거치고서도 재사용성에 저해되는 요소가 있는 가라는 질문에 답해보자. 이미 C++와 상당히 친숙한 독자라면 위의 함수가 가진 치명적인 약점을 이미 처음부터 간파하고 있었을 것이다. 다음과 같은 문제를 여태까지 개발한 accumulate를 가지고 잘 풀 수 있는 가를 따져보는 것으로 충분하다.
double 형으로 1에서 10까지 0.5씩 증가하는 수열의 곱이나 합을 구하는 함수를 작성하라.
Type-casting, type-conversion등은 이 문제를 해결하는 데 아무런 도움이 되지 않는 다. 그리고 일반적으로 C/C++에서 mixed type arithmetic 연산은 프로그램의 이식성이나 안전성에 가장 위험한 요소이므로 아예 생각하지 않는 편이 좋다. 자 그러므로 이제 template이 등장할 때가 되었다.
형과의 독립성 – (Type) Parameteric polymorphism #
여태까지 실컷 개발한 함수가 형이 다르다고 아무런 쓸모가 없다면 너무 슬픈 일이다. 형이 다르다고 제어구조가 달라질 리가 없음에도 언어가 형과 함수내부의 논리적 제어구조가 서로 분리될 수 있도록 지원하지 않는 다면 프로그래머에게는 악몽과 같은 일이 아닐 수 없다. 그러므로 독자가 C 프로그래머라면 필요할 때마다 모든 형을 위하여 동일한 프로그램을 형 이름만 바꾸어 가며 열심히 베끼고 편집하거나 C언어의 표준 라이브러리가 지원하는 quicksort와 같이 void */sizeof 연산자와의 전쟁을 시작해야 하는 도리밖에는 없다.
그러나 다행스럽게도 C++는 template을 지원한다. 주지하다시피 C++의 template이란, 여태 우리가 인자의 수를 늘리고 독립적인 추출해낼 수 있는 함수를 인자로 만든 것 처럼 accumulate와 같이 재사용성이 높은 함수를 특정한 형에만 의존하지 않도록 형 자체를 인자로 만들 수 있도록 하는 기능이다. 또한, C++의 template은 사용시점에 구체적으로 주어진 형에 따라 제약이 심하기는 하지만 어느 정도 수준에서 형을 유추하여 필요한 코드를 묵시적으로 생성해낸다. 그러므로 이러한 기능을 잘 활용하면 기존의 프로그램을 수정할 필요 없이 generic accumulate를 작성할 수 있다. 그리고 그 방법은 너무나 간단하다.
// add.h
…
export
template<typename T> T accumulate(T (*binop)(T, T), T idelem, T begin, T end,
T (*step)(T)=succ,
T (*term)(T)=id);
…
// add.cpp
template<typename T>
T accumulate(T (*binop)(T, T), T idelem, T begin, T end,
T (*step)(T),
T (*term)(T))
{
T sum = idelem;
for (T next = begin; next <= end; next = step(next))
{
sum = binop(sum, term(next));
}
return sum;
}
나머지 코드는 전혀 손댈 필요가 없어야 정상이고 만일 독자가 사용하는 compiler가 위와 같은 변화 후에 알 수 없는 에러 메시지를 내놓는 다면 template을 제대로 지원하지 못하기 때문에 생긴 버그라고 보면 된다. 물론 필자가 사용하는 EGCS의 경우도 “export"ing 기능을 아직 지원하진 않아서 accumulate의 정의를 add.cpp로 분리해 낼 수는 없었다.
export template은 C++98 표준에 있었지만 실제로 구현한 컴파일러가 없었다. 결국 C++11에서 폐기됐다. 지금은 template 정의를 헤더에 두는 것이 관례다. (2026)
다음으로 accumulate 이외에 여태 만들어둔 함수는 정말 가만히 놔두어도 좋은 가를 살펴 보자. 아마도 accumulate같은 문제점을 지적할 수 밖에 없을 것이다. 당장에야 int형만을 위해 사용하는 데는 문제가 없지만 double, float, short, long등의 built-in 형과 사용자 정의형 까지 무수히 많은 적용 가능한 형을 위해서 아무리 작은 함수라 하더라도 깡그리 다시 작성 한다는 것은 그리 유쾌한 일이 아니다. 최소한 여태 우리가 해왔던 작업의 방향과도 일치하지 않는다. 그러므로 모두 generic 함수로 만들어버리자. template
template<typename T>
T add(T x, T y) { return x + y; }
template<typename T>
T succ(T x) { return add(x, 1); }
template<typename T>
T id(T x) { return x; }
template<typename T>
T add_to_end(T end) { return add_from_to(1, end); }
그런데 문제는 add_from_to를 template으로 만들면서 발생한다. 더 이상 accumulate의 step, term을 위한 default argument로 succ/id의 함수 포인터를 사용할 수 없기 때문이다. 즉 이게 바로 C++가 효율성을 위하여 희생한 부분으로 좀 지저분하지만 잔 기술로 해결하는 수밖에 없다. 일단 accumulate의 사전선언부분에서 default arguments를 삭제하고 add_from_to의 정의를 다음처럼 변경하자. 일단은 별 무리가 없는 듯 보인다.
template<typename T>
T add_from_to(T begin, T end) {
return accumulate(add<T>, T(0), begin, end, succ<T>, id<T>);
}
add
template<typename T>
T add_from_to(T begin, T end) {
T (*addT)(T, T) = add<T>;
T (*succT)(T) = succ<T>;
T (*idT)(T) = id<T>;
return accumulate(addT, T(0), begin, end, succT, idT);
}
이제 약간 욕심을 더 부려보자. 예전에 C언어에서 함수형 포인터를 사용할 때 문법 때문에 typedef를 써서 좀 더 코드를 읽기 쉽게 만들어 본 경험이 있을 것이다. 만일 int형 이진 연산자를 빈번히 사용해야 한다면:
typedef int (*intBinOp)(int, int);
intBinOp addint = add<int>;
같이 사용할 수 있었다. 그런데 문제는 template typedef 가 없다는 점이다.
C++11에서 using alias template으로 해결됐다. template<typename T> using BinaryOp = T(*)(T, T); — 아래의 struct 우회 없이 바로 쓸 수 있다. (2026)
즉:
template< typename T> typedef T (*intBinOp)(int, int);
같은 선언이 불가능 하다 (이번 C++ 표준에서 필자가 매우 불만족스러워 하는 것 중에 하나다.). 그러므로 이때는 또 한번 template class를 사용하는 잔 기술을 부리는 수 박에 없다. 어느 쪽이 더 마음에 드는 지는 독자의 판단에 맡기는 바이지만 어쨌든 template typedef 대용으로 사용되는 숙어이므로 알아두면 편리할 때가 있을 것이라고 생각한다.
template<typename T>
struct BinaryOp {
typedef T (*type)(T, T);
};
template<typename T>
struct UnaryOp {
typedef T (*type)(T);
};
template<typename T>
T accumulate(BinaryOp<T>::type binop, T idelem, T begin, T end,
UnaryOp<T>::type step,
UnaryOp<T>::type term) { … }
template<typename T>
T add_from_to(T begin, T end) {
BinaryOp<T>::type addT = add<T>;
UnaryOp<T>::type succT = succ<T>, idT = id<T>;
return accumulate(addT, T(0), begin, end, succT, idT);
}
자 이제 독자들도 필자처럼 더 이상의 일반화가 불가능한가를 생각하는 습관이 들었을 것이라고 생각한다. 이러한 생각을 유도할 때 가장 좋은 것은 이미 작성된 기능과 유사하지만 성질이 약간 다른 특수한 문제를 스스로 생각해보고 개발된 프로그램이 여전히 사용될 수 있는 가를 따져 보는 것이다. 여태까지 개발한 프로그램에서 가장 일반적이고 핵심적인 함수는 accumulate다. Accumulate 그 자체는 template 기능을 사용함에 의해 더 이상의 일반화가 불가능 할 만큼 개선되었다. 그러나, 문제는 accumulate내부에서 처리되는 자료인 수열에 있다. 즉, accumulate가 처리할 수 있는 수열은 A(i+1) = term( step( Ai ) )라는 규칙을 따라 생성되고 있기 때문에 이와 같은 틀에 잘 들어맞지 않는 random sequence, 외부로부터 입력되는 T 형의 원소들, 이미 배열이나 어떤 컨테이너 내부에 들어 있는 원소들을 처리하기 위해 accumulate를 사용하는 것은 불가능하거나 매우 고통스런 작업이 될 것이다. 또 한가지 지나친 제약은 accumulate 내부의 for문에서 next <= end라는 종결조건에도 있다. 이와 같이 for문의 종결조건은 원소간의 엄격한 순서관계에 의존하기 때문에 만일 처리되어야 할 수열이 A(i+1) > A(I)라는 조건을 만족하지 않는 방식으로 생성된다면 무한반복이나 조기종료라는 비정상적인 수행결과를 얻을 수 밖에 없다. 그렇다면 여태까지의 일반화 과정이 무의미한 것인가? 그렇지 않다. 단지 작업이 완결되지 않았기 때문일 뿐이다. 문제는 accumulate 자체의 일반화 과정에 있다고 하기 보다 불완전한 추상화 때문에 발생한 모순이라는 편이 더욱 합당한 분석이라고 할 수 있다.
3부: 데이터 생성과 처리의 분리, 그리고 Iterator로 이어집니다.