Variadic function (가변 인자 함수)
가변 인자 함수는 아래와 같이 사용한다. va_start 로 가변 인자 커서를 만들고 그 커서를 사용해 va_arg 로 값 참조 및 커서 이동을 수행한다. (일반적으로 가변 인자는 단순한 스택 접근으로 구현되어 있다.)void write(int count, ...) { va_list args; va_start(args, count); while (count-- > 0) puts(va_arg(args, const char *)); va_end(args); }인자 순회가 간단한데 반해 받은 가변 인자를 그대로 다른 가변 인자 함수에게 넘길 수 있는 방법은 없다. 하지만 아래처럼 va_list 타입의 인자는 넘길 수 있다.
int vprintf(const char* fmt, va_list arg); void error(const char* fmt, ...) { puts("ERR:"); va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); }때문에 C 표준 가변 인자 함수들은 printf 와 vprintf 와 같이 ... 를 인자로 하는 함수와 va_list 를 인자로 하는 함수 이렇게 두 벌이 제공된다.
가변 인자는 최소 1개 이상의 고정 인자가 필요하다. va_start 에 마지막 고정 인자를 넘겨야 하기 때문이다. va_start 는 이 고정 인자가 스택의 어느 위치에 있는지를 확인 하고 그 다음 부터 가변 인자가 있다고 판단하기 때문에 고정 인자가 필요하다.
가변 인자를 넘겨 받은 함수에서 인자 개수를 알 방법이 없다. 때문에 위 예제처럼 count 를 넘기거나 printf 처럼 포맷 문자열에서 추정하거나 sentinel 값을 사용한다. 하지만 이 방법 모두 올바르게 사용되지 않았을 때 알아낼 방법이 없다. 때문에 종종 버그의 원인이 된다.
write_a(3, "a", "b", "c"); // 인자 개수를 넘김 write_b("%s %s %s", "a", "b", "c"); // 포맷 문자열로 추정 write_c("a", "b", "c", NULL); // Sentinel 값 사용가변 인자 함수는 넘겨 받은 인자의 타입을 알 수 있는 방법이 없다. printf("%s", 1) 과 같이 형식 문자열과 실제 인자가 일치하지 않으면 크래시가 될 수도 있다. 다행히 gcc 는 컴파일 시점에 형식 문자열과 인자가 일치하는지 확인하는 기능이 있다. 컴파일 옵션에 -Wformat 등을 넣으면 이 기능을 사용할 수 있다.
printf("%s", 10); // warning: format '%s' expects argument of ... printf("%d"); // warning: format '%d' expects a matching ... printf("%d", 1, 2); // warning: too many arguments for format ...
C++11 에는 다음과 같이 std::initializer_list를 사용해 가변 인자 함수를 흉내낼 수 있다.
void write(std::initializer_list<const char*> strs) { for (auto s : strs) std::cout << s << std::endl; } write({"a", "b", "c"});이 방법은 인자들 모두 같은 타입을 가져야 하는 제약이 있지만 인자 개수, 타입 제한이 가능해 좀 더 안전하고 편리하게 사용할 수 있다.
Variadic macro (가변 인자 매크로)
C99 이전의 C/C++ 에서는 매크로에서 가변 인자를 사용할 수 있는 방법이 없었다. 때문에 가변인자가 필요한 경우에는 인자 개수별로 매크로를 만드는 방법을 사용했다.#define PRINT_1(fmt,a) printf((fmt), (a)) #define PRINT_2(fmt,a,b) printf((fmt), (a), (b)) #define PRINT_3(fmt,a,b,c) printf((fmt), (a), (b), (c))작성하기도 사용하기도 불편한 문제를 해결하기 위해 C99 에서 가변 인자 매크로가 추가되었다. (C++11 에도 추가되었다)
#define PRINT(fmt,...) printf(fmt, __VA_ARGS__)가변 인자 매크로는 보통 받은 가변 인자를 그대로 넘기는 용도로 사용한다.
#define ERROR(fmt,...) \ puts("ERR:"); printf(fmt, __VA_ARGS__)가변 인자 함수로는 어려웠던 인자 넘기기가 쉽게 된다. 여기서 C99 에서 가변 인자 매크로를 추가한 목적을 알 수 있다.
매크로의 가변 인자의 개수가 0 이 되는 것은 곤란할 수 있다. 위 ERROR 의 경우 printf 가 전개 될 때 , 가 짝이 안맞기 때문인데 이를 해결하기 위해 gcc 는 문법을 확장해 빈 인자일 때 옆 콤마를 제거하는 ## 를 추가했다. vc++ 는 그냥 빈 인자일 때 옆에 있는 콤마를 무조건 제거한다. 둘 다 표준은 아니다.
가변 인자를 포워딩 하는 것은 간단하나 그 인자를 순회하는 것은 간단하지 않다. 우선 인자 개수는 아래와 같이 계산해 낼 수 있다.
#define VA_NUM_ARGS(...) VA_NUM_ARGS_IMPL_((__VA_ARGS__,5,4,3,2,1)) #define VA_NUM_ARGS_IMPL_(tuple) VA_NUM_ARGS_IMPL tuple #define VA_NUM_ARGS_IMPL(_1,_2,_3,_4,_5,N,...) N #define TEST(...) printf("%d\n", VA_NUM_ARGS(__VA_ARGS__)); TEST("a", "b", "c"); // 3개수 세기부터 간단하지 않으니 그 이상이 필요하다면 boost preprocessor 를 사용하자. 아래는 인자의 개수와 두 번째 인자를 얻어내는 예다.
#define TESTB(...) printf("%d %s\n", \ BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), \ BOOST_PP_VARIADIC_ELEM(1, __VA_ARGS__)); TESTB("a", "b", "c"); // 3 b
Variadic template (가변 인자 템플릿)
매크로와 마찬가지로 기존 템플릿도 가변 인자를 받지 못했다. 때문에 가변 인자가 필요한 템플릿의 경우 번거로운 작업이 필요했다.template <typename T1> void print(T1 a) { cout << a << endl; } template <typename T1, typename T2> void print(T1 a, T2 b) { cout << a << endl; cout << b << endl; } //... print(1, "a"); // 1 a반복적인 코드 작업이 번거롭기 때문에 보통 매크로를 사용해 문제를 우회하는데 복잡한 케이스는 아래와 같이 boost.preprocessor 를 사용해 해결할 수 있다.
#define PRINT_BODY(Z,N,_) \ cout << s##N << endl; #define PRINT_FUNC(Z,N,_) \ template<BOOST_PP_ENUM_PARAMS(N, typename T)> \ void print(BOOST_PP_ENUM_BINARY_PARAMS(N, T, s)) { \ BOOST_PP_REPEAT(N, PRINT_BODY, _); \ } BOOST_PP_REPEAT_FROM_TO(1, 10, PRINT_FUNC, 0) print(1, "a"); // 1 a하지만 이런 코드는 읽기에 썩 좋지 않은데이런 어려움을 해결하기 위해 C++11 는 가변 인자 템플릿을 추가했다. 가변 인자 선언은 아래와 같이 한다. (... 위치에 주의한다)
template<typename... Args> void print(Args... args) { //... }위 print 예제를 가변 인자 템플릿으로 구현하면 다음과 같다. 인자 순회를 재귀를 사용해 구현했다. 코드에 있는 args... 는 arg1, arg2, ..., argN 과 같이 확장된다.
void print(const char* s) { cout << s << endl; } template<typename T, typename... Args> void print(T s, Args... args) { cout << s << endl; print(args...); }재귀가 아닌 방식으로는 다음과 같이 구현할 수 있다. 단 아래 코드는 출력이 반대로 된다. (pass 에 넘겨지는 인자의 평가 순서가 오른쪽에서 왼쪽이기 때문이다) 만약 순서가 반대로 되어도 관계 없다면 아래와 같은 형식을 사용해도 좋다. 아래 코드에서 f(args)... 는 f(arg1), f(arg2), ..., f(argN) 과 같이 확장된다.
template<typename... Args> inline void pass(Args&&...) {} template<typename... Args> void print(Args... args) { auto f = [](const char* s) { cout << s << endl; return 1; }; pass( f(args)... ); } print("a", "b", "c"); // c b a가변인자의 개수는 sizeof... 로 간단히 확인할 수 있다.
template<typename... Args> void count(Args... args) { cout << sizeof...(args) << endl; } count("a", "b", "c", "d"); // 4