공부/C || C++

C++11 가변 인자 함수 템플릿(variadic template)

sudo 2021. 7. 4. 19:55
#define va_start(ap, v)  ( (ap) = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

사실 가변 인자 함수 템플릿이란 용어는 winapi 강의를 들으면서 처음 접해보는 단어였다. 그때는 이렇게 쓰면 되는구나~ 하고 넘어 갔는데 이번 기회에 좀 더 자세히 정리해보고자 한다.

 

사실 가변 인자 함수 템플릿 전에 가변 인자 함수라는 것을 알아보자. 

 

가변 인자 함수는 말 그대로 함수의 인자 갯수가 1개부터 여러개가 될 수 있도록 정의 하는 개념을 의미한다. 

C에서 printf 함수를 예로 많이 든다.

printf("%d\n", 3);
printf("%d, %d\n", 3, 4);
printf("%d, %d, %d\n", 3, 4, 5);
printf("%d, %d, %d, %d\n", 3, 4, 5, 6);

printf에 인자의 수는 1개부터 여러개 모두 가능한 이유는 printf 함수가 가변 인자 함수로 구현되어 있기 때문이다.

 

그렇다면 가변 인자 함수는 어떻게 만들까?

예를 들어서 인자들의 합을 구해주는 가변 인자 함수를 만들어 보자

/* 더할 대상들은 몇개가 될지 모르므로 가변 인수라고 부르고 ...으로 표현
count는 여기서 무조건 주어져야 하는 인수이므로 고정 인수라고 부름 */
int sum(int count, ...) 

그리고 몇가지 매크로들을 알아야 한다. 참고로 이 매크로들은 <cstdarg>헤더파일에 정의되어 있다.

 

1. va_list

찾아보면 char* 를 define 해놓은 것이다. 가변 인자 하나하나를 가리키는 스택 포인터라고 한다. va_start 매크로를 이용해서 맨 처음 가변 인자를 가리키게 만들고, va_arg 매크로를 통해 다음 가변 인자로 넘어갈 수 있다.

 

2. va_start

ap를 첫번째 가변 인자에 대한 포인터로 할당해준다. 매크로 정의는 다음과 같다.

#define va_start(ap, v)  ( (ap) = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

_ADDRESSOF()는 인자로 들어온 변수의 주소를 리턴하고, _INTSIZEOF()는 인자로 들어온 것의 정수형의 크기(32bit, 64bit 환경 모두 4byte)의 배수로 리턴한다. 정수형 크기(4byte)의 배수만큼 더해주는 이유는 32bit에서 스택한칸의 주소 차이가 정수형 크기이기 때문이다. (그런데 64비트에서는 스택 한칸이 4byte가 아니라 8byte라서 정수형 크기의 배수만큼 더해주면 안될 것 같아서 찾아보니 64bit에서는 _INTSIZEOF(v) 대신에 sizeof(_int64)를 사용한다고 한다)

cout << _INTSIZEOF(char) << endl; // 4
cout << _INTSIZEOF(short) << endl; // 4
cout << _INTSIZEOF(int) << endl;  // 4
cout << _INTSIZEOF(long) << endl; // 4
cout << _INTSIZEOF(double) << endl; // 8

 

그렇다면 저 계산으로 어떻게 ap는 어떻게 첫번째 가변인자에 대한 포인터가 되는것일까 생각해보면

가변 인자의 함수의 인자들은 스택에 이미 들어와 있는 상태인데 함수의 인자들은 맨 오른쪽 인자부터 스택에 쌓이고, 가장 왼쪽의 인자는 인자중에 주소값이 가장 낮을 것이고 가장 오른쪽 인자는 가장 높을 것이다. 예를 들어 sum(1, 2, 3)이면 1의 주소값이 가장 낮을 것이고 3의 주소값이 가장 높을 것이다. 따라서 고정인수인 ap의 시작 주소에 고정 인수의 길이만큼 더해주면 첫번째 가변 인수의 시작 주소를 얻을 수 있다.

 

아래 그림을 보면 더 쉽게 이해할 수 있다.

사진 출처:  https://aossuper8.tistory.com/17

3. va_arg

지금 ap가 가리키고 있는 가변 인자를 읽어줌과 동시에 다음 가변인자로 넘어가게 해주는 매크로다.

첫번째 인자로 va_list형인 ap가 들어가고, 두번째 인자로는 가변인자들의 타입이 들어간다

예를 들어서 va_arg(ap, int) 이런식으로 사용된다.

 

4. va_end

va_list 타입인 ap를 NULL로 만들어줌으로써 끝났을 때 관례적으로 써주는 것이다. 안해줘도 크게 문제가 되진 않지만 플랫폼에 따라서 가끔 문제가 되는 경우가 있다고 하니 넣어주는게 좋다.

 

마지막으로 가변인자 함수를 만들기 위한 조건이 몇가지 있다.

 

1. 최소한 1개의 고정인자가 있어야 한다. 이 고정인자의 스택 포인터 기준으로 가변인자의 스택 포인터를 알 수 있기 때문이다.

 

2. 함수 내부에서 고정인자의 갯수를 알 방법이 있어야 한다. 예를 들어 고정 인자로 가변인자 갯수를 알려주던가, 가변 인자 끝에는 0을 써준다는 방식으로 말이다.

 

