본문으로 건너뛰기 C++ Template Lambda | '템플릿 람다' 가이드

C++ Template Lambda | '템플릿 람다' 가이드

C++ Template Lambda | '템플릿 람다' 가이드

이 글의 핵심

C++ Template Lambda - "템플릿 람다" 가이드. C++ Template Lambda의 2. 타입 제약 (Concepts), 3. 실전 예제를 실전 코드와 함께 설명합니다.

들어가며

C++20의 Template Lambda는 람다에 명시적 템플릿 매개변수를 지정할 수 있게 해줍니다. 타입 제어와 Concepts 활용이 가능합니다.


1. Template Lambda 기본

C++14에서 도입된 제네릭 람다는 매개변수를 auto로 선언해 여러 타입에 재사용할 수 있게 해주었지만, 매개변수 각각이 독립적으로 타입을 추론하기 때문에 “두 매개변수가 반드시 같은 타입이어야 한다”는 관계를 표현할 방법이 없었습니다. C++20의 템플릿 람다는 []<typename T>(T a, T b)처럼 명시적 템플릿 매개변수 목록을 람다 시그니처 앞에 붙일 수 있게 해, 매개변수 간 타입 관계를 직접 제어하고 필요하면 타입을 명시적으로 지정해 호출할 수도 있게 해줍니다.

auto vs Template Lambda

addAuto처럼 auto 매개변수 두 개로 만든 람다는 각 인자가 서로 다른 타입이어도 자유롭게 호출할 수 있어, int + int, int + double, double + double 조합이 모두 컴파일됩니다. 반면 addTemplateT a, T b로 두 매개변수를 같은 템플릿 타입 T에 묶어두었기 때문에, addTemplate(1, 2.5)처럼 서로 다른 타입을 넘기면 템플릿 인자 추론이 실패해 컴파일 에러가 발생합니다. 이 차이가 바로 템플릿 람다를 쓰는 핵심 동기입니다. 매개변수 간에 반드시 같은 타입이어야 하는 제약을 코드로 강제할 수 있다는 것입니다.

#include <iostream>
#include <typeinfo>

int main() {
    // C++14: auto 람다 (Generic Lambda)
    auto addAuto = [](auto a, auto b) {
        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);  // 컴파일 에러: 타입 불일치
}

기본 사용

템플릿 람다의 또 다른 실용적인 장점은 람다 본문 안에서 매개변수의 실제 타입을 T라는 이름으로 직접 참조할 수 있다는 것입니다. 아래 print 람다는 typeid(T).name()으로 전달된 인자의 타입을 출력하는데, auto 매개변수만으로는 타입 이름 자체에 접근할 방법이 없어 이런 코드를 작성하기 어렵습니다. print(42), print(3.14), print("Hello")처럼 서로 다른 타입을 넘길 때마다 T가 각각 int, double, char const*로 치환되며, 이런 방식은 디버깅 로그나 제네릭 직렬화 코드에서 특히 유용합니다.

#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)

템플릿 람다가 명시적 템플릿 매개변수를 지원한다는 것은, 그 자리에 C++20 Concepts를 바로 적용해 허용할 타입의 범위를 제한할 수 있다는 뜻이기도 합니다. typename T 대신 std::integral Tstd::floating_point T처럼 구체적인 concept을 지정하면, 조건에 맞지 않는 타입으로 호출할 때 static_assert를 직접 작성하지 않아도 명확한 컴파일 에러를 얻을 수 있습니다.

Concepts로 타입 제한

addIntsstd::integral T로 제약되어 있어 int, long 같은 정수 타입만 받아들이고, addFloatsstd::floating_point T로 제약되어 double, float 같은 부동소수점 타입만 받아들입니다. addInts(1.5, 2.5)처럼 조건에 맞지 않는 타입으로 호출하면 오버로드 후보에서 아예 제외되어 “제약 조건을 만족하지 않는다”는 명확한 에러가 발생하는데, 이는 T를 아무 타입이나 받게 해두고 함수 본문에서 잘못된 연산을 시도했을 때 나오는 길고 알아보기 힘든 템플릿 에러 메시지보다 훨씬 진단하기 쉽습니다.

#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

