C++ Fold Expression 완벽 가이드 | 단항·이항·쉼표 fold·커스텀 연산자 실전

C++ Fold Expression 완벽 가이드 | 단항·이항·쉼표 fold·커스텀 연산자 실전

이 글의 핵심

C++ Fold Expression 완벽 가이드에 대한 실전 가이드입니다. 단항·이항·쉼표 fold·커스텀 연산자 실전 등을 예제와 함께 상세히 설명합니다.

들어가며: “파라미터 팩 전체에 연산을 적용하고 싶어요”

구체적인 문제 시나리오

가변 인자 템플릿을 쓸 때 이런 상황을 자주 겪습니다:

  • 여러 인자의 합을 구하려면 sum(args...)가 필요한데, 재귀 함수를 두 개나 만들어야 한다
  • 모든 타입이 정수형인지 검사하려면 (std::is_integral_v<Ts> && ...)로 한 줄에 쓰고 싶다
  • 로그 함수에서 std::cout << arg1 << arg2 << arg3처럼 인자를 순서대로 출력하려면 재귀 전개가 길다
  • 가변 인자 min/max를 구할 때 ((result = ...), ...) 패턴으로 반복문 없이 짧게 쓰고 싶다
  • 여러 컨테이너에 동시에 push_back을 호출하려면 (vec.push_back(args), ...) 같은 표현이 필요하다

C++11에서는 재귀 템플릿으로 이 모든 것을 구현해야 했지만, C++17 fold expression은 파라미터 팩 전체에 이항 연산자를 “접어 나가는” 문법을 제공해 한 줄로 간결하게 작성할 수 있습니다.

추가 문제 시나리오

시나리오 1: 타입 리스트 검사
가변 인자로 받은 여러 타입이 모두 포인터인지, 모두 정수형인지 검사해야 할 때, enable_ifif constexpr에서 조건을 쓰려면 재귀 템플릿으로 길게 작성해야 했습니다. fold expression (std::is_pointer_v<Ts> && ...)로 한 줄에 표현할 수 있습니다.

시나리오 2: 로깅·포맷 출력
log("User", id, "connected at", timestamp)처럼 인자 개수와 타입이 달라도 한 번에 출력하는 함수를 만들 때, 재귀 종료 조건과 재귀 호출을 두 번 작성해야 했습니다. (std::cout << ... << args)로 한 줄로 대체할 수 있습니다.

시나리오 3: JSON 스키마 검증
여러 필드가 모두 유효한지 검사할 때, (validate(f1) && validate(f2) && ...)처럼 fold로 &&를 연결하면 short-circuit으로 앞에서 결과가 정해지면 나머지는 평가하지 않아 효율적입니다.

시나리오 4: 프로토콜 버퍼 직렬화
여러 필드를 순서대로 직렬화할 때, (serialize(buf, args), ...)처럼 comma fold로 각 인자마다 serialize를 호출할 수 있습니다. 반복문은 런타임에만 쓰이지만, fold는 컴파일 타임에 전개되어 인라인 최적화가 잘 됩니다.

시나리오 5: 단위 테스트용 all/any
assert(all(cond1, cond2, cond3))처럼 여러 조건이 모두 참인지, 하나라도 참인지 검사할 때 (... && args)(... || args) fold로 간단히 표현할 수 있습니다.

시나리오 6: 가변 인자 apply
apply(f, a, b, c)처럼 함수 f에 인자 a, b, c를 순서대로 넘기는 함수를 만들 때, 재귀 없이 std::invoke와 fold를 조합할 수 있습니다 (C++17 std::apply가 이미 제공).

시나리오 7: 플래그 마스크 OR
여러 옵션 플래그(OPT_A | OPT_B | OPT_C)를 하나로 합칠 때, (0 | ... | flags) fold로 가변 개수의 플래그를 한 번에 OR할 수 있습니다. 반복문이나 std::accumulate 대신 컴파일 타임에 전개됩니다.

시나리오 8: 설정값 검증
설정 구조체의 여러 필드가 모두 유효한 범위인지 검사할 때, (validate_range(cfg.a) && validate_range(cfg.b) && ...)처럼 fold로 연결하면 short-circuit으로 첫 실패에서 즉시 반환할 수 있습니다.

시나리오 9: 다중 락 획득
여러 뮤텍스를 순서대로 lock()할 때, (mtx1.lock(), mtx2.lock(), ...)처럼 쉼표 fold로 데드락을 피하는 고정 순서 락을 한 줄에 표현할 수 있습니다.

fold expression 평가 시각화

