2 분 소요

C/C++에는 함수의 인자를 가변으로 다룰 수 있는 기능이 있는데, 이때 사용하는 것이 va_list, va_start, va_arg, va_end다. 여기서 va_list는 타입이고 나머지 셋은 매크로다. 함수가 아니다.

이 가변인자를 문자열 형태로 사용하려고 하면 문제가 많아서, 테스트하는 코드를 만들었다.

2026년 수정 안내 원래 실었던 코드는 그대로는 컴파일되지 않았고(A123 등이 문자열 리터럴이 아닌 미정의 식별자였다), sentinel로 쓴 NULL에도 이식성 문제가 있었다. 코드를 바로잡고, C++이라면 애초에 쓰지 않는 편이 나은 이유를 덧붙인다.

C 스타일 가변인자

#include <cstdarg>
#include <cstdio>

void TestImpl( const char* first, ... )
{
    va_list ap;
    va_start( ap, first );

    for ( const char* p = first; p != nullptr; p = va_arg( ap, const char* ) )
    {
        printf( "code : %s\n", p );
    }

    va_end( ap );
}

// 호출부에서 마지막에 종료 표시를 자동으로 붙여준다.
#define Test( ... ) TestImpl( __VA_ARGS__, static_cast<const char*>( nullptr ) )

int main()
{
    Test( "A123", "A234", "A345" );   // 반드시 문자열 리터럴이어야 한다
    return 0;
}

알아둘 점 세 가지

1. std::string...으로 넘길 수 없다.

가변인자로는 trivially copyable하지 않은 타입을 넘길 수 없다. std::string은 생성자/소멸자를 가진 클래스라 여기에 해당하며, 넘기더라도 정의되지 않은 동작이다. 그래서 const char*로 받아야 하고, 호출부에 std::string이 있다면 .c_str()을 붙여야 한다.

2. 종료 표시로 NULL을 그냥 쓰면 안 된다.

NULL은 구현에 따라 0(int)으로 확장될 수 있다. 그러면 64비트 환경에서 4바이트만 스택에 올라가는데 va_arg는 8바이트 포인터로 읽으므로 값이 깨진다. nullptr을 쓰거나, C라면 (const char*)NULL처럼 명시적으로 캐스팅해야 한다.

3. 개수를 알 방법이 없다.

가변인자는 몇 개가 넘어왔는지 스스로 알 수 없다. 그래서 위처럼 마지막에 sentinel을 붙이거나, printf처럼 첫 인자에 개수/포맷을 넘겨야 한다. 매크로로 sentinel을 자동으로 붙인 이유가 이것이다.

C++이라면 가변 템플릿을 쓰자

위 방식은 타입 안전성이 전혀 없다. 인자를 하나라도 잘못 넘기면 컴파일은 되고 런타임에 깨진다. C++11 이후라면 가변 템플릿(variadic template) 을 쓰는 편이 모든 면에서 낫다.

#include <iostream>
#include <string>

template<typename... Args>
void Test( const Args&... args )
{
    ( ( std::cout << "code : " << args << "\n" ), ... );   // C++17 폴드 표현식
}

int main()
{
    std::string dynamic = "A456";
    Test( "A123", "A234", "A345", dynamic );   // std::string도 그대로 넘어간다
    return 0;
}
  • 타입이 컴파일 시점에 검사된다.
  • std::string을 포함해 어떤 타입이든 넘길 수 있다.
  • sentinel이 필요 없다. 개수는 sizeof...(args)로 알 수 있다.

목록을 그냥 순회하기만 하면 되는 경우라면 std::initializer_list가 더 간단하다.

void Test( std::initializer_list<std::string_view> codes )
{
    for ( auto code : codes )
        std::cout << "code : " << code << "\n";
}

Test( { "A123", "A234", "A345" } );

참고

댓글 남기기