표준 라이브러리가 제공하는 concept만으로 부족하다면, concept 키워드로 직접 조건을 정의해 람다에 적용할 수 있습니다. 아래 Numeric concept은 std::integral<T> || std::floating_point<T>라는 논리식으로 “정수이거나 부동소수점인 타입”을 정의하고, multiply 람다는 이를 이용해 숫자 타입에는 곱셈을 허용하되 문자열 같은 비숫자 타입은 걸러냅니다. 이런 식으로 프로젝트 도메인에 맞는 concept을 미리 정의해두면 여러 템플릿 람다와 함수에서 일관되게 재사용할 수 있어 타입 제약 코드의 중복을 줄일 수 있습니다.

#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: 여러 템플릿 매개변수

convert 람다는 FromTo 두 개의 템플릿 매개변수를 받아 타입 변환을 수행합니다. To는 함수 인자만으로는 추론할 방법이 없는 타입이라서, convert.operator()<int, double>(10)처럼 .operator()<...> 문법으로 템플릿 인자를 명시적으로 지정해야 호출할 수 있습니다. 반면 toInt처럼 템플릿 매개변수가 하나뿐이고 함수 인자로부터 추론 가능하다면, toInt(3.14)처럼 평소와 같이 자연스럽게 호출할 수 있습니다.

#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: 컨테이너 처리

템플릿 람다는 컨테이너 타입 자체를 템플릿 매개변수로 받을 수도 있습니다. 아래 printContainerContainer라는 템플릿 매개변수로 벡터든 리스트든 어떤 컨테이너든 받아들이고, typename Container::value_type으로 그 컨테이너가 담고 있는 요소의 타입까지 알아낼 수 있습니다. 일반 함수 템플릿으로도 같은 기능을 만들 수 있지만, 람다로 작성하면 별도의 함수 선언 없이 사용하는 자리에서 바로 정의하고 넘길 수 있어 코드가 더 간결해집니다.

#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: 파라미터 팩

템플릿 람다는 가변 인자 템플릿(parameter pack)과도 결합할 수 있어, 인자 개수를 미리 알 수 없는 상황에도 대응할 수 있습니다. 아래 sum 람다는 typename...TsTs...values로 임의 개수의 인자를 받은 뒤, C++17의 폴드 표현식(fold expression) (values + ...)으로 모든 인자를 하나의 덧셈 체인으로 펼쳐 합산합니다. print 람다처럼 ((std::cout << values << " "), ...) 형태의 폴드 표현식을 쓰면 임의 개수의 인자를 순서대로 출력하는 것도 몇 줄 만에 구현할 수 있습니다.

#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: 컨테이너 변환

이번에는 컨테이너 템플릿 매개변수와 함수 객체 템플릿 매개변수를 함께 받는 transform 람다입니다. Container로 어떤 종류의 컨테이너인지, Func로 각 요소에 적용할 변환 함수를 각각 별도의 템플릿 매개변수로 분리해 받기 때문에, 이 람다 하나로 다양한 컨테이너와 변환 로직의 조합을 재사용할 수 있습니다. 아래 main에서는 정수 벡터의 각 요소를 2배로 만드는 데 사용했지만, 같은 transform 람다를 문자열 벡터에 대문자 변환 함수와 함께 넘기는 식으로도 그대로 재사용할 수 있습니다.

#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, [](auto x) { return x * 2; });
    
    for (int n : doubled) {
        std::cout << n << " ";  // 2 4 6 8 10
    }
    std::cout << std::endl;
}

4. auto vs Template Lambda

1절에서 간단히 짚었던 차이를 이번에는 조금 더 구체적인 예제로 다시 비교해 보겠습니다. 두 방식 모두 겉보기에는 “여러 타입을 받는 람다”라는 같은 목적을 이루지만, 타입 간의 관계를 강제할 수 있는지 여부에서 근본적으로 갈립니다.

비교 예제