flowchart TD
    A[fold expression] --> B{종류}
    B --> C[단항 fold]
    B --> D[이항 fold]
    C --> C1["(pack op ...) 우측"]
    C --> C2["(... op pack) 좌측"]
    D --> D1["(init op ... op pack) 좌측"]
    D --> D2["(pack op ... op init) 우측"]
    C1 --> E[예: 1 + 2 + 3 + 4]
    D1 --> E

fold vs 재귀 템플릿 비교

flowchart LR
    subgraph recursive["재귀 템플릿 (C++11)"]
        R1[종료 조건]
        R2[재귀 호출]
        R3[본문 2~3회 반복]
    end
    subgraph fold["Fold Expression (C++17)"]
        F1[한 줄]
        F2[가독성·유지보수]
        F3[컴파일 타임 전개]
    end

재귀 vs fold: 구체적 코드 비교

Before (C++11 재귀) — 합계·타입 검사 모두 종료 조건 + 재귀 호출 2개 함수 필요:

template <typename T> T sum_r(T v) { return v; }
template <typename T, typename... Args>
T sum_r(T first, Args... rest) { return first + sum_r(rest...); }

template <typename T> struct all_int : std::is_integral<T> {};
template <typename T, typename... R>
struct all_int<T, R...> : std::bool_constant<std::is_integral_v<T> && all_int<R...>::value> {};

After (C++17 fold) — 한 줄씩:

template <typename... Args> auto sum_fold(Args... args) { return (0 + ... + args); }
template <typename... Ts> constexpr bool all_int_fold = (std::is_integral_v<Ts> && ...);

단항 fold 전개 (좌측 vs 우측)

(... + args)((a + b) + c) + d (좌측). (args + ...)a + (b + (c + d)) (우측). 뺄셈·나눗셈은 순서에 따라 결과가 달라집니다.

이 글을 읽으면

  • fold expression의 네 가지 형태(단항 좌/우, 이항 좌/우)를 완전히 이해할 수 있습니다.
  • 단항 fold, 이항 fold, 쉼표 fold, 커스텀 연산자 fold를 실전에서 사용할 수 있습니다.
  • 자주 발생하는 에러와 해결법을 알 수 있습니다.
  • 프로덕션에서 바로 쓸 수 있는 패턴을 적용할 수 있습니다.

개념을 잡는 비유

템플릿 인자 자리는 붕어빵 틀의 칸 수가 정해지듯, 컴파일 시점에 크기·상수가 박혀 있어야 하는 경우가 많습니다. constexpr·컴파일 타임 계산은 그 값을 미리 찍어내어, 배열 크기와 static_assert 같은 곳에 그대로 얹을 수 있게 해 줍니다.


목차

  1. Fold Expression 기본
  2. 단항 fold 완전 예제
  3. 이항 fold 완전 예제
  4. 쉼표 fold·커스텀 연산자
  5. 논리 연산 fold
  6. 타입 fold (타입 검사)
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 정리

1. Fold Expression 기본

fold란?

fold(접기)는 함수형 프로그래밍에서 “여러 값을 하나의 값으로 줄이는” 연산입니다. [1, 2, 3, 4, 5]+로 fold하면 1 + 2 + 3 + 4 + 5 = 15가 됩니다. C++17에서는 파라미터 팩에 이항 연산자를 적용해 fold할 수 있습니다.

네 가지 fold 형태

문법이름전개 예시 (pack = a, b, c)
(pack op ...)단항 우측 folda op (b op c)
(... op pack)단항 좌측 fold(a op b) op c
(init op ... op pack)이항 좌측 fold((init op a) op b) op c
(pack op ... op init)이항 우측 folda op (b op (c op init))

지원 연산자

fold는 다음 연산자와 함께 사용할 수 있습니다: +, -, *, /, %, ^, &, |, =, +=, -=, *=, /=, %=, ^=, &=, |=, <<, >>, >>=, <<=, ==, !=, <, >, <=, >=, &&, ||, ,, .*, ->*.

빈 팩 처리

단항 fold에서 파라미터 팩이 비어 있으면 다음 기본값으로 평가됩니다:

연산자빈 팩 시 값
&&true
||false
,void (void는 대부분 문맥에서 사용 불가)
+, -, *, &, |, ^잘못된 형식 (대부분 컴파일 에러)

이항 fold는 초기값 init이 있으므로 빈 팩도 안전하게 처리됩니다.


2. 단항 fold 완전 예제

2.1 단항 우측 fold: (pack op ...)

