'C++' Variadic arguments & Fold expression

토스트·2025년 1월 18일
0

'C++' basic

목록 보기
16/35

가변 인자(Variadic arguments)

함수나 메서드가 인자의 수를 미리 알지 못하고, 호출 시에 전달되는 인자의 수가 가변적인 경우에 사용되는 개념입니다.
즉, 함수가 호출될 때 필요한 인자의 개수를 정확히 알 수 없는 경우에 인자를 받아들이는 방식입니다.

...로 표현되며, 그리스어에서 따온 단어로 ellipsis(생략)라고 부릅니다.

va_list

C++98에서 가변 인자를 처리하는 데 사용되는 타입이며, cstdarg 헤더 파일에 정의된 여러 매크로 함수들을 함께 사용합니다.

cstdarg 매크로 함수

va_start

void va_start(std::va_list ap, parm_n);

: 가변 인자의 목록을 처리하기 위해 초기화하는 매크로입니다. 가변 인자의 첫 번째 인자를 처리하는 데 사용됩니다.

  • ap : 원어로는 'argument pointer'로, va_list 타입의 변수로, 가변 인자 목록을 추적하는 데 사용됩니다.
  • parm_n : n개 parameter를 의미하며, 가변 인자 목록의 첫 번째 고정 인자를 나타내는 변수입니다.

va_arg

type va_arg(std::va_list ap, type);

: 가변 인자 목록에서 값을 하나 씩 꺼내는 매크로입니다.

  • ap : 원어로는 'argument pointer'로, va_list 타입의 변수입니다.
  • type : T는 추출하려는 인자의 타입을 나타냅니다. va_arg는 이 타입을 매개변수로 받아 가변 인자 리스트에서 해당 타입의 인자를 반환합니다. 이때 타입 T는 정확히 지정해야 합니다.

va_copy

void va_copy(std::va_list dest, std::va_list src);

: va_copy는 가변 인자 목록을 다른 va_list 변수에 복사할 때 사용됩니다.
va_list는 포인터처럼 동작하고, 한 번 va_arg로 인자를 읽어가면 그 상태는 소모되므로, 원본을 그대로 두고 복사본에서 다른 방식으로 인자를 읽어야 할 때 va_copy가 필요합니다.

  • dest : 원어로는 'destination'으로, 초기화할 va_list 타입의 변수입니다.
  • src : 원어로는 'source'로, 초기화에 사용될 va_list 타입의 변수입니다.

va_end

void va_end(va_list ap);

: 가변 인자 목록을 처리한 후 종료할 떄 호출하는 매크로입니다.

  • ap : 원어로는 'argument pointer'로, 정리할 va_list 타입의 변수입니다.

<example> | 출력 1

#include <iostream>
#include <cstdarg>

using namespace std;

void print(int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; ++i) {
        int num = va_arg(args, int); 
        cout << num << " ";
    }

    va_end(args);
    cout << endl;
}

int main() {
    print(3, 10, 20, 30); // 3개의 인자 전달
    print(5, 1, 2, 3, 4, 5); // 5개 인자 전달

    return 0;
}

결과값

가변 인자 템플릿(Variadic templates)

C++11에서 도입된 템플릿 기능으로, 임의의 수의 인수를 지원하는 클래스 또는 함수 템플릿을 작성할 수 있게 해줍니다.
즉, 템플릿을 정의할 때 인자 수를 고정하지 않고 여러 개의 인자를 받을 수 있는 기능을 제공하여 유연성을 제공합니다.

  • 팩 확장(Pack Expansion) : template parameter pack이나 function parameter pack과 결합되어 0개 이상의 인수를 처리할 수 있도록 합니다. 이는 템플릿이나 함수에서 여러 개의 인수를 펼쳐서 처리할 수 있게 해주는 기능입니다.

parameter pack

템플릿에서 인자의 개수가 동적으로 결정되는 경우에 사용됩니다. C++11 이후 도입된 이 기능은 하나 이상의 템플릿 매개변수를 패킹(packing)하여 함수나 클래스로 전달할 수 있습니다. 가변 인자 템플릿을 통해 여러 개의 인자를 처리할 수 있습니다.

  • 템플릿 파라미터 팩(template parameter pack) : 템플릿을 사용할 때, 하나의 템플릿 파라미터가 여러 개의 인수를 받을 수 있게 해주는 기능입니다. 즉, 템플릿 파라미터 팩을 사용하면 템플릿이 여러 인수로 확장될 수 있습니다.
template<typename... Arguments> class classname;
  • 함수 파라미터 팩(function parameter pack) : 함수 템플릿에서 함수가 여러 개의 인수를 받을 수 있도록 하는 기능입니다. 즉, 이 방식은 함수가 다양한 인수 개수와 타입을 처리할 수 있게 해줍니다.
template<typename... Arguments> void functionName(Arguments... arguments);
  • 람다 초기화 캡처 팩(lambda init-capture pack) : C++20에 구현된 기능으로, 초기화자(initializer)에서 팩 확장(pack expansion)을 사용하여 여러 변수들을 초기화 캡처하는 방식입니다.