func1auto 매개변수 두 개를 쓰기 때문에 func1(1, 2), func1(1, 2.0), func1(1.5, 2)처럼 서로 다른 타입 조합을 모두 허용하며, 내부적으로는 각 조합마다 다른 템플릿 인스턴스가 암묵적으로 생성됩니다. 반면 func2T a, T b로 두 매개변수를 같은 타입 T에 묶어두었기 때문에 func2(1, 2.0)처럼 타입이 다른 인자를 넘기면 컴파일 타임에 바로 에러가 발생합니다. 매개변수 간에 일관된 타입을 강제하고 싶다면 템플릿 람다가, 자유로운 타입 조합을 허용하고 싶다면 auto 람다가 적합합니다.

#include <iostream>

int main() {
    // auto: 각 매개변수 독립적
    auto func1 = [](auto a, auto b) {
        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++14C++20

5. 자주 발생하는 문제

템플릿 람다는 매개변수 간 타입을 엄격하게 강제하는 만큼, 그 강제성 때문에 예상치 못한 컴파일 에러를 마주치기 쉽습니다. 아래 네 가지는 실무에서 자주 겪는 문제와 그 해결 방법입니다.

문제 1: 타입 불일치

add 람다는 두 매개변수가 같은 타입 T여야 하므로 add(1, 2.0)처럼 intdouble을 섞어서 넘기면 템플릿 인자 추론이 실패해 컴파일 에러가 납니다. 이를 해결하는 방법은 두 가지입니다. add2처럼 매개변수마다 별도의 템플릿 타입(T, U)을 두어 서로 다른 타입을 허용하거나, add3처럼 원래대로 같은 타입 T를 강제하되 decltype(a + b)로 반환 타입을 명시해 연산 결과의 타입을 정확히 표현하는 방식입니다.

#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: 명시적 타입 지정

보통은 func(10)처럼 인자를 넘기면 컴파일러가 T를 자동으로 추론해주지만, 인자의 타입과 다른 타입으로 강제 변환해서 계산하고 싶을 때는 추론에만 의존할 수 없습니다. 아래 func.operator()<double>(10)은 인자로는 int 리터럴 10을 넘기면서도 템플릿 인자로 double을 명시해, 10Tdouble로 암묵적 변환된 뒤 연산이 수행되도록 강제합니다. 이렇게 .operator()<T>(...) 문법을 알아두면 타입 추론에 맡기지 않고 정확히 원하는 인스턴스화를 선택할 수 있습니다.

#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 제약 에러

std::integral T로 제약된 process 람다에 double 값을 넘기면, 컴파일러는 T = doublestd::integral concept을 만족하지 못한다는 사실을 즉시 알아채고 오버로드 후보에서 배제합니다. 이때 발생하는 에러 메시지는 아래처럼 “일치하는 operator()가 없다”와 “제약 조건이 만족되지 않는다”를 명확히 알려주는데, 이는 concept 없이 typename T만 썼을 때 함수 본문 깊숙한 곳에서 발생하는 장황하고 이해하기 어려운 템플릿 에러와 비교하면 원인 파악이 훨씬 쉽습니다.

#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: 반환 타입 추론

람다의 반환 타입을 명시하지 않으면 컴파일러가 함수 본문의 return 문으로부터 타입을 추론하는데, if constexpr의 각 분기가 서로 다른 타입을 반환하면 이 추론이 실패합니다. 아래 첫 번째 func는 정수 타입일 때 int를, 그 외에는 double을 반환하도록 작성되어 있어 반환 타입이 하나로 통일되지 않아 컴파일 에러가 발생합니다. func2처럼 트레일링 반환 타입(-> double)을 명시적으로 지정하면, 각 분기의 결과가 모두 그 타입으로 변환되도록 강제되어 문제가 해결됩니다.

#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: 제네릭 알고리즘

sortContainerContainer 템플릿 매개변수로 어떤 컨테이너 타입이든 받아 std::sort를 적용하는 얇은 래퍼입니다. 직접 std::sort(nums.begin(), nums.end())를 호출해도 되지만, 이렇게 래핑해두면 정렬 로직에 로깅이나 커스텀 비교자 적용 같은 부가 기능을 나중에 한 곳에서 추가하기 쉬워지고, 컨테이너 타입을 매번 명시하지 않아도 되는 편의성도 얻습니다.

#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: 팩토리 패턴

makeUnique 람다는 생성할 객체의 타입 T와 그 생성자에 전달할 가변 인자 Args...를 모두 템플릿 매개변수로 받아, std::make_unique<T>를 감싸는 제네릭 팩토리 역할을 합니다. T는 인자만으로 추론할 수 없으므로 makeUnique.operator()<std::vector<int>>(10, 42)처럼 명시적으로 지정해야 하며, 이렇게 하면 std::vector<int>(10, 42)(값 42로 채워진 크기 10짜리 벡터)를 가리키는 unique_ptr을 얻게 됩니다. 이런 팩토리 패턴은 객체 생성 로직을 한곳에 모아두고 싶을 때, 혹은 생성 시점에 로깅이나 캐싱 같은 부가 작업을 끼워 넣고 싶을 때 유용합니다.

#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: 조건부 처리

stringify 람다는 if constexpr을 이용해 컴파일 타임에 타입별로 다른 코드 경로를 선택하는 패턴을 보여줍니다. 숫자 타입(std::is_arithmetic_v<T>)이면 std::to_string으로 변환하고, 이미 std::string이면 그대로 반환하며, 그 외의 타입은 "unknown"으로 처리합니다. if constexpr의 각 분기는 해당 조건이 거짓인 타입에 대해서는 아예 컴파일되지 않기 때문에, 예를 들어 문자열이 아닌 타입에 대해 문자열 전용 연산을 시도하는 코드가 있어도 타입이 맞지 않는 분기는 인스턴스화되지 않아 컴파일 에러 없이 넘어갑니다.

#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. 실전 예제: 제네릭 유틸리티

마지막으로 지금까지 배운 개념들을 하나로 묶어, 함수형 프로그래밍 스타일의 컨테이너 유틸리티 세트를 만들어 보겠습니다. Utils 클래스는 filter(조건에 맞는 요소만 남기기), map(각 요소를 변환하기), range(정수 범위 생성)라는 세 개의 정적 템플릿 람다를 제공합니다. filtermap은 각각 ContainerPredicate/Func라는 별도의 템플릿 매개변수로 컨테이너 타입과 처리 함수를 분리해 다양한 조합에 재사용할 수 있게 했고, rangestd::integral T로 제약을 걸어 정수 계열 타입에만 사용하도록 제한했습니다. main에서 range → filter → map으로 이어지는 파이프라인은 1부터 9까지의 숫자 중 짝수만 골라 제곱한 결과를 얻는데, 이렇게 작은 제네릭 함수들을 조합해 복잡한 데이터 처리 흐름을 표현하는 방식은 여러 언어의 함수형 스트림 API와 비슷한 스타일입니다.

#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, [](auto n) { return n % 2 == 0; });
    
    // 제곱으로 변환
    auto squared = Utils::map(evens, [](auto n) { return n * n; });
    
    std::cout << "결과: ";
    for (int n : squared) {
        std::cout << n << " ";  // 4 16 36 64
    }
    std::cout << std::endl;
}

정리

핵심 요약

  1. Template Lambda: C++20, 명시적 템플릿 매개변수
  2. Concepts: 타입 제약 (std::integral, std::floating_point)
  3. 타입 제어: 매개변수 간 타입 관계 명시
  4. 파라미터 팩: 가변 인자 템플릿
  5. 명시적 호출: .operator()<T>()로 타입 지정

auto vs Template Lambda

사용 사례auto 람다Template Lambda
각 매개변수 타입 다름
같은 타입 강제
Concepts 제약
명시적 타입 지정

실전 팁

  1. 언제 사용할까

    • 매개변수 간 타입 관계가 중요할 때
    • Concepts로 타입 제약이 필요할 때
    • 명시적 타입 지정이 필요할 때
  2. 성능

    • 일반 템플릿 함수와 동일한 성능
    • 인라인 최적화 가능
    • 런타임 오버헤드 없음
  3. 디버깅

    • 컴파일 에러 메시지가 명확함
    • Concepts로 에러 메시지 개선
    • typeid로 타입 확인

다음 단계


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Everything about C++ Template Lambda : from basic concepts to practical applications.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


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

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


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

C++, lambda, template, C++20, generic 등으로 검색하시면 이 글이 도움이 됩니다.