가변 인자(Variadic arguments)

가변 인자(Variadic arguments)

함수가 받을 인자의 개수를 미리 정할 수 없을 때가 있어요. printf처럼 인자를 몇 개든 받을 수 있게 하려면? C++에서는 매개변수 목록의 마지막 매개변수를 생략 부호(..., ellipsis)로 두면 함수가 원하는 만큼 추가 인자를 받을 수 있어요. 이런 함수를 **가변 인자 함수(variadic function)**라고 불러요.

출처: cppreference

본문

함수의 매개변수 목록 마지막 매개변수가 생략 부호(...)이면 그 함수는 가변 인자 함수예요.

생략 부호 앞의 쉼표는 생략할 수 있음 (C++26에서 비권장)
// 다음과 같이 선언된 함수
int printx(const char* fmt, ...);
int printx(const char* fmt...); // 위와 동일하지만 C++26부터 비권장

// 인자를 하나 이상 받아 호출할 수 있음:
printx("hello world");
printx("a=%d b=%d", a, b);

int printy(..., const char* fmt); // 오류: ...는 마지막 매개변수만 될 수 있음
int printz(...); // 유효하지만, 인자를 이식 가능한 방식으로 접근할 수 없음

이것은 매개변수 선언자의 일부인 생략 부호로 표시되는 함수 매개변수 팩 확장과는 달라요. 매개변수 팩 확장과 "가변" 생략 부호는 함수 템플릿 선언에 함께 나타날 수 있어요. std::is_function이 그런 예예요. (C++11부터)

기본 인자 승격(Default argument promotions)

가변 인자 함수가 호출될 때, lvalue-to-rvalue, 배열-포인터, 함수-포인터 변환 후에 가변 인자 목록의 일부인 각 인자는 **기본 인자 승격(default argument promotions)**이라는 추가 변환을 거쳐요.

  • std::nullptr_tvoid*로 변환돼요. (C++11부터)
  • float 인자는 부동소수점 승격처럼 double로 변환돼요.
  • bool, char, short, 그리고 스코프 없는 열거형은 정수 승격처럼 int나 더 넓은 정수 타입으로 변환돼요.

비-POD 클래스 타입(until C++11)스코프 있는 열거형과, 자격 있는 비자명 복사 생성자, 자격 있는 비자명 이동 생성자, 또는 비자명 소멸자를 가진 클래스 타입(since C++11)은 잠재 평가(potentially-evaluated) 호출에서 구현 정의 의미와 함께 조건부로 지원돼요. (비평가 호출에서는 항상 지원돼요.)

가변 매개변수는 오버로드 해석에서 가장 낮은 순위를 가지므로, SFINAE에서 포괄적인 폴백(catch-all fallback)으로 자주 쓰여요.

가변 인자를 사용하는 함수 본문 안에서는 <cstdarg> 라이브러리 시설로 그 값들에 접근할 수 있어요.

헤더 <cstdarg>에 정의됨
va_start 가변 인자 함수 인자에 접근을 가능하게 함 (함수 매크로)
va_arg 다음 가변 인자에 접근 (함수 매크로)
va_copy(C++11) 가변 인자 함수 인자의 복사본을 만듦 (함수 매크로)
va_end 가변 인자 함수 인자의 순회를 끝냄 (함수 매크로)
va_list va_start, va_arg, va_end, va_copy에 필요한 정보를 보유 (typedef)

생략 부호 바로 앞의 마지막 매개변수가 참조 타입이거나, 기본 인자 승격 결과인 타입과 호환되지 않는 타입이면 va_start 매크로의 동작은 정의되지 않아요.

팩 확장이나 람다 캡처에서 생긴 실체가 va_start의 마지막 매개변수로 쓰이면, 프로그램은 진단 없이 잘못된 형태(ill-formed, no diagnostic required)예요. (C++11부터)

대안(Alternatives)

  • 가변 템플릿(variadic template)으로도 가변 개수의 인자를 받는 함수를 만들 수 있어요. 가변 템플릿은 인자 타입에 제한을 두지 않고, 정수·부동소수점 승격을 수행하지 않으며, 타입 안전하기 때문에 보통 더 나은 선택이에요.
  • 모든 가변 인자가 공통 타입을 공유하면, std::initializer_list가 (문법은 다르지만) 가변 인자에 접근하는 편리한 수단을 제공해요. 다만 std::initializer_list는 요소에 대한 const 포인터만 제공하므로 이 경우 인자를 수정할 수 없어요. (C++11부터)

주의할 점(Notes)

C 언어에서는 C23까지 생략 부호 매개변수 앞에 최소한 하나의 이름 있는 매개변수가 있어야 했어요. 그래서 R printz(...);는 C23까지 유효하지 않았어요. C++에서는 이런 형태가 허용되는데, 그런 함수에 전달된 인자는 접근할 수 없고(until C++26), 오버로드 해석에서 생략 부호 변환의 최저 우선순위를 활용하는 SFINAE의 폴백 오버로드로 흔히 쓰여요.

이 가변 인자 문법은 1983년 C++에서 생략 부호 앞의 쉼표 없이 도입됐어요. C89가 C++에서 함수 프로토타입을 채택할 때 쉼표를 요구하는 문법으로 바꿨어요. 호환성을 위해 C++98은 f(int n...)(C++ 스타일)과 f(int n, ...)(C 스타일) 둘 다 받아요. 원래 C++ 스타일 문법은 C++26부터 비권장이에요.

생략 부호가 가변 템플릿이 아니라 가변 함수를 의미하게 만들기 위해, 축약 함수 템플릿에서 쉼표를 쓸 수 있어요.

void f1(auto...);   // template<class... Ts> void f3(Ts...)와 동일
void f2(auto, ...); // template<class T> void f3(T, ...)와 동일

(C++20부터)

결함 보고(Defect reports)

이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 506 C++98 생략 부호에 비-POD 클래스 인자를 전달하면 정의되지 않은 동작이었음 그런 인자 전달은 구현 정의 의미와 함께 조건부로 지원됨
CWG 634 C++98 조건부로 지원되는 클래스 타입이 일부 SFINAE 관용구를 동작하지 않게 만들었음 비평가라면 항상 지원
CWG 2247 C++11 매개변수 팩이나 람다 캡처를 va_start에 전달하는 것에 제한이 없었음 진단 없이 잘못된 형태로 만듦
CWG 2347 C++11 스코프 있는 열거형을 생략 부호에 전달하는 것이 기본 인자 승격을 받는지 불분명했음 스코프 있는 열거형 전달은 구현 정의 의미와 함께 조건부로 지원

더 알아보기

  • 가변 템플릿: 타입 안전한 방식으로 가변 개수 인자를 처리하는 방법.
  • std::initializer_list: 공통 타입의 가변 인자를 다루는 또 다른 수단.
  • <cstdarg>: 가변 인자에 접근하는 매크로 시설.