C++ 제네릭 람다 | auto 매개변수·템플릿 람다(C++20) 완전 정리
이 글의 핵심
제네릭 람다는 매개변수에 auto를 쓰면 컴파일러가 클로저의 호출 연산자를 템플릿으로 만듭니다. C++20 템플릿 람다, STL과의 조합, 추론 규칙·성능까지 정리합니다.
제네릭 람다란?
제네릭 람다(generic lambda)는 C++14에서 도입된 기능으로, 람다의 매개변수에 auto를 사용해 여러 타입에 대해 같은 본문을 재사용할 수 있게 합니다. 내부적으로는 클로저 타입의 operator()가 함수 템플릿이 됩니다.
auto add = [](auto a, auto b) { return a + b; };
int x = add(1, 2); // int
double y = add(1.5, 2.5); // double
왜 필요한가?
- 중복 제거:
int용·double용 람다를 따로 쓰지 않아도 됨. - STL과 궁합:
std::sort,std::find_if등에 한 번 정의한 비교·조건을 넘기기 좋음. - C++20 이후: 더 명시적인 템플릿 람다 문법과 자연스럽게 이어짐.
람다 기초와 auto 타입 추론을 먼저 보면 이 글이 더 수월합니다.
일반 람다 vs 제네릭 람다
일반 람다 (비템플릿 operator())
매개변수 타입을 고정하면, 클로저의 operator()는 일반 멤버 함수 하나입니다.
auto cmp = [](int a, int b) { return a < b; };
// 개념적으로: struct __lambda { bool operator()(int a, int b) const; };
한 타입에만 맞고, double이나 std::string에 그대로 쓰려면 다른 람다를 새로 써야 합니다.
제네릭 람다 (auto 매개변수)
매개변수에 auto(또는 auto&, const auto& 등)를 쓰면 operator()가 함수 템플릿이 됩니다.
auto cmp = [](auto a, auto b) { return a < b; };
// 개념적으로: struct __lambda {
// template<typename T, typename U>
// auto operator()(T a, U b) const { return a < b; }
// };
같은 람다 객체로 int, double, 사용자 정의 타입에 operator<가 있으면 모두 정렬·비교에 쓸 수 있습니다.
| 구분 | 일반 람다 | 제네릭 람다 |
|---|---|---|
| 매개변수 | 구체 타입 | auto / decltype(auto) 등 |
operator() | 비템플릿 | 함수 템플릿 |
| 인스턴스 | 1개 | 호출마다 필요한 특화가 생성 |
| 가독성 | 단순한 경우 명확 | “여러 타입 허용” 의도가 드러남 |
auto 매개변수의 동작 원리
C++14 표준의 요지는 다음과 같습니다.
- 제네릭 람다의 각
auto매개변수마다 고유한 템플릿 타입 매개변수가 도입됩니다. - 람다의 반환 타입이 명시되지 않았다면, 반환은
decltype(auto)와 유사한 규칙으로 본문return표현식에서 추론됩니다(일반 람다와 같은 원리).
[](auto x) { return x * 2; } // x의 타입에 따라 템플릿 인스턴스 생성
[](auto& x) { return x; } // 참조로 받아 복사 없이 읽기/쓰기
주의: auto는 값 복사입니다. 큰 객체를 매번 복사하지 않으려면 const auto& 또는 auto&&(전달 참조에 가까운 추론)를 고려합니다. 완벽 전달 패턴과 맞물립니다.
std::vector<std::string> v = {"a", "b"};
std::for_each(v.begin(), v.end(), [](const auto& s) {
std::cout << s << '\n'; // 불필요한 복사 방지
});
컴파일러가 생성하는 클로저는 구현별 고유 타입이지만, 핵심은 operator()가 템플릿이라는 점입니다. 그래서 템플릿 인스턴스가 호출 패턴만큼 생기며, 링크되지 않은 TU마다 중복 생성될 수 있습니다(일반 함수 템플릿과 동일한 성격).
C++20 템플릿 람다
C++20에서는 제네릭 람다와 동일한 목적을 템플릿 문법으로 쓸 수 있습니다.
auto f = []<typename T>(T x) { return x + x; }; // 단일 타입 매개변수
auto g = []<class T, class U>(T a, U b) { return a < b; };
차이와 장점
- 명시적 타입 매개변수:
T가 무엇인지 이름으로 드러남. requires제약(C++20 개념)을 람다에 직접 걸기 쉬움.
auto clamp_positive = []<typename T>(T x) requires std::is_arithmetic_v<T> {
return x > T{0} ? x : T{0};
};
auto 매개변수만 쓸 때는 익명 템플릿 매개변수에 제약을 거는 방식이 번거로울 수 있는데, 템플릿 람다는 requires와 조합하기 좋습니다. 자세한 문법은 C++ 템플릿 람다 글과 제네릭 람다·오류 메시지도 참고하세요.
실전 활용: STL 알고리즘
정렬·비교
std::vector<std::pair<int, std::string>> items = {{2, "b"}, {1, "a"}};
std::sort(items.begin(), items.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
첫 번째 요소 기준 정렬에서 pair의 타입을 적을 필요가 없어집니다.
조건 검색
auto it = std::find_if(vec.begin(), vec.end(),
[](const auto& e) { return e.id == target_id; });
e의 타입이 vector의 value_type으로 추론됩니다.
변환·누적
std::transform(a.begin(), a.end(), out.begin(),
[](auto x) { return std::abs(x); });
int sum = std::accumulate(v.begin(), v.end(), 0,
[](auto acc, const auto& x) { return acc + x.size(); });
std::visit과 가변체
std::visit([](const auto& x) { std::cout << x; }, my_variant);
std::variant의 각 대안 타입에 대해 한 람다 본문으로 처리할 수 있습니다(각 대안마다 다른 operator() 인스턴스가 생깁니다).
타입 추론 규칙
- 매개변수
auto: 함수 템플릿의auto매개변수와 같은 계열로, 전달된 인자로 템플릿 인수가 치환됩니다. auto&/const auto&: 각각 좌값·상수성을 유지하려는 의도입니다.const auto&는 임시 객체도 받을 수 있어 범용적입니다.auto&&: 전달 참조 규칙이 적용되어, 임시와 좌값을 모두 효율적으로 받을 수 있습니다(제네릭 람다에서 “완벽 전달”할 때).- 반환 타입: 명시하지 않으면 본문의
return들이 서로 다른 타입이면 컴파일 오류입니다.if분기 양쪽에서 다른 타입을 반환하면 공통 타입 변환이 필요합니다.
// 오류 가능: 조건에 따라 int와 double
// [](auto x) { return cond ? 0 : 1.0; } // 추론 충돌
필요하면 명시적 반환 타입을 람다에 붙입니다.
[](auto x) -> double { return x * 1.0; }
성능 고려사항
- 오버헤드: 람다 호출 자체는 인라인 가능한 작은 함수와 같습니다. 제네릭이어도 가상 호출이 아닙니다(표준 람다 클로저는 다형성 없음).
- 코드 크기: 호출 인자 타입 조합마다 템플릿 인스턴스가 생깁니다. 서로 다른 타입에 수백 번 다른 시그니처로 쓰면 바이너리 팽창이 날 수 있어, 일반 함수나 단일 타입 람다가 나을 수 있습니다.
- 캡처:
[=],[&]등 캡처한 상태는 클로저 객체 크기에 들어갑니다. 제네릭 여부와 무관합니다. - 최적화: 컴파일러는
std::sort의 비교 람다를 강하게 인라인하는 경우가 많습니다. 프로파일 전에는 “제네릭이라 느리다”고 단정하지 말고 측정하는 것이 좋습니다.
decltype과 제네릭 람다
매개변수 이름 x에 대해 **decltype(x)**는 호출 시점의 실제 인자 타입을 반영합니다. 본문에서 “x와 같은 타입의 변수”가 필요할 때 유용합니다.
auto f = [](auto x) -> decltype(x) {
decltype(x) copy = x;
return copy;
};
decltype 가이드와 함께 보면, 반환 타입을 decltype(auto)로 두는 람다와의 차이도 정리하기 쉽습니다.
constexpr 제네릭 람다 (C++17)
람다가 **constexpr**로 표시되고, 본문이 상수 표현식 요건을 만족하면 컴파일 타임에 호출될 수 있습니다. 제네릭 람다도 마찬가지로, 인자가 리터럴 등이면 템플릿 인스턴스가 constexpr 호출로 평가될 수 있습니다.
constexpr auto sq = [](auto x) { return x * x; };
static_assert(sq(3) == 9);
다만 std::string 등 비트라이셜 타입을 인자로 쓰는 경우는 런타임이 됩니다. constexpr 람다 참고.
자주 하는 실수
auto로만 받고 수정하려는 경우: 값 복사본을 바꿔도 원본 컨테이너 요소는 안 바뀝니다. 수정하려면auto&또는auto*등을 씁니다.- 서로 다른
auto매개변수:auto a, auto b는 서로 다른 템플릿 매개변수입니다. 같은 타입을 강제하려면 C++20 템플릿 람다로template<typename T> ... (T a, T b)형태가 낫습니다. - 재귀: 이름 없는 람다를 자기 자신에서 부르기 어렵습니다.
std::function에 넣거나 Y 결합자 패턴, 또는 네임드 함수 객체를 고려합니다.
요약
| 키워드 | 설명 |
|---|---|
| 제네릭 람다 | auto 매개변수 → operator()가 템플릿 |
| C++20 | []<typename T>(T x){} 형태로 명시적 템플릿 람다 |
| STL | 정렬, 검색, 변환, visit에서 타입 반복 제거 |
| 성능 | 인라인·비가상; 인스턴스 수는 타입 수에 비례 |
관련 글: 람다 캡처, constexpr 람다, decltype.
같이 보면 좋은 글 (내부 링크)
- C++ 템플릿 람다
- C++ 람다 캡처
- C++ auto 타입 추론
관련 글
- C++ 람다 완전 정리
- 모던 C++ (C++11~C++20) 핵심 문법 치트시트