오른쪽부터 연산합니다. (args + ...)arg1 + (arg2 + (arg3 + arg4)) 형태로 전개됩니다.

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o unary_right unary_right.cpp && ./unary_right
#include <iostream>

template <typename... Args>
auto sum_right(Args... args) {
    return (args + ...);  // 단항 우측 fold
}

int main() {
    std::cout << sum_right(1, 2, 3, 4, 5) << "\n";  // 15
    return 0;
}

실행 결과: 15가 출력됩니다.

덧셈·곱셈처럼 결합 법칙이 있으면 좌측/우측 결과가 같습니다. 뺄셈·나눗셈은 순서에 따라 결과가 달라지므로 주의해야 합니다.

뺄셈·나눗셈 순서 차이 예시:

// 좌측: (((10 - 2) - 3) - 4) = 1
template <typename... Args>
auto sub_left(Args... args) {
    return (... - args);  // 10 - 2 - 3 - 4 = 1
}

// 우측: (10 - (2 - (3 - 4))) = 9
template <typename... Args>
auto sub_right(Args... args) {
    return (args - ...);  // 10 - (2 - (3 - 4)) = 9
}
// sub_left(10, 2, 3, 4) → 1
// sub_right(10, 2, 3, 4) → 9

산술 연산에서 결합 순서가 중요할 때는 반드시 좌측/우측을 명확히 선택해야 합니다.

2.2 단항 좌측 fold: (... op pack)

왼쪽부터 연산합니다. (... + args)(((arg1 + arg2) + arg3) + arg4) 형태로 전개됩니다.

#include <iostream>

template <typename... Args>
auto sum_left(Args... args) {
    return (... + args);  // 단항 좌측 fold
}

int main() {
    std::cout << sum_left(1, 2, 3, 4, 5) << "\n";  // 15
    return 0;
}

2.3 출력 연산자 fold (unary right fold)

std::cout에 인자를 순서대로 출력할 때 (std::cout << ... << args)를 사용합니다. <<는 우측 결합이므로 (a << (b << (c << d))) 형태가 되지만, std::cout<<의 왼쪽에 오므로 ((std::cout << a) << b) << c처럼 전개됩니다.

#include <iostream>
#include <string>

template <typename... Args>
void log(Args&&... args) {
    std::cout << "[LOG] ";
    (std::cout << ... << std::forward<Args>(args)) << "\n";
}

int main() {
    log("Server", " started on port ", 8080);
    log("User count: ", 42, ", memory: ", 128.5, " MB");
    return 0;
}

실행 결과:

[LOG] Server started on port 8080
[LOG] User count: 42, memory: 128.5 MB

2.4 문자열 연결 (ostringstream)

#include <sstream>
#include <string>
#include <iostream>

template <typename... Args>
std::string concat(Args&&... args) {
    std::ostringstream oss;
    (oss << ... << std::forward<Args>(args));
    return oss.str();
}

int main() {
    std::cout << concat("Hello, ", "World", "! ", 42) << "\n";
    // "Hello, World! 42"
    return 0;
}

3. 이항 fold 완전 예제

3.1 이항 좌측 fold: (init op ... op pack)

초기값이 왼쪽에 있고, 왼쪽부터 연산합니다. (0 + ... + args)(((0 + arg1) + arg2) + arg3) 형태입니다.

#include <iostream>

template <typename... Args>
auto sum_with_init(Args... args) {
    return (0 + ... + args);  // 이항 좌측 fold, 빈 팩 시 0
}

int main() {
    std::cout << sum_with_init(1, 2, 3) << "\n";  // 6
    std::cout << sum_with_init() << "\n";          // 0 (빈 팩)
    return 0;
}

3.2 이항 우측 fold: (pack op ... op init)

초기값이 오른쪽에 있고, 오른쪽부터 연산합니다. (args + ... + 0)arg1 + (arg2 + (arg3 + 0)) 형태입니다.

#include <iostream>

template <typename... Args>
auto sum_right_init(Args... args) {
    return (args + ... + 0);  // 이항 우측 fold
}

int main() {
    std::cout << sum_right_init(1, 2, 3) << "\n";  // 6
    return 0;
}

3.3 곱셈 fold (초기값 1)

#include <iostream>

template <typename... Args>
auto product(Args... args) {
    return (1 * ... * args);  // 빈 팩 시 1
}

int main() {
    std::cout << product(2, 3, 4) << "\n";  // 24
    std::cout << product() << "\n";         // 1
    return 0;
}

3.4 문자열 연결 (초기값 빈 문자열)

#include <iostream>
#include <string>

template <typename... Args>
std::string join(Args&&... args) {
    return (std::string{} + ... + std::string(args));  // C++17
}

