가변 인자 함수

수학 및 컴퓨터 프로그래밍에서 가변 인자 함수(variadic function)는 항수가 부정인 함수, 즉 가변 개수의 인자를 받는 함수이다. 가변 인자 함수에 대한 지원은 프로그래밍 언어마다 크게 다르다.

가변 인자(variadic)라는 용어는 1936년/1937년으로 거슬러 올라가는 신조어이다. 이 용어는 1970년대까지 널리 사용되지 않았다.

개요
자연스럽게 가변 인자 함수로 나타나는 많은 수학적 및 논리적 연산이 있다. 예를 들어, 숫자의 합계 또는 문자열이나 다른 시퀀스의 문자열 연결은 임의의 수의 피연산자에 적용될 수 있다고 생각할 수 있는 연산이다 (이러한 경우 공식적으로 결합법칙이 적용되더라도).

많은 언어에서 가변 인자 함수로 구현된 또 다른 연산은 출력 포맷팅이다. C 함수 와 커먼 리스프 함수 이 두 가지 예이다. 둘 다 출력의 포맷팅을 지정하는 하나의 인수를 받고, 포맷팅할 값을 제공하는 임의의 수의 인수를 받는다.

가변 인자 함수는 일부 언어에서 자료형 안전 문제를 노출할 수 있다. 예를 들어, C의 는 부주의하게 사용하면 포맷 스트링 공격으로 알려진 보안 취약점의 한 종류를 유발할 수 있다. 이 공격은 가변 인자 함수에 대한 언어 지원이 자료형 안전하지 않기 때문에 가능하다. 즉, 함수가 스택에 배치된 것보다 더 많은 인수를 꺼내려고 시도하여 스택을 손상시키고 예상치 못한 동작을 유발한다. 이로 인해 CERT 코디네이션 센터는 C의 가변 인자 함수를 높은 심각도의 보안 위험으로 간주한다.

함수형 프로그래밍 언어에서 가변 인자 함수는 함수와 목록/시퀀스/배열을 인수로 받아 해당 목록에 제공된 인수로 함수를 호출하는 apply 함수와 상호 보완적으로 간주될 수 있다. 이로써 함수에 가변 개수의 인수를 전달한다. 함수형 언어인 하스켈에서는 가변 인자 함수가 타입 클래스 의 값을 반환함으로써 구현될 수 있다. 만약 의 인스턴스가 최종 반환 값 과 함수 라면, 이는 임의의 수의 추가 인수 를 허용한다.

항 재작성 연구에서 관련 주제는 hedges 또는 hedge variables라고 불린다. 인자를 가진 함수인 가변 인자 함수와 달리, hedges는 인자 시퀀스 그 자체이다. 또한 '4개 이하의 인자만 받기'와 같이 제약 조건('정확히 4개의 인자 받기'와 같이 변수 길이가 아님)을 가질 수 있으므로, 이를 가변 인자 함수라고 부르는 것은 오해의 소지가 있다. 그러나 이들은 동일한 현상을 지칭하며, 때로는 용어가 혼합되어 가변 변수(hedge와 동의어)와 같은 이름이 나오기도 한다. 함수형 프로그래밍 및 항 재작성에서 '변수'라는 단어의 이중 의미와 인자와 변수 간의 차이에 유의한다. 예를 들어, 한 항(함수)은 세 개의 변수를 가질 수 있고, 그 중 하나는 hedge이므로, 그 항은 세 개 이상의 인자(또는 hedge가 비어 있을 수 있다면 두 개 이상)를 받을 수 있다.

예시
C에서
C 언어에서 가변 인자 함수를 이식성 있게 구현하려면 표준 헤더 파일을 사용한다. 이전 헤더는 를 선호하여 구식화되었다. C++에서는 헤더 파일 가 사용된다.

#include 

#include 

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

 va_start(ap, count); // Before C23: Requires the last fixed parameter (to get the address)
 for (int j = 0; j < count; ++j) {
 sum += va_arg(ap, int); /* Increments ap to the next argument. */
 }
 va_end(ap);

 return sum / count;
}

