본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : Variadic Templates and Fold Expressions (11, 17)

by snowoods 2025. 9. 26.

Modern C++

 

Variadic Templates and Fold Expressions

 

개요

C++에서 템플릿은 강력한 제네릭 프로그래밍 도구입니다. Variadic Templates는 임의의 개수의 템플릿 인자를 받을 수 있게 하여 템플릿의 유연성을 극대화합니다. 이를 통해 printf와 같이 가변 인자를 받는 함수를 타입에 안전하게 구현할 수 있습니다.

Fold Expressions는 C++17부터 도입된 기능으로, Variadic Template의 파라미터 팩(parameter pack)을 매우 간결하고 직관적인 방식으로 처리할 수 있게 해주는 구문입니다.

이 문서에서는 Variadic Templates의 기본 개념과 재귀를 이용한 고전적인 처리 방식, 그리고 C++17의 Fold Expressions를 활용한 현대적인 처리 방식을 설명합니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++11: Variadic Templates 도입
  • C++17: Fold Expressions 도입

 

내용 설명

Variadic Templates (C++11)

Variadic Template은 typename... 또는 class... 키워드를 사용하여 0개 이상의 템플릿 인자를 받는 템플릿을 의미합니다. 이 인자들을 파라미터 팩(parameter pack) 이라고 부릅니다.

template <typename... Args>
void myFunction(Args... args);

파라미터 팩은 그 자체로 직접 사용하기 어렵기 때문에, 보통 팩 확장(pack expansion) 을 통해 개별 인자로 풀어 사용합니다. C++11에서는 주로 재귀 함수 호출을 통해 파라미터 팩을 하나씩 처리했습니다.

아래 concat1 함수는 재귀를 통해 파라미터 팩을 처리하는 전형적인 예시입니다.

  1. concat1(T first, Args... args): 첫 번째 인자 first와 나머지 인자 팩 args를 받습니다.
  2. first를 처리하고, 나머지 args를 다시 concat1에 넘겨 재귀 호출합니다.
  3. concat1(T first): 인자가 하나만 남았을 때 재귀를 멈추는 기본 함수(base case)입니다.

Fold Expressions (C++17)

C++17부터는 Fold Expressions를 사용하여 파라미터 팩을 훨씬 간결하게 처리할 수 있습니다. Fold는 파라미터 팩의 모든 요소에 대해 이항 연산자(binary operator)를 적용하는 구문입니다.

주요 구문은 다음과 같습니다.

  • Unary Right Fold: (... op pack) -> (E1 op (E2 op (E3 op ...)))
  • Unary Left Fold: (pack op ...) -> (((... op E3) op E2) op E1)
  • Binary Right Fold: (pack op ... op init)
  • Binary Left Fold: (init op ... op pack)

예를 들어, (... + args)는 파라미터 팩 args의 모든 요소를 오른쪽에서부터 + 연산자로 합산합니다.

 

예제 코드

#include <iostream>
#include <string>

// C++11: 재귀를 이용한 Variadic Template 처리
template <typename T>
T concat_recursive(T first)
{
    return first;
}

template <typename T, typename... Args>
T concat_recursive(T first, Args... args)
{
    // 첫 번째 인자와 나머지 인자들을 재귀적으로 합산
    return first + concat_recursive(args...);
}

// C++17: Fold Expression을 이용한 처리
template <typename... Args>
auto concat_fold(Args... args)
{
    // Unary Right Fold: (arg1 + (arg2 + (arg3 + ...)))
    return (... + args);
}

// Unary Left Fold 예시
template <typename... Args>
auto subtract_fold_left(Args... args)
{
    // ( (... - arg3) - arg2) - arg1
    return (args - ...);
}

// Unary Right Fold 예시
template <typename... Args>
auto subtract_fold_right(Args... args)
{
    // (arg1 - (arg2 - (arg3 - ...)))
    return (... - args);
}

int main()
{
    // 문자열 합치기
    std::string s1 = "He";
    std::string s2 = "ll";
    std::string s3 = "o World";
    std::cout << "[Recursive] String: " << concat_recursive(s1, s2, s3) << std::endl;
    std::cout << "[Fold]      String: " << concat_fold(s1, s2, s3) << std::endl;

    std::cout << "---" << std::endl;

    // 정수 합치기
    std::cout << "[Recursive] Integer: " << concat_recursive(1, 2, 3, 4, 5) << std::endl;
    std::cout << "[Fold]      Integer: " << concat_fold(1, 2, 3, 4, 5) << std::endl;

    std::cout << "---" << std::endl;

    // Fold 방향에 따른 연산 순서 차이 (뺄셈)
    // Left Fold: (((10 - 5) - 3) - 1) = 1
    std::cout << "[Fold Left]  Subtract (10, 5, 3, 1): " << subtract_fold_left(10, 5, 3, 1) << std::endl;
    // Right Fold: (10 - (5 - (3 - 1))) = 7
    std::cout << "[Fold Right] Subtract (10, 5, 3, 1): " << subtract_fold_right(10, 5, 3, 1) << std::endl;

    return 0;
}

 

실행 결과

[Recursive] String: HelloWorld
[Fold]      String: HelloWorld
---
[Recursive] Integer: 15
[Fold]      Integer: 15
---
[Fold Left]  Subtract (10, 5, 3, 1): 1
[Fold Right] Subtract (10, 5, 3, 1): 7

 

활용팁

  • 가독성과 간결성: C++17 이상을 사용한다면 재귀 방식보다 Fold Expressions를 사용하는 것이 코드를 훨씬 간결하고 읽기 쉽게 만듭니다. 컴파일러 최적화 측면에서도 더 유리할 수 있습니다.
  • 다양한 연산자 활용: +, - 외에도 &&, ||, ,(comma operator) 등 다양한 이항 연산자와 함께 사용할 수 있습니다. 예를 들어, 쉼표 연산자를 사용하면 파라미터 팩에 포함된 모든 함수를 순차적으로 실행할 수 있습니다.
    template<typename... Funcs>
    void execute_all(Funcs... funcs) {
        (..., funcs()); // 모든 함수를 순서대로 호출
    }
  • Perfect Forwarding: Variadic Templates는 std::forward와 함께 사용되어 인자의 값 카테고리(lvalue/rvalue)를 그대로 전달하는 Perfect Forwarding을 구현하는 데 핵심적인 역할을 합니다. 이는 팩토리 함수나 wrapper 함수를 만들 때 매우 유용합니다.