int main() {
    std::cout << join("Hello", ", ", "World", "!") << "\n";
    return 0;
}

주의: std::string + const char*std::stringoperator+가 호출되므로, std::string(args)로 변환해야 합니다. std::string으로 변환 가능한 타입만 허용하려면 std::string 래퍼를 쓰거나 if constexpr로 분기할 수 있습니다. join 예제는 문자열 리터럴·std::string에만 사용하세요.


4. 쉼표 fold·커스텀 연산자

4.1 쉼표 fold 기본

쉼표 연산자 ,는 왼쪽부터 순서대로 평가하고, 마지막 표현식의 결과를 반환합니다. (expr1, expr2, expr3)expr1 실행 → expr2 실행 → expr3 실행 → expr3의 값 반환입니다.

#include <iostream>

template <typename... Args>
void print_with_space(Args... args) {
    ((std::cout << args << " "), ...) << "\n";
}

int main() {
    print_with_space(1, 2, 3);  // 1 2 3
    return 0;
}

전개: (std::cout << 1 << " ", std::cout << 2 << " ", std::cout << 3 << " ") 순서대로 실행됩니다.

4.2 쉼표 fold로 최대값

#include <iostream>

template <typename T, typename... Args>
T max_of(T first, Args... rest) {
    T result = first;
    ((result = (rest > result ? rest : result)), ...);
    return result;
}

int main() {
    std::cout << max_of(3, 1, 4, 1, 5) << "\n";  // 5
    return 0;
}

4.3 쉼표 fold로 최소값

#include <iostream>

template <typename T, typename... Args>
T min_of(T first, Args... rest) {
    T result = first;
    ((result = (rest < result ? rest : result)), ...);
    return result;
}

int main() {
    std::cout << min_of(3, 1, 4, 1, 5) << "\n";  // 1
    return 0;
}

4.4 쉼표 fold로 여러 컨테이너에 push_back

#include <vector>
#include <iostream>

template <typename Container, typename... Args>
void push_all(Container& c, Args&&... args) {
    (c.push_back(std::forward<Args>(args)), ...);
}

int main() {
    std::vector<int> vec;
    push_all(vec, 1, 2, 3, 4, 5);
    for (int x : vec) std::cout << x << " ";
    std::cout << "\n";
    return 0;
}

실행 결과: 1 2 3 4 5

4.5 커스텀 연산자 fold

operator+를 오버로드한 타입이라면 fold에서 그대로 사용할 수 있습니다. operator<<도 마찬가지입니다. 커스텀 이항 연산을 쓰려면 람다나 함수 객체를 fold로 직접 쓰는 것은 불가능하고, std::accumulate처럼 초기값 + 이항 함수 형태가 필요합니다. fold는 기본 연산자만 지원합니다.

#include <iostream>

struct Point {
    int x, y;
    Point operator+(const Point& other) const {
        return {x + other.x, y + other.y};
    }
};

template <typename... Args>
auto sum_points(Args... args) {
    return (args + ...);  // Point::operator+ 사용
}

int main() {
    Point p1{1, 2}, p2{3, 4}, p3{5, 6};
    auto total = sum_points(p1, p2, p3);
    std::cout << total.x << ", " << total.y << "\n";  // 9, 12
    return 0;
}

4.6 비트 연산 fold

#include <iostream>
#include <cstdint>

template <typename... Args>
auto bitwise_or(Args... args) {
    return (0 | ... | args);  // 이항 fold: 빈 팩 시 0
}

int main() {
    std::cout << bitwise_or(1u, 2u, 4u) << "\n";  // 7
    return 0;
}

주의: 단항 | fold에서 빈 팩은 0으로, 단항 & fold에서 빈 팩은 ~0(모든 비트 1)로 평가됩니다. 이항 fold (0 | ... | args)처럼 초기값을 명시하는 것이 안전합니다.

4.7 비트 AND fold

template <typename... Args>
auto bitwise_and(Args... args) {
    return (~0u & ... & args);  // 빈 팩 시 ~0
}

4.8 커스텀 operator<< fold

operator<<를 오버로드한 타입은 (std::cout << ... << args) fold에서 그대로 사용할 수 있습니다.


5. 논리 연산 fold

5.1 all (모두 true)

#include <iostream>

template <typename... Args>
bool all(Args... args) {
    return (... && args);  // short-circuit
}

int main() {
    std::cout << all(true, true, true) << "\n";   // 1
    std::cout << all(true, false, true) << "\n";  // 0
    std::cout << all() << "\n";                   // 1 (빈 팩)
    return 0;
}

