본문으로 건너뛰기 C++ Fold Expressions 이해하기: 단항·이항부터 C++26 Pack Indexing까지

C++ Fold Expressions 이해하기: 단항·이항부터 C++26 Pack Indexing까지

C++ Fold Expressions 이해하기: 단항·이항부터 C++26 Pack Indexing까지

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

구체적인 문제 시나리오

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

  • 여러 인자의 합을 구하려면 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은 파라미터 팩 전체에 이항 연산자를 “접어 나가는” 문법을 제공해 한 줄로 간결하게 작성할 수 있습니다.

추가 문제 시나리오

타입 리스트 검사

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

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

여러 필드가 모두 유효한지 검사할 때, (validate(f1) && validate(f2) && ...)처럼 fold로 &&를 연결하면 short-circuit으로 앞에서 결과가 정해지면 나머지는 평가하지 않아 효율적입니다. 프로토콜 버퍼 직렬화

여러 필드를 순서대로 직렬화할 때, (serialize(buf, args), ...)처럼 comma fold로 각 인자마다 serialize를 호출할 수 있습니다. 반복문은 런타임에만 쓰이지만, fold는 컴파일 타임에 전개되어 인라인 최적화가 잘 됩니다. 단위 테스트용 all/any

assert(all(cond1, cond2, cond3))처럼 여러 조건이 모두 참인지, 하나라도 참인지 검사할 때 (... && args)(... || args) fold로 간단히 표현할 수 있습니다. 가변 인자 apply

apply(f, a, b, c)처럼 함수 f에 인자 a, b, c를 순서대로 넘기는 함수를 만들 때, 재귀 없이 std::invoke와 fold를 조합할 수 있습니다 (C++17 std::apply가 이미 제공). 플래그 마스크 OR

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

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

여러 뮤텍스를 순서대로 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 기본

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를 활용해 로그·타입 검사·합산·최대/최소·문자열 연결 등을 재귀 없이 간결하게 작성할 수 있습니다.

다음 단계


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

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


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

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)

관련 글