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

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++14C++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;
}

정리

핵심 요약

  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로 타입 확인

다음 단계

  • C++ Generic Lambda
  • C++ constexpr Lambda
  • C++ Concepts

관련 글

  • C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기