가변 파라미터 함수를 구현하는 방법
가변 파라미터(매개변수) 함수를 구현하는 방법은 C스타일과 C++(C++11) 스타일 2개가 존재합니다.
C 스타일은 stdarg.h
을 사용하고 C++ 스타일은 템플릿 가변 파라미터(Template)를 사용하는 것입니다.
C 스타일 가변 파라미터 함수
C스타일의 가변 파라미터 함수는 __cdecl
함수 호출 규약을 사용합니다.
__cdecl
호출 규약은 호출자(caller)가 스택을 정리하는 방법으로, 컴파일 타임에 인자의 수를 피호출자(calee)가 미리 알 수 없기 때문에, 호출시 인자의 수를 계산할 수 있는 호출자(caller)가 스택을 정리합니다.
자세한 내용은 함수 호출 규약 (cdecl, stdcall, thiscall, fastcall) 을 참조해 주시길 바랍니다.
C스타일의 가변 파라미터 함수를 구현하는 방법은, stdarg.h
헤더 파일에 정의된 va_list
, va_start
, va_arg
, va_end
매크로를 사용합니다.
각 매크로의 역할
va_list
가변 인자 목록을 위한 타입입니다.
이 변수는 가변 인자들을 저장하고 관리하는 데 사용됩니다.
va_start
va_start 매크로는 va_list
변수를 초기화하고, 첫 번째 가변 인자의 위치를 설정합니다.
이 매크로는 가변 인자 목록 바로 앞에 위치한 마지막 고정 인자를 기준으로 바로 다음의 인자부터 가변 인자로 간주합니다.
예를 들어, 함수의 시그니처가 func(int lastFixedArg, …)
일 때, va_start(va_list, lastFixedArg);
에서 lastFixedArg
는 가변 인자 목록 바로 앞에 위치한 마지막 고정 인자를 말합니다.
va_arg
va_arg 매크로는 va_list
에서 다음 인자를 가져옵니다. 이 매크로는 두 번째 인자로 가져올 인자의 타입을 받습니다.
예를 들어, va_arg(va_list, int)
는 현재 va_list
에서 가리키고 있는 인자를 int
형으로 가져옵니다.
va_copy
하나의 va_list
변수의 내용을 다른 va_list
변수로 복사합니다.
이는 va_list
변수를 재사용하거나 여러 위치에서 동시에 사용할 필요가 있을 때 유용합니다.
va_end
va_list
변수의 사용을 종료합니다.
이 매크로는 va_list
변수를 정리하고, 필요한 경우 내부적으로 관련 리소스를 해제합니다.
보통 va_start
와 함께 사용되어야 하며, 함수가 리턴하기 전에 호출되어야 합니다.
구현
가변 파라미터 함수의 대표적인 예로는, printf
가 있으며 비슷하게 구현 해보도록 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// std arg를 사용한 전통적인 C 스타일의 가변 파라미터 함수
inline int __cdecl MyPrintf(const char* format, ...)
{
va_list arg; // 가변 인자 목록을 위한 변수
va_start(arg, format); // 가변 인자 목록 초기화
while (*format != '\0')
{
if (*format == '%')
{
format++;
switch (*format)
{
case 'd':
{
int i = va_arg(arg, int); // 가변 인자 목록에서 다음 인자를 int로 가져옵니다.
std::cout << i;
}
break;
case 'c':
{
char c = va_arg(arg, char);
std::cout << c;
}
break;
}
}
else
{
std::cout << *format;
}
format++;
}
va_end(arg); // 가변 인자 목록을 사용한 후에는 va_end를 호출해 인자를 정리 합니다.
return 0;
}
int main()
{
MyPrintf("Hello! %c is %d in ASCII code.", 'A', 'A');
}
이 소스에서 va_list
와 va_start
로 가변 인자 리스트를 초기화 하고, format
이 가리키는 char
가 '\0'
이 될 때까지 반복합니다.
이때, 문자열에서 %
가 나온다면, %
뒤 문자를 보고 d
의 경우에는 int
를, c
의 경우에는 char
를 출력합니다.
만약, %
의 형식이 아니라면 문자열을 그대로 출력합니다.
이후, 모든 문자열을 돌게되면, va_end
로 가변 인자 목록을 정리하고 리턴합니다.
C++ 스타일 템플릿 가변 파라미터(Variadic Templates) 함수
C++11 이상에서는 템플릿(Template)을 사용하여 가변 파라미터 함수를 구현할 수 있습니다. 이 방법은 타입 안전성을 제공하며, template<typename... Args>
구문을 사용합니다.
이때, 이 방법은 특정 함수 호출 규약에 의존할 필요가 없습니다.
함수 템플릿의 경우, 컴파일러는 이 템플릿을 사용하여 모든 함수 오버로드를 생성합니다.
즉, 컴파일 타임에 각 오버로드가 코드 호출에 필요한 고정된 수의 인자를 가지기 때문에 피호출자(calee)도 인자의 크기를 알 수 있습니다.
또한, 템플릿 방식은 각 인자의 타입을 컴파일 시점에 결정할 수 있기 때문에 타입 안전성을 보장합니다.
하지만 모든 함수 오버로드를 생성하기 때문에, 각 인자 조합에 최적화된 코드를 제공하지만, 프로그램의 코드 영역이 커지게 될 수 있습니다.
종료 조건
사실, 가변 파라미터 함수에는 종료 조건이 필요할 수 있습니다.
위에서 본 C 스타일 가변 파라미터 함수는 일반적으로 쓰던 함수 방식으로 쓰면 되기 때문에, 자연스럽게 종료 조건을 생각할 수 있지만…
C++ 스타일 가변 파라미터 함수는 생소할 수도 있기 때문에 종료를 위한 방법 몇가지를 소개시켜 드리려고 합니다.
1. 특정 종료 인자를 사용
가변 파라미터 템플릿을 (T value, Args... args)
방식으로 사용할 때, value
에 특정 종료 인자가 들어오면 종료시키는 방법을 사용할 수 있습니다.
2. 가변 인자가 더 이상 없을 때, 호출하는 종료 함수 만들기
가변 파라미터 템플릿은 인자가 존재할 때, 계속해서 호출될 수 있습니다.
템플릿 가변 파라미터 함수는 컴파일 타임에 필요한 함수 오버로드를 생성한다고 했습니다.
여기에서 가변인자 종료를 위한 함수를 만들지 않으면, 어쩌면, 코드에 따라 무한 재귀 호출이 발생할 수 있습니다.
하지만, 가변 인자가 더 이상 없을 때, 호출되는 함수를 만들어두면, 같은 시그니처 오버로드 함수를 만들지 않으니, 이 함수를 이용해 종료시킬 수 있습니다.
자세한 구현은 아래 내용에서 이어 쓰겠습니다.
구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 가변인자 종료를 위한 함수
int _cdecl MyTPrintf(const char* format)
{
std::cout << format << std::endl;
return 0;
}
// C++11의 템플릿 가변 파라미터 (Variadic Templates) 함수
template<typename T, typename... Args>
int _cdecl MyTPrintf(const char* format, T value, Args... args)
{
while (*format != '\0')
{
if (*format == '%')
{
format++;
switch (*format)
{
case 'd':
std::cout << (int)value;
break;
case 'c':
std::cout << (char)value;
break;
}
return MyTPrintf(++format, args...);
}
else
{
std::cout << *format;
}
format++;
}
return 0;
}
int main()
{
MyTPrintf("Hello! %c is %d in ASCII code.", 'A', 'A');
}
저는 같은 이름의, 가변 인자가 더 이상 없을 때, 호출하는 종료 함수를 만들었습니다.
그리고, 템플릿 가변 파라미터 함수 내에서, 문자열을 반복하며 출력하다가,
문자열에서 %
가 나온다면, %
뒤 문자를 보고 d
의 경우에는 int
를, c
의 경우에는 char
를 출력합니다.
그리고, 이때 마지막에 같은 함수를 재귀 호출하며 return
합니다.
즉, 같은 가변 인자 템플릿 함수를 여러개 만들도록 유도하고, 사용합니다.
정리
C 스타일 가변 파라미터의 경우에는 하나의 함수에서 가변 인자를 처리합니다.
따라서, 더 작은 코드 크기, 런타임에 유연한 인자 처리를 할 수 있지만, 하나의 함수(callee)에서 인자의 개수를 컴파일 타임에 정할 수 없기 때문에, __cdecl
함수 호출 규약을 사용합니다.
반면, 템플릿 가변 파라미터의 경우에는 컴파일 타임에 가변 파라미터 함수의 오버로드를 생성하기 때문에, 타입 안전성, 컴파일 시점의 오류 검출, 각 타입 조합에 최적화된 코드를 생성할 수 있습니다.
하지만, 함수 오버로드가 많아질수록, 프로그램의 크기가 증가하게 됩니다.