C, C++/C++ 여행

[A Tour of C++] 7장 - 템플릿

Basaeng 2025. 6. 10. 13:57

개요

template은 타입 또는 값 집합으로 매개변수화한 클래스나 함수이다.

 

클래스나 함수 선언 앞에 template<typename T>를 붙이면, T는 선언의 타입 매개변수가 된다.

typename대신 class를 사용해 template<class T>와 같이 표현할 수 있으며 차이는 없다.

 

c++20 이후에는 concept를 사용해, 템플릿에 제약조건을 걸 수 있다.

 

템플릿을 사용할 때는 vector<int>와 같이 사용할 타입을 <> 안에 넣고 사용한다.

 

추가적으로 템플릿은 구체화 혹은 특수화가 가능하다.


매개변수화 타입

C++ 템플릿은 특정 타입에 종속되지 않고, 다양한 타입에 대해 일반화된 기능을 제공할 수 있도록 한다. 템플릿은 컴파일 타임에 인스턴스화되어, 실제로 사용하는 타입마다 해당 타입에 맞는 코드가 생성된다.

template<typename T>
T test(T a)
{
    return a;
}

int _tmain(int argc, TCHAR* argv[])
{
    int b = test(1);
    char c = test('c');
    return 0;
}

이렇게 템플릿을 사용했을 때 

int _tmain(int argc, TCHAR* argv[])
{
00591002 EC                   in          al,dx  
00591003 83 EC 08             sub         esp,8  
    int b = test(1);
00591006 6A 01                push        1  
00591008 E8 23 00 00 00       call        test<int> (0591030h)  
0059100D 83 C4 04             add         esp,4  
00591010 89 45 F8             mov         dword ptr [b],eax  
    char c = test('c');
00591013 6A 63                push        63h  
00591015 E8 26 00 00 00       call        test<char> (0591040h)  
0059101A 83 C4 04             add         esp,4  
0059101D 88 45 FF             mov         byte ptr [c],al  
    return 0;
00591020 33 C0                xor         eax,eax  
}

보는것 처럼, int와 char에 대해 각각의 함수 호출 주소가 다른 것을 볼 수 있다.

 

제한된 템플릿 인수

대부분의 템플릿은 정말 모든 타입을 허용한다기 보다는, 일정 기준에 부합하는 템플릿 인수만 허용한다.

이를 위해 template<typename T>의 typename 대신에 T타입이 조건에 만족하는지를 검사하는 Predicate인 concept를 사용한다.

 

값 템플릿 인수

템플릿은 타입 인수 말고도 값 인수도 받을 수 있다.

template<typename T, int N> 처럼 선언하여 Heap 메모리를 사용하지 않고도 동적인 크기의 사이즈를 지정할 수 있다.

 

템플릿 인수 추론

템플릿 인수를 명시하지 않는다면 생성자가 초기자로부터 템플릿 인수를 추론할 수 있다.

하지만 이 경우에 템플릿 인수 추론 규칙에 따라 프로그래머가 의도하지 않은 방향으로 추론될 수 있다.

concepts는 강력한 해결방법이지만, 이전에 작성된 코드의 모호함을 해결하기 위해서 추론 안내를 추가할 수 있다.

template<typname Iter>
Vector(Iter, Iter) -> Vector<typename Iter::value+type>;

더 자세한 추론 규칙은 이펙티브 모던 C++등의 책을 참고하자.


매개변수화 연산

템플릿은 단순히 타입을 매개변수화 하는 것을 넘어 알고리즘을 매개변수화 하는 등 여러 방면에서 사용된다.

 

핵심적인 3가지의 매개변수화 연산 방법에 대해 알아보자.

1. 함수 템플릿

2. 함수 객체

3. 람다식

 

함수 템플릿

함수에 템플릿을 사용해, 특정 타입에 종속되지 않고 다양한 타입에 대해 동일한 로직을 수행할 수 있도록 일반화된 함수 형태이다.

함수 템플릿은 멤버 함수일 수는 있지만 virtual일 수 없다. 컴파일러가 사용된 모든 구체화를 알지 못해, 타입마다 대응하는 virtual table을 생성할 수 없다.

 

함수 객체(functor)

말 그대로 함수처럼 호출할 수 있는 객체를 정의할 때 사용한다.

함수처럼 호출하기위해 operator()를 오버로딩해서 사용한다.

 

템플릿을 사용하면 더욱 일반화된 함수 객체를 사용할 수 있다.

template<typename T>
class MoreThan
{
public:
    MoreThan(const T& v) : val{ v }{};
    bool operator()(const T& x)
    {
        return val < x;
    };
private:
    const T val;
};

int _tmain(int argc, TCHAR* argv[])
{
    MoreThan<int> moreThan(20);

    cout << moreThan(10) << endl;
    cout << moreThan(30) << endl;
    return 0;
}

 

람다식

https://learn.microsoft.com/ko-kr/cpp/cpp/lambda-expressions-in-cpp?view=msvc-170

 

C++ 람다 식

자세한 정보: C++의 람다 식

learn.microsoft.com

 

임시적인 함수 객체를 만드는 문법을 람다식이라고 한다.

C++에서 람다식은 [](){} 형태로 사용된다.

함수객체 형태이기 때문에 내부적으로 operator()가 오버로딩되고, 따라서 [](){}();와 같이 사용되면 정의와 동시에 사용할 수 있다.

 

캡처절

[]안에는 캡처절이 들어간다. 

캡처절에는 해당 함수 스코프 내의 변수를 사용할 수 있다.

[x]와 같이 사용하면 x를 값 복사 형태로 람다에서 사용하겠다는 뜻이다. 