int main(int argc, char* argv[]) {
 printf("%f\n", average(3, 1, 2, 3));
 return 0;
}

이것은 임의의 수의 인수의 평균을 계산할 것이다. 함수는 인수의 수나 그 타입을 알지 못한다. 위 함수는 타입이 일 것이며, 인수의 수가 첫 번째 인수로 전달될 것이라고 예상한다 (이는 자주 사용되지만 언어나 컴파일러에 의해 강제되지는 않는다). 다른 경우, 예를 들어 printf에서는 인수의 수와 타입이 포맷 문자열에서 파악된다. 두 경우 모두, 이는 프로그래머가 올바른 정보를 제공하는 것에 달려 있다. (대안으로, 매개변수 목록의 끝을 나타내기 위해 또는 과 같은 센티넬 값이 사용될 수 있다.) 함수가 믿는 것보다 적은 인수가 전달되거나 인수의 타입이 올바르지 않으면, 메모리의 잘못된 영역을 읽을 수 있고 포맷 스트링 공격과 같은 취약점으로 이어질 수 있다. 시스템에 따라 을 센티넬로 사용하는 것조차 그러한 문제를 야기할 수 있다; 또는 올바른 대상 타입의 전용 null 포인터가 이를 피하는 데 사용될 수 있다.

는 타입을 선언하고, , , , 의 네 가지 매크로를 정의한다. 및 의 각 호출은 해당 호출과 일치해야 한다. 가변 인자를 다룰 때, 함수는 일반적으로 매크로에 의해 조작될 타입의 변수(예시에서는 )를 선언한다.

 는 두 개의 인수를 받는데, 객체와 함수의 마지막 매개변수(생략 부호 앞의 매개변수; 매크로는 이를 사용하여 위치를 파악한다)에 대한 참조이다. C23에서는 두 번째 인수가 더 이상 필요하지 않으며, 가변 인자 함수는 생략 부호 앞에 명명된 매개변수가 더 이상 필요하지 않을 것이다. 이는 또는 에서 사용할 객체를 초기화한다. 컴파일러는 일반적으로 참조가 올바르지 않으면(예: 마지막 매개변수가 아닌 다른 매개변수에 대한 참조, 또는 완전히 다른 객체에 대한 참조) 경고를 발행하지만, 컴파일이 정상적으로 완료되는 것을 막지는 않는다.
 는 두 개의 인수를 받는데, 객체(이전에 초기화됨)와 타입 서술자이다. 이는 다음 가변 인수로 확장되며, 지정된 타입을 갖는다. 의 연속적인 호출은 각 가변 인자를 차례로 처리할 수 있도록 한다. 타입이 올바르지 않거나 다음 가변 인자가 없으면 지정되지 않은 동작이 발생한다.
 는 하나의 인수, 즉 객체를 받는다. 이는 정리하는 역할을 한다. 예를 들어, 가변 인자를 두 번 이상 스캔하고 싶다면, 프로그래머는 를 호출한 다음 다시 를 호출하여 객체를 다시 초기화할 것이다.
 는 두 개의 인수를 받는데, 둘 다 객체이다. 이는 두 번째 객체(초기화되어야 함)를 첫 번째 객체로 복제한다. "가변 인자를 두 번 이상 스캔"하는 예시로 돌아가서, 이는 첫 번째 에 대해 를 호출한 다음, 를 사용하여 이를 두 번째 로 복제함으로써 달성될 수 있다. 첫 번째 와 를 사용하여 가변 인자를 처음 스캔한 후(로 처리함), 프로그래머는 두 번째 와 를 사용하여 가변 인자를 두 번째 스캔할 수 있다. 포함하는 함수가 반환되기 전에 복제된 에 대해서도 가 호출되어야 한다.

C#에서
C#는 키워드를 사용하여 가변 인자 함수를 설명한다. 인수에 대한 타입이 제공되어야 하지만, 는 모든 것을 포괄하는 타입으로 사용될 수 있다. 호출 지점에서는 인수를 하나씩 나열하거나 필요한 요소 타입을 가진 기존 배열을 전달할 수 있다. 가변 인자 형식을 사용하는 것은 후자에 대한 신택틱 슈거이다.