auto lambda = [init_capture1, init_capture2, ...](parameter_pack) { };
  • 인자 팩(Argument Pack) : 템플릿 파라미터 팩이나 함수 파라미터 팩에서 여러 개의 인자를 하나의 '묶음'으로 받는 역할을 합니다. (매개변수)
  • 전개되지 않은 팩(unexpanded pack) : 인자 팩으로 받은 '묶음'을 의미합니다.
  • 인자 팩 확장(Argument Pack Expansion) : 전개되지 않은 팩을 개별적으로 펼쳐서 사용하는 과정입니다.

<example> | 출력 2

#include <iostream>

using namespace std;

// 재귀 종료 조건 : 하나의 인자만 남았을 때
template<typename T>
void print(T value) {
    cout << value << " " << endl;
}

// 재귀적 케이스 : 두 개 이상의 인자가 남았을 때
template<typename T, typename... Args> // function parameter pack
void print(T val, Args... args) { // Argument Pack
    cout << val << " ";

    print(args...); // Argument Pack Expansion
}

int main() {
    print(10, 20, 30);
    
    print(1, 2, 3, 4, 5);

    return 0;
}

결과값

Fold expression을 사용하지 않고, 가변 인자 템플릿을 사용할 때는 재귀 탈출 조건을 반드시 설정해야 합니다.

폴드 표현식(Fold expression)

C++17에서 도입된 기능으로, 이진 연산자에 대한 팩을 Reduces(또는 folds)합니다.

특히 템플릿 매개변수 팩(parameter pack)을 처리할 때 유용하게 사용됩니다.

  • op : 이진 연산자를 의미합니다.
    ex) + - / % ^ & | = < > << >> += -= = /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*
  • pack : 전개되지 않은 팩(unexpanded pack)을 포함한 표현식을 의미합니다. 우선순위 규칙에 따라 팩 표현식에는 cast(casting : 형 변환)보다 낮은 우선순위를 가지는 연산자가 포함되지 않아야 합니다.
  • init : 전개되지 않은 팩(unexpanded pack)이 포함되지 않고, 최상위에 cast(casting : 형 변환)보다 낮은 우선순위의 연산자가 포함되지 않는 표현식입니다.

cast는 단항 연산으로 높은 우선 순위를 가지지만, 단항 연산 중에선 가장 낮은 우선순위를 가집니다.
즉, 후위 증감 연산자와 전위 증감 연산자 다음 우선순위를 가집니다.

Unary right fold (pack op ...)

[E1E_{1} op (...... op (EN1E_{N-1} op ENE_{N}))]

<example>

#include <iostream>

using namespace std;

template <typename... Args>
int UnaryRightFold(Args... args) {
	return (args - ...);
}

int main() {
	cout << UnaryRightFold(1, 2, 3, 4);

	return 0;
}

1 - (2 - (3 - 4)) = 1 - (2 - (-1)) = 1 - 3 = -2

결과값

Unary left fold (... op pack)

[((E1E_{1} op E2E_{2}) op ......) op ENE_{N}]

<example>

#include <iostream>

using namespace std;

template <typename... Args>
int UnaryLeftFold(Args... args) {
	return (... - args);
}

int main() {
	cout << UnaryLeftFold(1, 2, 3, 4);

	return 0;
}

((1 - 2) - 3) - 4 = (-1 -3) - 4 = -4 -4 = -8

결과값

Binary right fold (pack op ... op init)

[E1E_{1} op (...... op (EN1E_{N−1} op (ENE_{N} op II)))]

동일한 op를 사용해야 합니다. 즉, 'pack - ... + init'는 사용이 불가능합니다.

<example>

#include <iostream>

using namespace std;

#define init 10

template <typename... Args>
int BinaryRightFold(Args... args) {
	return (args - ... - init);
}

int main() {
	cout << BinaryRightFold(1, 2, 3, 4);

	return 0;
}

1 - (2 - (3 - (4 - 10))) = 1 - (2 - (3 - (-6))) = 1 - (2 - 9) = 1 - (-7) = 8

결과값

Binary left fold (init op ... op pack)

[(((II op E1E_{1}) op E2E_{2}) op ......) op ENE_{N}]

동일한 op를 사용해야 합니다. 즉, 'init + ... - pack'은 사용이 불가능합니다.

<example>

#include <iostream>

using namespace std;

#define init 10

template <typename... Args>
int BinaryLeftFold(Args... args) {
	return (init - ... - args);
}

int main() {
	cout << BinaryLeftFold(1, 2, 3, 4);

	return 0;
}

(((10 - 1) - 2) - 3) - 4 = ((9 - 2) - 3) -4 = (7 - 3) - 4 = 4 - 4 = 0

결과값

<example> | 출력 3

#include <iostream>

using namespace std;

// Fold expression을 사용하여 여러 인자를 출력하는 함수
template<typename... Args>
void print(Args... args) {
    ((cout << args << " "), ...);

    cout << endl;
}

int main() {
    print(10, 20, 30);
    print(1, 2, 3, 4, 5);

    return 0;
}

결과값

0개의 댓글