short-circuit: all(false, foo(), bar())에서 false가 나오면 foo(), bar()는 호출되지 않습니다.

5.2 any (하나라도 true)

#include <iostream>

template <typename... Args>
bool any(Args... args) {
    return (... || args);  // short-circuit
}

int main() {
    std::cout << any(false, false, true) << "\n";  // 1
    std::cout << any(false, false, false) << "\n"; // 0
    std::cout << any() << "\n";                    // 0 (빈 팩)
    return 0;
}

5.3 none (모두 false)

template <typename... Args>
bool none(Args... args) {
    return !(... || args);
}

6. 타입 fold (타입 검사)

6.1 모든 타입이 정수형인지 검사

#include <type_traits>

template <typename... Ts>
constexpr bool all_integral = (std::is_integral_v<Ts> && ...);

static_assert(all_integral<int, long, short>);
static_assert(!all_integral<int, double>);

6.2 모든 타입이 포인터인지 검사

#include <type_traits>

template <typename... Ts>
constexpr bool all_pointers = (std::is_pointer_v<std::decay_t<Ts>> && ...);

static_assert(all_pointers<int*, double*, char*>);
static_assert(!all_pointers<int*, int>);

6.3 enable_if와 조합

#include <type_traits>
#include <iostream>

template <typename... Ts>
std::enable_if_t<(std::is_integral_v<Ts> && ...), int>
sum_integral(Ts... args) {
    return (args + ...);
}

int main() {
    std::cout << sum_integral(1, 2, 3) << "\n";  // 6
    // sum_integral(1, 2.5);  // 컴파일 에러
    return 0;
}

6.4 if constexpr와 조합

#include <type_traits>
#include <iostream>

template <typename... Ts>
auto process(Ts... args) {
    if constexpr ((std::is_integral_v<Ts> && ...)) {
        return (args + ...);
    } else {
        return 0;
    }
}

int main() {
    std::cout << process(1, 2, 3) << "\n";  // 6
    std::cout << process(1, 2.5) << "\n";   // 0
    return 0;
}

6.5 추가 타입 fold: any_pointer, all_same, all_copyable

template <typename... Ts>
constexpr bool any_pointer = (std::is_pointer_v<std::decay_t<Ts>> || ...);

template <typename T, typename... Ts>
constexpr bool all_same = (std::is_same_v<T, Ts> && ...);

template <typename... Ts>
constexpr bool all_copyable = (std::is_copy_constructible_v<Ts> && ...);

fold vs 재귀 템플릿: 언제 무엇을 쓸까?

fold가 적합한 경우

  • 단순 연산: 합, 곱, &&, ||, 출력, push_back
  • 동일 연산 반복: 모든 인자에 같은 연산을 적용할 때
  • 가독성: 한 줄로 의도가 명확할 때

재귀 템플릿이 적합한 경우

  • 인자마다 다른 처리: 첫 번째 인자만 특별히 다루거나, 인덱스에 따라 분기할 때
  • 조건부 전개: 일부 인자만 선택적으로 처리할 때
  • 복잡한 타입 계산: index_of, type_at 같은 메타 함수

성능

fold expression은 컴파일 타임에 전개되며, 재귀 템플릿과 동일하게 인라인됩니다. 런타임 성능 차이는 거의 없고, 컴파일 시간코드 가독성에서 fold가 유리합니다.

성능 비교: 재귀 vs fold

항목재귀 템플릿fold expression
런타임 성능동일 (인라인 후)동일
컴파일 시간재귀 깊이만큼 인스턴스화한 번에 전개
코드 크기종료+재귀 2개 함수1개 함수
디버그 심볼더 많음더 적음

요약: fold는 재귀 템플릿과 런타임 성능은 동일하지만, 컴파일이 더 빠르고 코드가 짧아 유지보수가 쉽습니다.


7. 자주 발생하는 에러와 해결법

에러 1: fold expression 괄호 누락

원인: fold expression은 반드시 괄호로 감싸야 합니다. 괄호 없이 args + ...만 쓰면 컴파일 에러가 납니다.

// ❌ 잘못된 코드
template <typename... Args>
auto sum(Args... args) {
    return args + ...;  // 컴파일 에러
}

// ✅ 올바른 코드
template <typename... Args>
auto sum(Args... args) {
    return (args + ...);
}

에러 2: 빈 파라미터 팩에서 단항 fold

원인: +, -, * 등은 빈 팩에 대한 기본값이 없어 컴파일 에러가 납니다.

