다른 사람이 만들어 놓은 '가변인자' 함수를 쓰기만 하다가 직접 작성하면서 모르던 것을 알게 되었습니다.
아래 코드를 보면, 함수 T1, T2 는 제대로 동작하지만 T3 는 args 값이 제대로 들어오지 않습니다.
몇 시간을 삽질하다가 찾아보니 C++ standard 18.7/3 에 이런 구절이 있다는 걸 알게 되었습니다.
The restrictions that ISO C places on the second parameter to the va_start() macro in header are different in this International Standard. The parameter parmN is the identifier of the rightmost parameter in the variable parameter list of the function definition (the one just before the ...). If the parameter parmN is declared with a function, array, or reference type, or with a type that is not compatible with the type that results when passing an argument for which there is no parameter, the behavior is undefined.
그러니까, 아래의 va_start 에 넣어주는 마지막고정인수(parmN) 에 function, array, reference 는 들어올 수 없다고 하네요.
#include
va_list args;
va_start(args, 마지막고정인수);
va_arg(args, 인수타입);
va_end(args);
#include <stdarg.h>
struct Test { int a; };
void T1(int n, ...) {
va_list args;
va_start(args, n);
char* p = va_arg(args, char*);
va_end(args);
}
void T2(Test n, ...) {
va_list args;
va_start(args, n);
char* p = va_arg(args, char*);
va_end(args);
}
void T3(const Test& n, ...) {
va_list args;
va_start(args, n);
char* p = va_arg(args, char*); // p corrupt!!
va_end(args);
}
int _tmain(int argc, _TCHAR* argv[]) {
const Test t;
T1(1, "Test1");
T2(t, "Test2");
T3(t, "Test3");
return 0;
}
그럼 왜 reference 타입은 못 넣느냐...
C 함수의
default argument promotions 인가 integral promotion 인가 하는 문제 때문인듯 한데,
이 부분은 좀 더 확실하게 공부하고나서 정리할까 합니다.
지금은 아래 코드만 올려놓습니다.
struct T { int a; int b; int c; };
void Test(T* pt, T& rt) {
// 00413673 sub esp,0C0h : esp 를 12 빼고 있다.
printf("Test. T ref:%d ptr:%d sizeof(%d, %d)\n", sizeof(rt), sizeof(pt), _INTSIZEOF(T), _INTSIZEOF(T&));
}
int _tmain(int argc, _TCHAR* argv[]) {
//#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
//#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
//#define _crt_va_end(ap) ( ap = (va_list)0 )
// #define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
// #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
// va_start(args, n);
// args = (va_list)_ADDRESSOF(n) + (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1);
char c; char &rc = c; char *pc = &c;
int i; int &ri = i; int *pi = &i;
T t; T &rt = t; T *pt = &t;
// char ref: 1 ptr:4 sizeof(4, 4)
// int ref:4 ptr:4 sizeof(4, 4)
// T ref:12 ptr:4 sizeof(12, 12)
// Test. T ref:12 ptr:4 sizeof(12, 12)
printf("char ref:%d ptr:%d sizeof(%d, %d)\n", sizeof(rc), sizeof(pc), _INTSIZEOF(char), _INTSIZEOF(char&));
printf("int ref:%d ptr:%d sizeof(%d, %d)\n", sizeof(ri), sizeof(pi), _INTSIZEOF(int), _INTSIZEOF(int&));
printf("T ref:%d ptr:%d sizeof(%d, %d)\n", sizeof(rt), sizeof(pt), _INTSIZEOF(T), _INTSIZEOF(T&));
Test(pt, rt);
return 0;
}
여전히 모르는 게 많네요 :)
참고
KLDP Wiki : C언어 가변인자
I am Developer! - 가변인수 va_start(), va_end(), va_arg, va_list
HardCore in Programming - 가변인자를 이용한 함수(va_list)
StackOverflow - Are there gotchas using varargs with reference parameters
Proposed Changes to C++ <stdarg>
..
- 댓글 -
우연히 va_list 관련해서 구글링하다 글을 보게 됐네요. reference를 사용해서는 안된다는 내용은 몰랐던 부분이지만 몇 가지 확인하다보니 이유를 알 것 같아 코멘트 남겨봅니다. 이하 내용은 VS2010 기준입니다.
<stdarg.h> 중...
#define va_start _crt_va_start
<vadefs.h> 중...
#ifdef __cplusplus
#define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
#else
#define _ADDRESSOF(v) ( &(v) )
#endif
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
보시면 아시겠지만 va_start 매크로의 2번째 파라미터(이하 v라고 함)는 해당 스택 프레임의 파라미터 리스트의 가변 인자의 시작 주소를 계산하기 위해 사용됩니다. 그 계산 방법이라는 게 v의 주소값에 대해 v의 _INTSIZEOF 매크로를 통해 얻은 v의 크기를 더함으로써 주소값을 취합니다. 즉, v라는 건 해당 스택 프레임의 파라미터 리스트 내의 특정 위치에 해당하는 변수이어야 하며, 그렇기 때문에 그 뒤쪽의 파라미터의 시작 주소를 얻을 수 있는 것이라고 생각됩니다. 그런데 특정 객체 또는 변수(Primitive type 포함)의 reference를 파라미터로 사용하게 되면 v로 받은 인자의 주소값이 현재 스택 프레임의 가변 파라미터 리스트 바로 앞이 아닌 다른 스택 프레임 내의 주소이거나 혹은 힙일 수 있기 때문에 문제가 생기는 걸로 생각됩니다.
그리고 추가적으로 _ADDRESSOF 매크로에 보면 const char & 로 v를 reinterpret_cast하기 때문에 주소값을 얻는데에 문제가 생길 가능성도 있지 않나 싶습니다. 특정 클래스의 인스턴스를 call by value로 전달함으로써 스택 프레임 안에 위치시킨다 하더라도 virtual table을 갖는 클래스의 인스턴스를 특정 부모 클래스의 타입으로 캐스팅해서 사용했을 경우 캐스팅된 타입에 따라 주소값이 바뀌기 때문입니다. 이는 virtual 함수를 여러 개 포함하는 클래스를 상속한 클래스에서 재차 virtual 함수를 추가하고 나서 최하위 클래스(상속을 가장 많이 받은 클래스)의 포인터를 reinterpret_cast< const char * >로 출력하고, 그 상위 클래스의 포인터로 캐스팅하고 동일하게 reinterpret_cast< const char * >로 출력했을 때 주소값이 다른 것에 기인합니다. 이 주소값 차이는 VS가 취하고 있는 virtual table의 상속 구현 방법으로 인해 발생한다고 알고 있는데, sizeof 연산자에는 영향을 주지 않기 때문에(멤버 변수가 늘어난 게 아니라 virtual 함수만 늘어나면 상위 클래스와 하위 클래스의 주소값이 서로 약간 다른 경우에도 sizeof의 결과는 변하지 않음) 문제가 될 수 있을 듯 합니다.