using System;

class Program
{
 static int Foo(int a, int b, params int[] args)
 {
 // Return the sum of the integers in args, ignoring a and b.
 int sum = 0;
 foreach (int i in args)
 sum += i;
 return sum;
 }

 static void Main(string[] args)
 {
 Console.WriteLine(Foo(1, 2)); // 0
 Console.WriteLine(Foo(1, 2, 3, 10, 20)); // 33
 int[] manyValues = new int[] { 13, 14, 15 };
 Console.WriteLine(Foo(1, 2, manyValues)); // 42
 }
}

C++에서
C++의 기본 가변 인자 기능은 C의 기능과 거의 동일하다. C++26 이전에는 생략 부호 앞의 쉼표를 생략할 수 있었다. C++는 명명된 매개변수가 없는 가변 인자 함수를 허용하지만, 가 함수의 마지막 고정 인수의 이름을 요구하므로 해당 인수에 접근할 방법은 제공하지 않는다. 

import std;

// before C++26, my_printf(const char* fmt...) was legal
void my_printf(const char* fmt, ...) {
 va_list args;
 va_start(args, fmt);

 while (*fmt != '\0') {
 if (*fmt == 'd') {
 int i = va_arg(args, int);
 std::println("{}", i);
 } else if (*fmt == 'c') {
 // note automatic conversion to integral type
 int c = va_arg(args, int);
 std::println("{}", static_cast
(c));
 } else if (*fmt == 'f') {
 double d = va_arg(args, double);
 std::println("{}", d);
 }
 ++fmt;
 }

 va_end(args);
}

int main(int argc, char* argv[]) {
 my_printf("dcff", 3, 'a', 1.999, 42.5);
}

가변 인자 템플릿 (매개변수 팩)은 C++에서 언어 내장 폴드 표현식과 함께 사용될 수도 있다.

import std;

template 

void fooPrint(Ts... args) {
 ((std::print("{} ", args)), ...);
}

int main(int argc, char* argv[]) {
 fooPrint(1, 3.14f); // 1 3.14
 fooPrint("Foo", 'b', true, nullptr); // Foo b true nullptr
}

CERT 코딩 표준 for C++는 오용 위험이 낮기 때문에 C++에서 C 스타일 가변 인자 함수보다 가변 인자 템플릿 (매개변수 팩)을 사용하는 것을 강력히 선호한다. 가변 인자 템플릿은 자바 스타일의 자료형 안전 가변 매개변수를 달성하는 유일한 방법이다.

포트란에서
포트란 90 개정 이후, 포트란 함수나 서브루틴은 선택적 인수를 받아들일 수 있다. 인수 목록은 여전히 고정되어 있지만, 속성을 가진 인수는 함수/서브루틴 호출에서 생략될 수 있다. 내장 함수 는 선택적 인수의 존재를 감지하는 데 사용될 수 있다. 선택적 인수는 인수 목록의 어느 곳에나 나타날 수 있다.