// ❌ 잘못된 코드
template <typename... Args>
auto sum(Args... args) {
    return (args + ...);  // sum() 호출 시 에러
}

// ✅ 올바른 코드: 이항 fold로 초기값 제공
template <typename... Args>
auto sum(Args... args) {
    return (0 + ... + args);  // sum() → 0
}

에러 3: 출력 fold에서 인자 0개

원인: (std::cout << ... << args)에서 인자가 0개면 std::cout << (void) 같은 잘못된 표현이 됩니다.

// ❌ 잘못된 코드
template <typename... Args>
void log(Args... args) {
    (std::cout << ... << args) << "\n";  // log() 호출 시 에러
}

// ✅ 올바른 코드: 오버로드로 빈 호출 처리
template <typename... Args>
void log(Args... args) {
    if constexpr (sizeof...(args) > 0) {
        (std::cout << ... << args) << "\n";
    } else {
        std::cout << "\n";
    }
}

// 또는 void log() 오버로드 추가
void log() { std::cout << "\n"; }
template <typename T, typename... Args>
void log(T first, Args... rest) {
    std::cout << first;
    (std::cout << ... << rest) << "\n";
}

에러 4: 연산자 우선순위

원인: fold expression은 괄호가 없으면 주변 연산자와 우선순위가 예상과 다를 수 있습니다.

// ❌ 의도와 다를 수 있음
return args + ... + 0;  // 괄호 없음 → 파싱 에러

// ✅ 올바른 코드
return (args + ... + 0);

에러 5: 쉼표 fold에서 void 반환

원인: (expr, ...)에서 각 exprvoid를 반환하면 전체 fold도 void입니다. auto 반환 시 타입 불일치 에러가 납니다. process가 값을 반환하는 경우에만 사용하세요.

에러 6: 타입이 다른 인자 합산

원인: (args + ...)에서 int, double, std::string 등이 섞이면 operator+가 없어 에러가 납니다. std::common_type_t로 공통 타입 변환하거나, enable_if로 타입을 제한하세요.

에러 7: constexpr 맥락에서 fold

fold는 constexpr에서 사용 가능합니다. fold 안의 표현식이 constexpr이 아니면 에러가 납니다. (args + ...)에서 인자가 모두 상수면 constexpr로 평가됩니다.

에러 8: MSVC에서 fold 문법 차이

원인: 일부 MSVC 버전에서 (std::cout << ... << args) 파싱이 다를 수 있습니다. ((std::cout << args), ...) 형태로 바꾸면 해결되는 경우가 있습니다.

