C++0x 미리보기 7, 가변 인수 템플릿

Twitter icon류광, 2008-05-30 23:05
임의의 개수의 템플릿 인수들을 받는 템플릿을 좀 더 간단하게 구현할 수 있게 됩니다.

(이 글을 쓰는 현재 최신 State of C++ Evolution 문서는 2008/n2565입니다.)

가변 인수 템플릿은 말 그대로 템플릿 인수의 개수가 가변적인(물론 실행 시점이 아니라 컴파일 시점에서) 템플릿을 말합니다. 가변 인수 템플릿이 필요한 이유는 굳이 이야기하지 않겠습니다. TR1의 tuple이나 ‘형식에 안전한’ printf를 생각하면 금방 알 수 있을 것입니다.

현재의 C++ 표준에서 가변 인수 템플릿을 만들려면 상당히 지저분한 과정을 거쳐야 합니다. 이는 Boost(의 일부 라이브러리)의 소스 코드를 들여다보고는 질려버리는 이유 중 하나이기도 합니다. 현재의 표준에서 가변 인수 템플릿을 만드는(또는 흉내내는) 방법에 대해서는 Modern C++ Design이나 C++ Template Metaprogramming 같은 책에 자세히 나와 있으니 역시 생략하고요. 가변 인수 템플릿의 필요성, 중요성에 비해 그 구현이 너무 지저분하고 한계가 많으므로 언어 차원에서 가변 인수 템플릿을 지원하자는 것이 이 제안의 핵심입니다.

개수가 가변적인 템플릿 인수들을 언어 차원에서 지원한다고 할 때 혹시 컴파일 시점에서 형식들의 배열을 훑는 어떤 루프 구조 같은 것(이를테면 for_each_type<Args> ??)이 언어에 새로 추가되는 게 아닌가 생각할 수 있겠는데, 아쉽게도(?) 그런 것은 현재의 C++에서 너무 급격한 변화를 불러올 것입니다. 제안된 가변 인수 템플릿은 그냥 “루프는 재귀로”라는 통상적인 템플릿 메타프로그래밍의 원리를 따릅니다. 각설하고, 간단한 예를 보죠(N2080에서 발췌).

template<typename T, typename... Args>
struct count<T, Args...> {
    static const int value = 1 + count<Args...>::value;
}

template<> // 재귀의 끝
struct count<> { static const int value = 0;}

실행 시점 가변 인수 함수에서처럼 ...라는 표기가 쓰인다는 점을 알 수 있습니다. Args는 키워드가 아니고 그냥 사용자가 정한 식별자입니다. 이제

count<int, char, double>::value

라는 표현식이 주어진 경우, T == <int>, Args... == <char, double>이 됩니다. 따라서 value 우변의 count<Args...>count<char, double>이 되고요. 애초에 주어진 템플릿 인수들(<int, char, double>) 중 첫 번째 인수인 int가 “추출”되었음에 주목해야 합니다. 이러한 과정이 더 이상 인수가 없을 때까지 재귀적으로 반복됩니다. 즉, count<char, double>::value의 인스턴스화에서는 T == <char>, Args... == <double>이 되고, 그 다음 단계에서는 T == <double>, Args... == <>이 됩니다. 이제 count<Args...>::valuecount<>::value이므로 여기서 재귀가 끝납니다. 이처럼, 각 재귀에서 가변 인수 목록의 제일 첫 인수를 추출함으로써 모든 가변 인수를 차례로 훑는다는 것이 가변 인수 템플릿의 핵심입니다.

가변 인수 템플릿 “함수” 역시 비슷한 방식입니다. 다음은 가변 인수 템플릿을 이용해서 “형식에 안전한” printf를 구현한 예입니다(역시 N2080에서 발췌). 필드 너비 지정자 같은 것은 지원하지 않는 단순화된 버전입니다.

void printf(const char* s) {
  while(*s) {
    if (*s == '%' && *++s != '%') 
       throw std::runtime_error(
         "invalid format string: missing arguments");
    std::cout << *s++;
  }
}

template<typename T, typename... Args>
void printf(const char* s, T value, Args... args) {
  while (*s) {
    if (*s == '%' && *++s != '%') {
      std::cout << value;
     return printf(++s, args...);
    }
    std::cout << *s++;
  }
  throw std::runtime_error(
    "extra arguments provided to printf");
}

템플릿 버전에서 args라는 인수는 하나의 인수가 아니라 여러 가지 형식의, 여러 개의 인수들을 의미합니다. printf("%s%d%f!!", string("hello"), 10, 1.0f)의 경우 첫 재귀에서는 T value == const string& "hello"가 되고 Args... arg == int 10, float 1.0f가 됩니다. while 루프에서 "%s"에 도달하면 cout << value에 의해 "hello"가 출력되고, printf("%d%f!!!", 10, 1.0f)가 호출됩니다. 그러면 T value == int 10, Args... arg == float 1.0f이 됩니다. 같은 과정을 거치면 결국 printf("!!!")이 호출되며, 그러면 비템플릿 버전이 호출되어서 재귀가 끝납니다.

이 버전은 서식 지정자(%s, %d 등)와 해당 인수의 형식이 일치하는지(예를 들어 "%s"에 대한 value가 실제로 string인지)를 점검하지는 않습니다. 단지 %?들의 개수와 인수들의 개수가 일치하는지만 점검합니다. 사실 이러한 가변 인수 템플릿에서는 각 인수의 개별적인 서식 지정자들로 지정한다는 게 별로 무의미합니다. 그냥 해당 추가 인수가 ostream에 대한 << 연산을 지원하기만 하면 됩니다. 따라서 예를 들어 fs = "오늘은 %_월 %_일입니다" 같은 서식 문자열에 대해 printf(fs, 5, 31)이라고 호출해도 되고 printf(fs, "오", "삼십일")이라고 호출해도 되는 유연한 printf가 가능합니다. (printf의 구현에 대한 이야기는 단지 예일 뿐, 이 제안이 printf의 구체적인 구현 방식까지 제안하는 것은 아닙니다.)

N2080에는 이 외에도 여러 가지 재미있는 예들이 나와 있습니다. 기본적으로 이러한 가변 인수 템플릿은 최종 응용 프로그래머보다는 기반 라이브러리 작성자에게 큰 도움이 되는 기능이겠지만, 사실 모든 C++ 프로그래머는 잠재적으로 라이브러리 작성자의 본성을 가지고 있으므로(^^) 라이브러리 전문 개발자가 아닌 사람이라도 가변 인수 템플릿을 미리 공부해 두면 좋을 것입니다.

태그: C++ C++0x

comments powered by Disqus

예전 댓글(읽기 전용)