3. 함수 내부에서 고정인자의 타입을 알 방법이 있어야 한다. printf에서 %d, %s와 같은 포맷팅 문자열로 알려주는 것 처럼. 고정인자 타입을 알아야 va_start같은 매크로에서 고정인자가 스택에서 몇칸을 차지하는지 알 수 있기 때문이다. 32bit 기준 int는 1칸을 차지하니 고정인자 스택 포인터에서 4씩, double은 8씩 더하면서 가변인자의 스택 포인터를 찾을 수 있을 것이다.

 

정수의 합을 구해주는 간단한 가변인자 함수를 만들어 봤다.

int sum(int count, ...)
{
	int res = 0;
	va_list ap;

	va_start(ap, count);

	for (int i = 0; i < count; ++i)
	{
		res += va_arg(ap, int);
	}
	va_end(ap);

	return res;
}

사실 위의 가변인자 함수는 C에서 주로 사용되고 C++11 부터는 가변인자 함수 템플릿이 지원돼서 주로 이걸 사용한다.

 

그렇다면 가변인자 함수 템플릿은 무엇일까?

가변인자 함수처럼 함수(또는 클래스에) 인자의 갯수를 선언시에 미리 정하지 않고, 말그대로 인자 갯수가 변할 수 있는 함수인데 예시를 보면 빨리 이해할 수 있다.

#include <iostream>

using namespace std;

template <typename T>
void myprint(T arg)
{
	cout << arg;
}

template <typename T, typename ... types>
void myprint(T arg, types... args)
{
	cout << arg ;
	myprint(args...);
}

int main()
{
	myprint("m", "y nam", "e is ", "d", "k", "lee");
	return 0;

}

문자열을 출력해주는 함수이다. 

 

여기서 주의해야 할 점

1. ...이 선언시에는 type명(여기선 types) 앞에오고, 함수 인자 적어주는 곳에는 type명 뒤에 온다.

2. 첫번째 myprint는 재귀호출로 치면 leaf node case라서 두번째 myprint보다 먼저 선언해줘야한다.

3. 두번째 myprint 함수 안에서 myprint를 재귀호출 할때 인자 args뒤에 ...을 꼭 붙여줘야 한다(안쓰면
"오류 C3520 'args': 이 컨텍스트에서 매개 변수 팩을 확장해야 합니다" 이런 오류가 뜨더라...)

 

참고로 template<typename T, typename ... types>에 typename뒤에 ...으로 오는 것을 '템플릿 파라미터 팩'이라고 부르며, void myprint(T arg, types... args)에 ...으로 오는것을 '함수 파라미터 팩'이라고 한다.

 

C에서 쓰는 가변인자 함수는 매크로를 써야 했지만 여기선 쓰지 않아도 되니 더 편리하게 쓸 수 있다.

 

추가적으로 유용한 함수가 있는데 sizeof...이다. 원래 sizeof는 인자로 들어온 녀석의 크기를 알려주는데 이것은 다르게 가변인자의 갯수를 알려준다. 가변 인자 템플릿으로 평균을 구하는 예를 살펴보면

#include <iostream>

using namespace std;

template <typename T>
int mysum(T arg)
{
	return arg;
}

template <typename T, typename ... types>
int mysum(T arg, types... args)
{
	return arg + mysum(args...);
}

template <typename... args> /*  mysum처럼 재귀호출을 하는게 아니므로 템플릿에 타입 종류도 하나 */
double myaverage(args... nums) /* 함수 인자 종류도 하나도 하나로 했다 */
{
	return mysum(nums...) / sizeof...(nums);
}

int main()
{
	int res = myaverage(10, 20, 30, 40, 50);
	cout << res << endl;
	return 0;

}

위의 예시에서 sizeof...(nums)은 인자의 총 갯수(5개)를 알려준다.

 

특이하게 sizeof뒤에 ...을 붙여주니 인자에는 ...을 붙이지 않고 그냥 nums만 넘긴다.

 

 

C++17에 Fold 형식으로 가변인자 템플릿 작성을 더 간편하게 만드는 문법이 나왔다고 하는데 공부해보고 다음 포스팅에서 작성해보고 싶다.

 

Reference

https://norux.me/19

 

C언어 가변인자(가변파라미터)를 사용해보자

C언어 가변 인자(가변 파라미터)를 사용해보자 1. 가변인자란 무엇일까? printf 함수를 써보셨나요? 우리는 자연스럽게 printf("%d * %d = %d", 3, 5, 3*5) 라고 쓰고 있습니다. 가만보면 printf라는 함수는 인

norux.me

https://jhnyang.tistory.com/293

 

[C,C++] 가변인자 함수의 사용(va_start, va_arg, va_list등등) 함수에 불특정 여러개의 인자를 넘기고 싶

[C, C++ 프로그래밍 강좌 목차] 안녕하세요~ 양햄찌 주인장입니다. 오늘은 오랜만에 프로그래밍 언어에 관련된 포스팅을 들고왔어요. 오늘의 주제 포스팅을 들어가기 전 'C++의 오버로딩'에 대한

jhnyang.tistory.com

https://aossuper8.tistory.com/17

 

C언어 가변인자

C언어 가변인자 가변 인자란?  - 인수의 개수와 타입이 미리 정해져 있지 않다는 뜻  - 대표적인 함수 : printf  - printf 함수는 전달되는 인수의 개수와 타입이 모두 다르지만 정상적으로 컴파일

aossuper8.tistory.com