C++ Template Lambda | "템플릿 람다" 가이드
이 글의 핵심
C++ Template Lambda에 대한 실전 가이드입니다.
들어가며
C++20의 Template Lambda는 람다에 명시적 템플릿 매개변수를 지정할 수 있게 해줍니다. 타입 제어와 Concepts 활용이 가능합니다.
1. Template Lambda 기본
auto vs Template Lambda
#include <iostream>
#include <typeinfo>
int main() {
// C++14: auto 람다 (Generic Lambda)
auto addAuto = {
return a + b;
};
// 각 매개변수가 독립적인 타입
std::cout << addAuto(1, 2) << std::endl; // int + int
std::cout << addAuto(1, 2.5) << std::endl; // int + double
std::cout << addAuto(1.5, 2.5) << std::endl; // double + double
// C++20: Template Lambda
auto addTemplate = []<typename T>(T a, T b) {
return a + b;
};
// 두 매개변수가 같은 타입이어야 함
std::cout << addTemplate(1, 2) << std::endl; // OK: int + int
std::cout << addTemplate(1.5, 2.5) << std::endl; // OK: double + double
// addTemplate(1, 2.5); // 컴파일 에러: 타입 불일치
}
기본 사용
#include <iostream>
#include <typeinfo>
int main() {
// 템플릿 람다
auto print = []<typename T>(const T& value) {
std::cout << "타입: " << typeid(T).name()
<< ", 값: " << value << std::endl;
};
print(42); // 타입: int, 값: 42
print(3.14); // 타입: double, 값: 3.14
print("Hello"); // 타입: char const*, 값: Hello
}
핵심 개념:
- 명시적 타입:
<typename T>구문으로 템플릿 지정 - 타입 제어: 매개변수 간 타입 관계 명시
- Concepts 지원: 타입 제약 가능
2. 타입 제약 (Concepts)
Concepts로 타입 제한
#include <iostream>
#include <concepts>
int main() {
// 정수 타입만 허용
auto addInts = []<std::integral T>(T a, T b) {
return a + b;
};
std::cout << addInts(1, 2) << std::endl; // OK: int
std::cout << addInts(10L, 20L) << std::endl; // OK: long
// addInts(1.5, 2.5); // 컴파일 에러: double은 integral 아님
// 부동소수점 타입만 허용
auto addFloats = []<std::floating_point T>(T a, T b) {
return a + b;
};
std::cout << addFloats(1.5, 2.5) << std::endl; // OK: double
std::cout << addFloats(1.5f, 2.5f) << std::endl; // OK: float
// addFloats(1, 2); // 컴파일 에러: int는 floating_point 아님
}
커스텀 Concepts
#include <iostream>
#include <concepts>
// 커스텀 Concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
int main() {
auto multiply = []<Numeric T>(T a, T b) {
return a * b;
};
std::cout << multiply(3, 4) << std::endl; // 12 (int)
std::cout << multiply(3.5, 2.0) << std::endl; // 7.0 (double)
// multiply("a", "b"); // 컴파일 에러: string은 Numeric 아님
}
3. 실전 예제
예제 1: 여러 템플릿 매개변수
#include <iostream>
int main() {
// 타입 변환 람다
auto convert = []<typename From, typename To>(From value) {
return static_cast<To>(value);
};
// 명시적 타입 지정
auto result1 = convert.operator()<int, double>(10);
std::cout << result1 << std::endl; // 10.0
auto result2 = convert.operator()<double, int>(3.14);
std::cout << result2 << std::endl; // 3
// 타입 추론 (From만)
auto toInt = []<typename From>(From value) {
return static_cast<int>(value);
};
std::cout << toInt(3.14) << std::endl; // 3
}
예제 2: 컨테이너 처리
#include <iostream>
#include <vector>
#include <list>
#include <typeinfo>
int main() {
auto printContainer = []<typename Container>(const Container& c) {
using ValueType = typename Container::value_type;
std::cout << "컨테이너 타입: " << typeid(Container).name() << std::endl;
std::cout << "요소 타입: " << typeid(ValueType).name() << std::endl;
std::cout << "요소: ";
for (const auto& item : c) {
std::cout << item << " ";
}
std::cout << std::endl;
};
std::vector<int> vec = {1, 2, 3, 4, 5};
printContainer(vec);
std::list<double> lst = {1.1, 2.2, 3.3};
printContainer(lst);
}
예제 3: 파라미터 팩
#include <iostream>
int main() {
// 가변 인자 합계
auto sum = []<typename... Ts>(Ts... values) {
return (values + ...); // Fold expression
};
std::cout << sum(1, 2, 3) << std::endl; // 6
std::cout << sum(1.5, 2.5, 3.5) << std::endl; // 7.5
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // 15
// 가변 인자 출력
auto print = []<typename... Ts>(Ts... values) {
((std::cout << values << " "), ...);
std::cout << std::endl;
};
print(1, 2, 3); // 1 2 3
print("Hello", 42, 3.14); // Hello 42 3.14
}
예제 4: 컨테이너 변환
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 컨테이너 변환 람다
auto transform = []<typename Container, typename Func>(
const Container& input,
Func func
) {
using ValueType = typename Container::value_type;
Container output;
for (const auto& item : input) {
output.push_back(func(item));
}
return output;
};
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 각 요소를 2배로
auto doubled = transform(numbers, { return x * 2; });
for (int n : doubled) {
std::cout << n << " "; // 2 4 6 8 10
}
std::cout << std::endl;
}
4. auto vs Template Lambda
비교 예제
#include <iostream>
int main() {
// auto: 각 매개변수 독립적
auto func1 = {
std::cout << "a: " << typeid(a).name()
<< ", b: " << typeid(b).name() << std::endl;
return a + b;
};
func1(1, 2); // OK: int, int
func1(1, 2.0); // OK: int, double
func1(1.5, 2); // OK: double, int
// Template Lambda: 같은 타입
auto func2 = []<typename T>(T a, T b) {
std::cout << "타입: " << typeid(T).name() << std::endl;
return a + b;
};
func2(1, 2); // OK: 둘 다 int
func2(1.5, 2.5); // OK: 둘 다 double
// func2(1, 2.0); // 컴파일 에러: int vs double
}
비교표
| 특징 | auto 람다 | Template Lambda |
|---|---|---|
| 매개변수 타입 | 각각 독립적 | 명시적 제어 |
| 타입 제약 | 불가능 | Concepts 사용 가능 |
| 명시적 호출 | 불가능 | 가능 |
| C++ 버전 | C++14 | C++20 |
5. 자주 발생하는 문제
문제 1: 타입 불일치
#include <iostream>
int main() {
// ❌ 타입 불일치
auto add = []<typename T>(T a, T b) {
return a + b;
};
// add(1, 2.0); // 컴파일 에러: int vs double
// ✅ 해결 방법 1: 여러 타입 매개변수
auto add2 = []<typename T, typename U>(T a, U b) {
return a + b;
};
std::cout << add2(1, 2.0) << std::endl; // OK: 3.0
// ✅ 해결 방법 2: 공통 타입 사용
auto add3 = []<typename T>(T a, T b) -> decltype(a + b) {
return a + b;
};
std::cout << add3(1, 2) << std::endl; // OK: 3
}
문제 2: 명시적 타입 지정
#include <iostream>
int main() {
auto func = []<typename T>(T value) {
return value * 2;
};
// 타입 추론
auto r1 = func(10); // T = int
auto r2 = func(3.14); // T = double
// 명시적 타입 지정
auto r3 = func.operator()<int>(10);
auto r4 = func.operator()<double>(10); // int를 double로 변환
std::cout << r3 << std::endl; // 20
std::cout << r4 << std::endl; // 20.0
}
문제 3: Concepts 제약 에러
#include <iostream>
#include <concepts>
int main() {
auto process = []<std::integral T>(T value) {
return value * 2;
};
std::cout << process(10) << std::endl; // OK: 20
// process(3.14); // 컴파일 에러
}
에러 메시지:
error: no matching function for call to 'operator()(double)'
note: constraints not satisfied
문제 4: 반환 타입 추론
#include <iostream>
#include <type_traits>
int main() {
// ❌ 반환 타입이 다를 수 있음
auto func = []<typename T>(T value) {
if constexpr (std::is_integral_v<T>) {
return value * 2; // int
} else {
return value * 2.0; // double
}
};
// 컴파일 에러: 반환 타입 불일치
// ✅ 명시적 반환 타입
auto func2 = []<typename T>(T value) -> double {
if constexpr (std::is_integral_v<T>) {
return value * 2.0;
} else {
return value * 2.0;
}
};
std::cout << func2(10) << std::endl; // 20.0
std::cout << func2(3.14) << std::endl; // 6.28
}
6. 고급 활용 패턴
패턴 1: 제네릭 알고리즘
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
// 제네릭 정렬 람다
auto sortContainer = []<typename Container>(Container& c) {
std::sort(c.begin(), c.end());
};
std::vector<int> nums = {5, 2, 8, 1, 9};
sortContainer(nums);
for (int n : nums) {
std::cout << n << " "; // 1 2 5 8 9
}
std::cout << std::endl;
}
패턴 2: 팩토리 패턴
#include <iostream>
#include <memory>
#include <vector>
int main() {
// 제네릭 팩토리
auto makeUnique = []<typename T, typename... Args>(Args&&... args) {
return std::make_unique<T>(std::forward<Args>(args)...);
};
auto ptr1 = makeUnique.operator()<std::vector<int>>(10, 42);
std::cout << "크기: " << ptr1->size() << std::endl; // 10
auto ptr2 = makeUnique.operator()<int>(100);
std::cout << "값: " << *ptr2 << std::endl; // 100
}
패턴 3: 조건부 처리
#include <iostream>
#include <type_traits>
#include <string>
int main() {
auto stringify = []<typename T>(const T& value) -> std::string {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return value;
} else {
return "unknown";
}
};
std::cout << stringify(42) << std::endl; // "42"
std::cout << stringify(3.14) << std::endl; // "3.140000"
std::cout << stringify(std::string("Hello")) << std::endl; // "Hello"
}
7. 실전 예제: 제네릭 유틸리티
#include <iostream>
#include <vector>
#include <algorithm>
#include <concepts>
// 제네릭 유틸리티 모음
class Utils {
public:
// 컨테이너 필터링
static auto filter = []<typename Container, typename Predicate>(
const Container& input,
Predicate pred
) {
Container output;
std::copy_if(input.begin(), input.end(),
std::back_inserter(output), pred);
return output;
};
// 컨테이너 변환
static auto map = []<typename Container, typename Func>(
const Container& input,
Func func
) {
Container output;
std::transform(input.begin(), input.end(),
std::back_inserter(output), func);
return output;
};
// 숫자 범위 생성
static auto range = []<std::integral T>(T start, T end) {
std::vector<T> result;
for (T i = start; i < end; i++) {
result.push_back(i);
}
return result;
};
};
int main() {
// 범위 생성
auto numbers = Utils::range(1, 10);
// 짝수 필터링
auto evens = Utils::filter(numbers, { return n % 2 == 0; });
// 제곱으로 변환
auto squared = Utils::map(evens, { return n * n; });
std::cout << "결과: ";
for (int n : squared) {
std::cout << n << " "; // 4 16 36 64
}
std::cout << std::endl;
}
정리
핵심 요약
- Template Lambda: C++20, 명시적 템플릿 매개변수
- Concepts: 타입 제약 (
std::integral,std::floating_point) - 타입 제어: 매개변수 간 타입 관계 명시
- 파라미터 팩: 가변 인자 템플릿
- 명시적 호출:
.operator()<T>()로 타입 지정
auto vs Template Lambda
| 사용 사례 | auto 람다 | Template Lambda |
|---|---|---|
| 각 매개변수 타입 다름 | ✅ | ❌ |
| 같은 타입 강제 | ❌ | ✅ |
| Concepts 제약 | ❌ | ✅ |
| 명시적 타입 지정 | ❌ | ✅ |
실전 팁
-
언제 사용할까
- 매개변수 간 타입 관계가 중요할 때
- Concepts로 타입 제약이 필요할 때
- 명시적 타입 지정이 필요할 때
-
성능
- 일반 템플릿 함수와 동일한 성능
- 인라인 최적화 가능
- 런타임 오버헤드 없음
-
디버깅
- 컴파일 에러 메시지가 명확함
- Concepts로 에러 메시지 개선
typeid로 타입 확인
다음 단계
- C++ Generic Lambda
- C++ constexpr Lambda
- C++ Concepts
관련 글
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기