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_if나 if 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 같은 곳에 그대로 얹을 수 있게 해 줍니다.
목차
- Fold Expression 기본
- 단항 fold 완전 예제
- 이항 fold 완전 예제
- 쉼표 fold·커스텀 연산자
- 논리 연산 fold
- 타입 fold (타입 검사)
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 정리
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 ...) | 단항 우측 fold | a 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) | 이항 우측 fold | a 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::string의 operator+가 호출되므로, 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, ...)에서 각 expr이 void를 반환하면 전체 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까지