program test
implicit none

 real :: x

 !> all arguments are passed:
 call foo( 1, 2, 3.0, 4, x )
 !< outputs 1 \ 2 \ 3.0 \ 4 \ 6.0 (the "\" denotes a newline)

 !> the last 2 arguments are omitted:
 call foo( 1, 2, 3.0 )
 !< outputs 1 \ 2 \ 3.0

 !> the 2nd and 4th arguments are omitted: the arguments that are positioned after
 !> an omitted argument must be passed with a keyword:
 call foo( 1, c=3.0, e=x )
 !< outputs 1 \ 3.0 \ 6.0

 !> alternatively, the Fortran 2023 revision has introduced the .NIL. pseudo constant
 !> to denote an omitted argument
 call foo( 1, .NIL., 3.0, .NIL., x )
 !< outputs 1 \ 3.0 \ 6.0

contains

 !> the subroutine foo() has 2 mandatory and 3 optional arguments
 subroutine foo( a, b, c, d, e )
 integer, intent(in) :: a
 integer, intent(in), optional :: b
 real, intent(in) :: c
 integer, intent(in), optional :: d
 real, intent(out), optional :: e

 print*, a
 if (present(b)) print*, b
 print*, c
 if (present(d)) print*, d
 if (present(e)) then
 e = 2*c
 print*, c
 end if
 end subroutine

end program

Go에서
Go의 가변 인자 함수는 어떤 수의 후행 인수와 함께 호출될 수 있다. 은 흔한 가변 인자 함수이며, 모든 것을 포괄하는 타입으로 빈 인터페이스를 사용한다.

package main

import "fmt"

// This variadic function takes an arbitrary number of ints as arguments.
func sum(nums ...int) {
 fmt.Print("The sum of ", nums) // Also a variadic function.
 total := 0
 for _, num := range nums {
 total += num
 }
 fmt.Println(" is", total) // Also a variadic function.
}

func main() {
 // Variadic functions can be called in the usual way with individual
 // arguments.
 sum(1, 2) // "The sum of [1 2] is 3"
 sum(1, 2, 3) // "The sum of [1 2 3] is 6"

 // If you already have multiple args in a slice, apply them to a variadic
 // function using func(slice...) like this.
 nums := []int{1, 2, 3, 4}
 sum(nums...) // "The sum of [1 2 3 4] is 10"
}

출력:

The sum of [1 2] is 3
The sum of [1 2 3] is 6
The sum of [1 2 3 4] is 10

자바에서
C#과 마찬가지로, 자바의 타입은 모든 것을 포괄하는 타입으로 사용할 수 있다.

자바에서 매개변수는 생략 부호 표기법을 사용하여 가변 인자로 지정할 수 있다. 이는 본질적으로 배열과 동일하지만 배열로 래핑할 필요가 없다. 예를 들어, String... args와 String[] args는 본질적으로 동일하다.
public class Program {
 // Variadic methods store any additional arguments they receive in an array.
 // Consequentially, `printArgs` is actually a method with one parameter: a
 // variable-length array of `String`s.
 private static void printArgs(String... strings) {
 for (String s : strings) {
 System.out.println(s);
 }
 }

 public static void main(String[] args) {
 printArgs("hello"); // short for printArgs(new String[] {"hello"})
 printArgs("hello", "world"); // short for printArgs(new String[] {"hello", "world"})
 }
}

자바스크립트에서
자바스크립트는 가변 인자의 타입에 신경 쓰지 않는다.

function sum(...numbers) {
 return numbers.reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(3, 2)); // 5
console.log(sum()); // 0

 키워드로 생성된 함수에서만 사용할 수 있지만, arguments 객체를 사용하여 가변 인자 함수를 만드는 것도 가능하다.

function sum() {
 return Array.prototype.reduce.call(arguments, (a, b) => a + b, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(3, 2)); // 5
console.log(sum()); // 0

루아에서
루아 함수는 키워드를 사용하여 다른 값들과 동일한 방식으로 가변 인자를 다른 함수에 전달할 수 있다. 테이블은 Lua 버전 5.2 이상에서 , 또는 Lua 5.1 이하에서 을 사용하여 가변 인자 함수로 전달될 수 있다. 가변 인자는 가변 인자를 값으로 사용하여 테이블을 구성함으로써 테이블로 사용될 수 있다.function sum(...) --... designates varargs
 local sum=0
 for _,v in pairs({...}) do --creating a table with a varargs is the same as creating one with standard values
 sum=sum+v
 end
 return sum
end

values={1,2,3,4}
sum(5,table.unpack(values)) --returns 15. table.unpack should go after any other arguments, otherwise not all values will be passed into the function.

function add5(...)
 return ...+5 --this is incorrect usage of varargs, and will only return the first value provided
end

entries={}
function process_entries()
 local processed={}
 for i,v in pairs(entries) do
 processed[i]=v --placeholder processing code
 end
 return table.unpack(processed) --returns all entries in a way that can be used as a vararg
end

print(process_entries()) --the print function takes all varargs and writes them to stdout separated by newlines

파스칼에서
파스칼은 ISO 표준 7185("표준 파스칼") 및 10206("확장 파스칼")에 의해 표준화되었다.
표준화된 파스칼 형식은 특정 내장 함수(/ 및 /, 그리고 추가적으로 에서 /)를 제외하고는 가변 인자 루틴을 지원하지 않는다.

그럼에도 불구하고, 파스칼 방언들은 가변 인자 루틴과 유사한 메커니즘을 구현한다.
델파이는 마지막 형식 매개변수와 연결될 수 있는 데이터 타입을 정의한다.
루틴 정의 내에서 는 배열인 variant record의 이다.
앞서 언급된 데이터 타입의 멤버는 인수의 데이터 타입을 검사하고 적절하게 처리할 수 있게 한다.
프리 파스칼 컴파일러도 델파이의 가변 인자 루틴을 지원한다.

그러나 이 구현은 기술적으로 단일 인수, 즉 를 필요로 한다.
파스칼은 배열이 동질적이어야 한다는 제한을 부과한다.
이 요구사항은 variant record를 활용하여 우회된다.
GNU 파스칼은 생략 부호()를 사용하여 실제 가변 인자 형식 매개변수 사양을 정의하지만, 2022년 현재 그러한 것을 사용하기 위한 이식 가능한 메커니즘은 정의되지 않았다.

GNU 파스칼과 프리파스칼 모두 외부에서 선언된 함수가 생략 부호()를 사용하여 가변 인자 형식 매개변수 사양을 사용할 수 있도록 한다.

PHP에서
PHP는 인수가 타입이 지정되지 않은 한 가변 인자의 타입에 신경 쓰지 않는다.

function sum(...$nums): int
{
 return array_sum($nums);
}

echo sum(1, 2, 3); // 6

그리고 타입이 지정된 가변 인자:

function sum(int ...$nums): int
{
 return array_sum($nums);
}

echo sum(1, "a", 3); // TypeError: Argument 2 passed to sum() must be of the type int (since PHP 7.3)

파이썬에서
파이썬은 가변 인자의 타입에 신경 쓰지 않는다.

from typing import Any

def foo(a: Any, b: Any, *args: tuple[Any, ...]) -> None:
 print(args) // args는 튜플 (변경 불가능한 시퀀스)이다.

if __name__ == "__main__":
 foo(1, 2) // ()
 foo(1, 2, 3) // (3,)
 foo(1, 2, 3, "hello") // (3, "hello")

키워드 인수는 딕셔너리에 저장될 수 있다.
def bar(*args: tuple[Any, ...], **kwargs: dict[str, Any]]) -> Any:
 // 함수 본문

라쿠에서
라쿠에서 가변 인자 함수를 생성하는 매개변수의 타입은 슬러피 배열 매개변수라고 알려져 있으며, 세 가지 그룹으로 분류된다:

평탄화된 슬러피
이 매개변수는 단일 별표(*)로 선언되며, 반복 가능한 요소(즉, Iterables)의 하나 이상의 계층을 해체하여 인수를 평탄화한다.

sub foo($a, $b, *@args) {
 say @args.perl;
}

foo(1, 2) # []
foo(1, 2, 3) # [3]
foo(1, 2, 3, "hello") # [3 "hello"]
foo(1, 2, 3, [4, 5], [6]); # [3, 4, 5, 6]

비평탄화된 슬러피
이 매개변수들은 두 개의 별표(**)로 선언되며, 목록 내의 반복 가능한 인수를 평탄화하지 않고 인수를 거의 있는 그대로 유지한다:

sub bar($a, $b, **@args) {
 say @args.perl;
}

bar(1, 2); # []
bar(1, 2, 3); # [3]
bar(1, 2, 3, "hello"); # [3 "hello"]
bar(1, 2, 3, [4, 5], [6]); # [3, [4, 5], [6]]

문맥적 슬러피
이 매개변수들은 더하기(+) 기호로 선언되며, 문맥에 따라 슬러피 인수를 처리하는 방법을 결정하는 "단일 인수 규칙"을 적용한다. 간단히 말해서, 단일 인수만 전달되고 그 인수가 반복 가능하면, 그 인수는 슬러피 매개변수 배열을 채우는 데 사용된다. 다른 경우에는 +@가 **@처럼 작동한다(즉, 비평탄화된 슬러피).

sub zaz($a, $b, +@args) {
 say @args.perl;
}

zaz(1, 2); # []
zaz(1, 2, 3); # [3]
zaz(1, 2, 3, "hello"); # [3 "hello"]
zaz(1, 2, [4, 5]); # [4, 5], 단일 인수가 배열을 채운다
zaz(1, 2, 3, [4, 5]); # [3, [4, 5]], **@처럼 작동한다
zaz(1, 2, 3, [4, 5], [6]); # [3, [4, 5], [6]], **@처럼 작동한다

루비에서
루비는 가변 인자의 타입에 신경 쓰지 않는다.

def foo(*args)
 print args
end

foo(1)
# prints `[1]=> nil`

foo(1, 2)
# prints `[1, 2]=> nil`

러스트에서
러스트는 함수에서 가변 인수를 지원하지 않는다. 대신, 가변 인수를 지원하는 매크로를 사용한다. 이것이 이 함수가 아닌 매크로인 이유이다. 매크로는 형식을 지정하기 위해 가변 인수를 받기 때문이다.

macro_rules! calculate {
 // The pattern for a single `eval`
 (eval $e:expr) => {{
 {
 let val: usize = $e; // Force types to be integers
 println!("{} = {}", stringify!{$e}, val);
 }
 }};

 // Decompose multiple `eval`s recursively
 (eval $e:expr, $(eval $es:expr),+) => {{
 calculate! { eval $e }
 calculate! { $(eval $es),+ }
 }};
}

fn main() {
 calculate! {\
 eval 1 + 2,
 eval 3 + 4,
 eval (2 * 3) + 1
 }
}

러스트는 기능 스위치를 통해 C의 가변 시스템과 상호작용할 수 있다. 다른 C 인터페이스와 마찬가지로, 이 시스템은 러스트에게 로 간주된다.

스칼라에서
object Program {
 // Variadic methods store any additional arguments they receive in an array.
 // Consequentially, `printArgs` is actually a method with one parameter: a
 // variable-length array of `String`s.
 private def printArgs(strings: String*): Unit = {
 strings.foreach(println)
 }

 def main(args: Array[String]): Unit = {
 printArgs("hello"); // short for printArgs(["hello"])
 printArgs("hello", "world"); // short for printArgs(["hello", "world"])
 }
}

스위프트에서
스위프트는 가변 인자의 타입에 신경 쓰지만, 모든 것을 포괄하는 타입을 사용할 수 있다.

func greet(timeOfTheDay: String, names: String...) {
 // here, names is [String]

 print("Looks like we have \(names.count) people")

 for name in names {
 print("Hello \(name), good \(timeOfTheDay)")
 }
}

greet(timeOfTheDay: "morning", names: "Joseph", "Clara", "William", "Maria")

// Output:
// Looks like we have 4 people
// Hello Joseph, good morning
// Hello Clara, good morning
// Hello William, good morning
// Hello Maria, good morning

Tcl에서
Tcl 프로시저 또는 람다는 마지막 인수가 일 때 가변 인자이다. 이것은 나머지 모든 인수의 목록(비어 있을 수도 있음)을 포함할 것이다. 이 패턴은 많은 다른 프로시저와 유사한 메서드에서 흔하다.

proc greet {timeOfTheDay args} {
 puts "Looks like we have [llength $args] people"

 foreach name $args {
 puts "Hello $name, good $timeOfTheDay"
 }
}

greet "morning" "Joseph" "Clara" "William" "Maria"

# Output:
# Looks like we have 4 people
# Hello Joseph, good morning
# Hello Clara, good morning
# Hello William, good morning
# Hello Maria, good morning

 같이 보기 
 자바 프로그래밍 언어의 가변 인수
 가변 매크로 (C 프로그래밍 언어)
 가변 인자 템플릿

 각주 

 외부 링크 
 Variadic function. 120개 이상의 프로그래밍 언어에서 가변 인자 함수 구현을 보여주는 로제타 코드 작업.
 Variable Argument Functions — C++ 가변 인자 함수 튜토리얼
 GNU libc 매뉴얼

분류:프로그래밍 언어의 비교
분류:함수 (프로그래밍)