[&x]를 하면 참조 형태로 사용한다.

추가적으로 [&]는 스코프 내의 모든 변수를 참조형태로, [=]는 값 형태로 사용한다. (실제로는 사용된 변수만 캡쳐됨)

 

매개 변수 목록

()안에는 매개 변수 목록이 들어간다.

함수의 인자와 같다.

매개 변수의 인자를 auto로 한다면 template 형태로 사용할 수 있다.

 

람다 본문

{}안에는 람다 함수 본문이 들어간다.

캡쳐된 데이터, 인자 데이터, 클래스 내라면 클래스의 데이터 등을 참조해서 구현할 수 있다.

 

예시

    auto lambda = [val](int x) {return val < x; };
00A8109D 8D 55 F4             lea         edx,[val]  
00A810A0 52                   push        edx  
00A810A1 8D 4D F0             lea         ecx,[lambda]  
00A810A4 E8 17 00 00 00       call        `wmain'::`2'::<lambda_1>::<lambda_1> (0A810C0h)

람다식을 정의한다면, 마치 함수 객체를 만드는 것 처럼 세팅해준다. 

코드를 보면 캡쳐한 val을 edx에 넣고, 이를 push해서 call을 해주는 것을 볼 수 있다.

 

    auto moreThanLambda = [val](int x) {return val < x; };
006810D0 55                   push        ebp  
006810D1 8B EC                mov         ebp,esp  
006810D3 51                   push        ecx  
006810D4 89 4D FC             mov         dword ptr [this],ecx  
006810D7 8B 45 FC             mov         eax,dword ptr [this]  
006810DA 8B 4D 08             mov         ecx,dword ptr [v]  
006810DD 8B 11                mov         edx,dword ptr [ecx]  
006810DF 89 10                mov         dword ptr [eax],edx  
006810E1 8B 45 FC             mov         eax,dword ptr [this]  
006810E4 8B E5                mov         esp,ebp  
006810E6 5D                   pop         ebp  
006810E7 C2 04 00             ret         4
    auto moreThanLambda = [&val](int x) {return val < x; };
00D310D0 55                   push        ebp  
00D310D1 8B EC                mov         ebp,esp  
00D310D3 51                   push        ecx  
00D310D4 89 4D FC             mov         dword ptr [this],ecx  
00D310D7 8B 45 FC             mov         eax,dword ptr [this]  
00D310DA 8B 4D 08             mov         ecx,dword ptr [__val]  
00D310DD 89 08                mov         dword ptr [eax],ecx  
00D310DF 8B 45 FC             mov         eax,dword ptr [this]  
00D310E2 8B E5                mov         esp,ebp  
00D310E4 5D                   pop         ebp  
00D310E5 C2 04 00             ret         4

 

실제 람다 생성자 함수 안에서는, 값 복사인 경우에는 val의 값을 복사하고, 참조 복사인 경우, val의 주소를 그대로 넣어주는 것을 확인할 수 있다.

 

  • 값 캡쳐 시: mov edx, [ecx]; mov [eax], edx → 역참조해서 값 복사
  • 참조 캡쳐 시: mov ecx, [addr]; mov [eax], ecx → 주소 그대로 복사

 

 

함수 인수로서의 람다: 인자를 람다로 받는 템플릿 함수를 만들 수 있다.

제네릭 람다: 위에서 언급했듯이 람다의 인자 ()안의 값의 타입을 auto로 한다면 어떤 타입의 값이든 초기자로 허용한다.

필요에 따라 concepts로 인자를 제한할 수 있다.

람다로 초기화: 람다를 활용해 명령문(statement)을 표현식(expression)으로 바꿀 수 있다.

int code = ...;
std::string result = [code]() {
    switch (code) {
        case 1: return "One";
        case 2: return "Two";
        default: return "Other";
    }
}();

 

 

finally: 스코프가 종료될 때 필요한 동작인 finally를 람다로 만들 수 있다. 람다를 이용해 스코프 종료 시 실행될 동작을 담은 객체를 만들고, 해당 객체의 소멸자에 정리 코드를 작성하면, 해당 스코프가 종료될 때 자동으로 소멸자가 실행된다. 

c++23부터는 scope_exit이 편입되어 finally기능을 사용할 수 있다.


템플릿 메커니즘

좋은 템플릿을 위한 언어 기능 지원이 필요하다.

1. 타입에 따른 값: 변수 템플릿

2. 타입과 템플릿의 alias: alias template

3. 컴파일 타임 선택 메커니즘: if constexpr

4. 타입과 표현식의 속성을 묻는 컴파일 타임 메커니즘

 

변수 템플릿

타입에 따라 상수값을 다르게 지정할 수 있게 해준다.

template<typename T>
constexpr int type_size = sizeof(T);

int a = type_size<int>;     // 4 
int b = type_size<double>;  // 8

 

 

에일리어스

템플릿에 별칭을 붙여줄 수 있다. 복잡한 타입일 수록 간편하다.

template<typename T>
using AliasName = SomeTemplate<T>;

 

컴파일 타임 if

template을 사용하면 코드가 컴파일 타임에 사용하는 타입마다 각각 생성된다.

따라서 템플릿의 타입에 따라 분기하는 if문이 있다면 컴파일 타임에 처리하는 것이 성능 최적화에 좋을 것이다.

따라서 constexpr을 사용해 if문을 컴파일 타임에 평가할 수 있도록 한다.

template<typename T>
void print_type_info(const T& x) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "정수형입니다\n";
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "실수형입니다\n";
    } else {
        std::cout << "기타 타입입니다\n";
    }
}

concepts와 함께 사용한다면 더욱 유용하다.