C++ 제네릭 람다 | auto 매개변수·템플릿 람다(C++20) 완전 정리

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 표준의 요지는 다음과 같습니다.

  1. 제네릭 람다의 auto 매개변수마다 고유한 템플릿 타입 매개변수가 도입됩니다.
  2. 람다의 반환 타입이 명시되지 않았다면, 반환은 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의 타입이 vectorvalue_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() 인스턴스가 생깁니다).


타입 추론 규칙

  1. 매개변수 auto: 함수 템플릿의 auto 매개변수와 같은 계열로, 전달된 인자로 템플릿 인수가 치환됩니다.
  2. auto& / const auto&: 각각 좌값·상수성을 유지하려는 의도입니다. const auto&는 임시 객체도 받을 수 있어 범용적입니다.
  3. auto&&: 전달 참조 규칙이 적용되어, 임시와 좌값을 모두 효율적으로 받을 수 있습니다(제네릭 람다에서 “완벽 전달”할 때).
  4. 반환 타입: 명시하지 않으면 본문의 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 람다 참고.


자주 하는 실수

  1. auto로만 받고 수정하려는 경우: 값 복사본을 바꿔도 원본 컨테이너 요소는 안 바뀝니다. 수정하려면 auto& 또는 auto* 등을 씁니다.
  2. 서로 다른 auto 매개변수: auto a, auto b서로 다른 템플릿 매개변수입니다. 같은 타입을 강제하려면 C++20 템플릿 람다로 template<typename T> ... (T a, T b) 형태가 낫습니다.
  3. 재귀: 이름 없는 람다를 자기 자신에서 부르기 어렵습니다. 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) 핵심 문법 치트시트