// 대안: 쉼표 fold
template <typename... Args>
void log(Args... args) {
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

에러 9: pack이 괄호 밖에 있는 잘못된 fold

원인: fold에서 pack은 연산자의 한쪽에만 와야 합니다. (args + args + ...)처럼 pack을 두 번 쓰면 에러입니다.

// ❌ 잘못된 코드
template <typename... Args>
auto wrong(Args... args) {
    return (args + args + ...);  // pack 중복 사용 → 에러
}

// ✅ 올바른 코드
template <typename... Args>
auto right(Args... args) {
    return (args + ...);
}

에러 10: init과 pack의 타입 불일치

원인: 이항 fold (init op ... op pack)에서 init과 pack 요소의 타입이 호환되지 않으면 연산자 오버로드 해석이 실패합니다.

// ❌ 잘못된 코드
template <typename... Args>
auto sum_mixed(Args... args) {
    return (std::string{} + ... + args);  // args가 int면 에러
}

// ✅ 올바른 코드: 공통 타입 사용
template <typename... Args>
auto sum_strings(Args&&... args) {
    return (std::string{} + ... + std::string(args));
}

8. 베스트 프랙티스

1. 빈 팩을 고려해 이항 fold 사용

인자가 0개일 수 있으면 이항 fold로 초기값을 명시합니다. (0 + ... + args), (true && ... && args) 등.

// ✅ 권장
template <typename... Args>
auto sum(Args... args) {
    return (0 + ... + args);
}

2. 출력 fold는 오버로드로 빈 호출 처리

log()처럼 인자 없이 호출될 수 있으면 void log() 오버로드를 두거나 if constexpr (sizeof...(args) > 0)로 분기합니다.

void log() { std::cout << "\n"; }
template <typename T, typename... Args>
void log(T first, Args... rest) {
    std::cout << first;
    (std::cout << ... << rest) << "\n";
}

3. 타입 검사는 fold로 간소화

enable_if로 길게 쓰던 조건을 (std::is_integral_v<Ts> && ...)로 한 줄로 대체합니다.

template <typename... Ts>
std::enable_if_t<(std::is_integral_v<Ts> && ...), int>
sum_all(Ts... args) {
    return (args + ...);
}

4. 논리 fold는 short-circuit 활용

(... && args)(... || args)는 short-circuit이 있어, 앞에서 결과가 정해지면 나머지는 평가하지 않습니다. 성능과 안전성에 유리합니다.

5. 쉼표 fold로 부수 효과

여러 컨테이너에 push_back, 여러 lock_guard 생성 등 “순서대로 실행”만 필요할 때 쉼표 fold를 사용합니다.

(push_all(vec1, args), ...);
// 또는
(vec1.push_back(args), ...);

6. 괄호는 항상 명시

fold expression은 반드시 (expr) 형태로 괄호로 감쌉니다. args + ...만 쓰면 파싱 에러가 납니다.

7. std::forward로 완벽 전달

출력 fold나 push_back fold에서 인자를 다른 함수에 넘길 때는 std::forward를 사용해 rvalue 참조를 보존합니다.

template <typename... Args>
void log(Args&&... args) {
    (std::cout << ... << std::forward<Args>(args)) << "\n";
}

template <typename Container, typename... Args>
void push_all(Container& c, Args&&... args) {
    (c.push_back(std::forward<Args>(args)), ...);
}

8. 산술 fold에서 결합 순서 확인

뺄셈·나눗셈은 (a - b) - c ≠ a - (b - c)이므로, 의도한 순서에 맞는 좌측/우측 fold를 선택합니다.

9. 타입 fold는 decay 적용

const int*, int* const 등 수정자를 무시하고 검사하려면 std::decay_t<Ts>를 사용합니다.

template <typename... Ts>
constexpr bool all_pointers = (std::is_pointer_v<std::decay_t<Ts>> && ...);

체크리스트

  • 빈 팩 가능 여부 확인 → 이항 fold 또는 오버로드
  • 출력 fold는 log() 오버로드 또는 sizeof...(args) > 0 분기
  • 타입 검사는 (traits && ...) fold로 간소화
  • 논리 fold는 short-circuit 활용
  • 모든 fold expression에 괄호

9. 프로덕션 패턴

패턴 1: 로깅 유틸리티 (완전판)

#include <iostream>
#include <chrono>
#include <iomanip>

void log() { std::cout << "\n"; }

template <typename T, typename... Args>
void log(T first, Args&&... rest) {
    auto now = std::chrono::system_clock::now();
    auto t = std::chrono::system_clock::to_time_t(now);
    std::cout << "[" << std::put_time(std::localtime(&t), "%H:%M:%S") << "] ";
    std::cout << first;
    (std::cout << ... << std::forward<Args>(rest)) << "\n";
}

int main() {
    log("Server", " started on port ", 8080);
    log("User count: ", 42);
    return 0;
}

패턴 2: 타입 안전한 printf

#include <iostream>
#include <sstream>
#include <string>

template <typename... Args>
std::string format(Args&&... args) {
    std::ostringstream oss;
    (oss << ... << std::forward<Args>(args));
    return oss.str();
}

// 사용
// format("User ", id, " has ", count, " items");

패턴 3: 가변 인자 min/max

template <typename T, typename... Args>
constexpr T min_of(T first, Args... rest) {
    T result = first;
    ((result = (rest < result ? rest : result)), ...);
    return result;
}

template <typename T, typename... Args>
constexpr T max_of(T first, Args... rest) {
    T result = first;
    ((result = (rest > result ? rest : result)), ...);
    return result;
}

패턴 4: all_of / any_of 타입 버전

#include <type_traits>

template <typename... Ts>
constexpr bool all_integral = (std::is_integral_v<Ts> && ...);

template <typename... Ts>
constexpr bool any_pointer = (std::is_pointer_v<std::decay_t<Ts>> || ...);

패턴 5: 여러 컨테이너에 동시 삽입

template <typename Container, typename... Args>
void emplace_all(Container& c, Args&&... args) {
    (c.emplace_back(std::forward<Args>(args)), ...);
}

패턴 6: 직렬화 (순서대로 쓰기)

template <typename Stream, typename... Args>
void serialize(Stream& out, const Args&... args) {
    (out.write(reinterpret_cast<const char*>(&args), sizeof(args)), ...);
}

패턴 7: 조건부 컴파일 (타입 검사)

template <typename... Ts>
struct AllSame;

template <typename T>
struct AllSame<T> : std::true_type {};

template <typename T, typename U, typename... Rest>
struct AllSame<T, U, Rest...>
    : std::bool_constant<std::is_same_v<T, U> && AllSame<T, Rest...>::value> {};

// fold 버전
template <typename T, typename... Ts>
constexpr bool all_same = (std::is_same_v<T, Ts> && ...);

패턴 8: 가변 인자 검증

template <typename Validator, typename... Args>
bool validate_all(Validator&& v, Args&&... args) {
    return (v(std::forward<Args>(args)) && ...);
}

패턴 9: 여러 스트림에 동시 쓰기 (Tee)

template <typename... Streams>
struct Tee {
    std::tuple<Streams&...> streams;
    explicit Tee(Streams&... s) : streams(s...) {}
    template <typename T>
    Tee& operator<<(const T& value) {
        std::apply([&value](Streams&... s) { ((s << value), ...); }, streams);
        return *this;
    }
};

패턴 10: 옵션 플래그 조합

template <typename... Options>
constexpr unsigned combine_flags(Options... opts) {
    return (static_cast<unsigned>(opts) | ...);
}

10. 정리

요약 표

항목내용
단항 우측(pack op ...)a op (b op c)
단항 좌측(... op pack)(a op b) op c
이항 좌측(init op ... op pack)((init op a) op b) op c
이항 우측(pack op ... op init)a op (b op (c op init))
빈 팩AND는 true, OR은 false, 산술 연산자(+, -, *)는 이항 fold로 초기값 제공
쉼표 fold(expr, ...)로 순서대로 부수 효과
타입 fold(std::is_integral_v<Ts> && ...)로 타입 검사

한 줄 요약

fold expression은 파라미터 팩 전체에 이항 연산자를 “접어” 한 줄로 연산하는 C++17 문법입니다. 단항·이항·쉼표 fold를 활용해 로그·타입 검사·합산·최대/최소·문자열 연결 등을 재귀 없이 간결하게 작성할 수 있습니다.

다음 단계

  • 가변 인자 템플릿 (#09-3) — 파라미터 팩 기초
  • 메타프로그래밍 진화 (#44-3) — constexpr·if·fold 통합
  • constexpr 기초 (#43-1) — 컴파일 타임 계산

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression
  • C++ 메타프로그래밍의 진화: Template에서 Constexpr, 그리고 Reflection까지
  • C++ constexpr 완벽 가이드 | 컴파일 타임 계산·if constexpr·consteval 실전

이 글에서 다루는 키워드 (관련 검색어)

C++ fold expression, 단항 fold, 이항 fold, 쉼표 fold, 파라미터 팩, 가변 인자 템플릿, variadic templates, C++17, 메타프로그래밍 등으로 검색하시면 이 글이 도움이 됩니다.


자주 묻는 질문 (FAQ)

Q. 단항 fold와 이항 fold 중 언제 무엇을 쓰나요?

A. 인자가 0개일 수 있으면 이항 fold로 초기값을 명시합니다. (0 + ... + args), (true && ... && cond) 등. 인자가 항상 1개 이상이면 단항 fold도 가능합니다.

Q. (std::cout << ... << args)에서 인자가 0개면?

A. 컴파일 에러가 납니다. void log() 오버로드를 두거나 if constexpr (sizeof...(args) > 0)로 분기하세요.

Q. fold는 컴파일 타임에 전개되나요?

A. 네. fold expression은 컴파일 타임에 파라미터 팩이 전개되어, 런타임에는 일반 연산처럼 동작합니다. 인라인 최적화가 잘 됩니다.

Q. 커스텀 이항 함수(람다)를 fold에 쓸 수 있나요?

A. fold는 기본 연산자만 지원합니다. 람다나 함수 객체를 쓰려면 std::accumulate처럼 반복문이 필요합니다. 또는 (f(args), ...) 형태로 쉼표 fold에 f 호출을 넣을 수 있지만, 이때는 “마지막 반환값”만 의미가 있고, fold의 “합” 의미는 없습니다.

참고 자료



한 줄 요약: fold expression으로 파라미터 팩 전체에 연산을 한 줄로 적용할 수 있습니다. 단항·이항·쉼표 fold를 활용해 로그·타입 검사·합산·최대/최소를 재귀 없이 간결하게 작성하세요.

다음 글: C++ 메타프로그래밍 진화 (#44-3)

이전 글: C++ 가변 인자 템플릿 (#09-3)


관련 글

  • C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression
  • C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들 [#44-1]
  • C++ SFINAE 완벽 가이드 | enable_if·void_t
  • C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체 [#44-2]
  • C++ 메타프로그래밍의 진화: Template에서 Constexpr, 그리고 Reflection까지