C++0x 미리보기 7, 가변 인수 템플릿
임의의 개수의 템플릿 인수들을 받는 템플릿을 좀 더 간단하게 구현할 수 있게 됩니다.
- 제안의 명칭: Variadic Templates
- 제안자: D. Gregor, J. J?rvi, G. Powell
- 관련 문서: N1483, N1603, N1704, N2080, N2152, N2191, N2242,
(이 글을 쓰는 현재 최신 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...>::value
는 count<>::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++ 프로그래머는 잠재적으로 라이브러리 작성자의 본성을 가지고 있으므로(^^) 라이브러리 전문 개발자가 아닌 사람이라도 가변 인수 템플릿을 미리 공부해 두면 좋을 것입니다.
예전 댓글(읽기 전용)
-
혹시 언어의 기능들 사이의 직교성에 대한 언급인가요? 흥미로운 주제인데 좀 더 논의해 보았으면 합니다...
그나저나 이번 달 안에 한 꼭지 더 써야 그나마 월간 연재라고 우길 수 있을텐데 간단한 항목 하나 골라서 후딱 써야겠습니다...
-
AI Programming Wisdom 시리즈 잘 읽고 있습니다. :) 그나저나 "std::runtime error" - 밑줄(underscore) 하나가 증발한 거 아닌지...
-
cppig1995님 고맙습니다~ 오타 맞습니다. 고칠게요~
-
좋은 글 잘 읽고 있습니다. 그런데... Concept 에 대한 설명은 안 해주시나요 ? ^^ 이번에 도입된 상당히 큰 변화가 아닐까 싶은데, 류광님 글을 통해서 더 자세히 알고 싶네요.
-
안녕하세요~ 김윤수님의 C++ 이야기 잘 읽고 있습니다. Concept는 글이 엄청 길어질 것 같아서 못 건드리고 있습니다 ㅠ.ㅠ rvalue ref나 lambda도 마찬가지이고요. 사실 지금까지 주로 간단하게 설명할 수 있는 것들만 이야기해왔는데 앞으로도 당분간은 그럴 것 같습니다. 다음 편 주제로는 위임 생성자를 생각하고 있습니다....
-
NDos, 2012-10-14 14:10 :
그런데 클래스의 생성자를 가변 인수 템플릿으로 구현하려면 어떻게 해야 되죠? 생성자가 호출될 때마다 멤버들이 초기화되버리니까 재귀 호출을 할 수 없는 것 아닌가요?
-
일단 드는 생각은.... 가변 인수 템플릿의 인수들은 형식들이고, 멤버 초기화는 형식들이 모두 결정된 뒤에 일어날테니 괜찮지 않을까요?
들어가면 꽤 흥미롭다고 느껴진 것들이 하나씩 제안되어 좋긴 한데, 점점, 언어의 연합체에서 하나의 국가가 되어가고 있다는 느낌이 드네요. ^^