C & C++/C++

[C++] 템플릿, 타입

거북이 코딩 2024. 8. 16. 22:56

Ch 06. 템플릿

함수 템플릿

void swap(int& x, int& y)
{
    int temp = x;
    x = y;
    y = temp;
}

위 함수는 간단한 swap 함수이다. 하지만 int로 정의되어 있어 다른 자료형의 데이터는 받을 수 없다. 다른 자료형을 받으려면 오버로딩 해야 하는데 매우 번거롭다. 이를 해결하기 위한 툴이 함수 템플릿이다.

template<typename T>
void swap(T& x, T& y)
{
    int temp = x;
    x = y;
    y = temp;
}

int main()
{
	int x = 10, y = 20;
	swap<int>(x, y); // 타입 명시
	swap(x, y); // 타입 추론

위 코드는 함수가 아니라 함수 템플릿이다. 함수를 호출하는 순간에 템플릿을 기반으로 함수가 만들어진다. 템플릿은 타입에 의존하지 않고 하나의 값이 여러 타입을 가질 수 있는 일반화 프로그래밍을 가능하게 한다.

위 코드가 템플릿의 파라메터로 타입을 받았다면, 아래 코드는 템플릿의 파라미터로 값을 받고 있다.

// 템플릿의 파라메터로 int 값을 받는다
template<int N>
int func(int (&nums)[N])
{
    return N;
}

// 타입 파라메터와 값 파라메터를 같이 받는 템플릿
template<int N, typename T>
int getSize(T(&nums)[N])
{
    return N;
}

int main()
{
	{
	    int nums[] = { 1, 2, 3 };
	    cout << func<3>(nums) << endl; // 값 명시
	    cout << func(nums) << endl;  
	    // 3을 추론한다. 즉 배열의 사이즈를 추론을 통해서 구할 수 있다
	    // 컴파일 타임에 추론하기 때문에 정적 배열만 가능하다
	}
	{
	    int nums[] = { 1, 2, 3 };
	    cout << getSize(nums) << endl;
	
	    char chars[] = {'a'};
	    cout << getSize(chars);
	}
}

클래스 템플릿

템플릿을 활용하면 타입에 의존하지 않는 Stack을 구현할 수 있다. 다른 클래스 템플릿으로 클래스 템플릿을 넘겨줄 수 도 있다. Stack → vector

#include <iostream>
#include <vector>

using std::cout;
using std::endl;

template<typename T>
class Stack
{
private:
    std::vector<T> _items;

public:
    Stack();
    void push(T item);
    void pop();
    T& top();
};

template<typename T>
Stack<T>::Stack()
    _items{}
{

}

template<typename T>
void Stack<T>::push(T item)
{
		_items.push_back(item);
}

template<typename T>
void Stack<T>::pop()
{
    if (_size == 0)
    {
        throw std::out_of_range("underflow");
    }
    _items.pop_back();
}

template<typename T>
T& Stack<T>::top()
{
    if (_size == 0)
    {
        throw std::out_of_range("underflow");
    }
    return _items.back();
}

템플릿 특수화

특정 클래스나 타입에 대해서만 템플릿 함수를 특수화 할 수 있다.

template <typename T>
void swap(T& x, T& y)
{
    std::cout << "swap" << std::endl;
    T temp = x;
    x = y;
    y = temp;
}

class Foo
{

};

template <>
void swap<Foo>(Foo& x, Foo& y) // 명시적 특수화, swap 뒤의 <Foo> 를 지우면 암시적 특수화
{
    std::cout << "swap<Foo>" << std::endl;
}

// Foo class가 swap을 했을 때 위에 있는 특수화된 템플릿으로 들어가게 된다.

포인터에 대한 특수화, 포인터가 가르키는 대상끼리의 값을 변경

template <typename T>
void swap(T* x, T* y) // 포인터에 대한 특수화, 오버로딩으로 봐야함 *와 &는 다르기 때문
{
    std::cout << "swap pointer" << std::endl;
    T temp = *x;
    *x = *y;
    *y = temp;
}

클래스 템플릿 특수화

// 템플릿 특수화
template<typename T, typename S>
class Test
{
public:
    T num0;
    S num1;
};

// 완전 특수화
template<>
class Test<int, float> // 타입이 정해져 있음
{

};

// 부분 특수화, 하나의 타입만 특수화 할 경우
template<typename T>
class Test<T, int>
{

};

int main()
{
    Test<int, int> t0;
    Test<int, float> t1;
    Test<float, int> t2;

    // bool을 그대로 사용하면 효율이 떨어지기 때문에 vector에서 특수화를 사용한다
    std::vector<bool> vb;
}

템플릿 구체화

템플릿은 구체화 하기 전까지 함수도, 클래스도 아니다. 형판에 불과하다.

template
void swap<int>(int&, int&); // 명시적 구체화

swap(x, y); // 암시적 구체화, <int> 가 생략됨

템플릿의 헤더파일과 소스코드를 분리할 경우

template<typename T>
void swap(T& x, T& y);
#include "swap.h"

template<typename T>
void swap(T& x, T& y)
{
    T temp = x;
    x = y;
    y = temp;
}
#include <iostream>
#include "swap.h"

using std::cout;
using std::endl;

int main()
{
    int x = 10, y = 20;
    swap(x, y); // 암시적 구체화
}

위 프로젝트는 제대로 링킹이 되지 않는다. 왜냐하면 swap.cpp파일의 템플릿이 구체화되지 않았기 때문에 기계어로 바뀌지 않았고, 헤더파일에서 구현을 링크하지 못하기 때문이다. 그래서 이렇게 분리할 경우 cpp파일에 사용할 타입을 모두 구체화해주어야 한다.

#include "swap.h"

template<typename T>
void swap(T& x, T& y)
{
    T temp = x;
    x = y;
    y = temp;
}

template
void swap<int>(int&, int&); // 명시적 구체화 되어 기계어로 바뀜

하지만 사용할 모든타입의 명시적 구체화는 너무 비용이 크기 때문에 선언과 정의를 분리하지 않고 다음과 같이 헤더파일에 한 번에 선언과 정리를 둔다.

template<typename T>
void swap(T& x, T& y);

template<typename T>
void swap(T& x, T& y)
{
    T temp = x;
    x = y;
    y = temp;
}

이렇게 되면 헤더파일만 include해도 정의를 찾을 수 있기 때문에 링크가 된다.

가변 인자

가변 인자는 인자의 개수가 정해지지 않은것을 말한다.

printf("%d %d %d", a, b, c); // 인자의 수가 정해지지 않음

C style 가변인자

#include <iostream>
#include <cstdarg>

using std::cout;
using std::endl;

// C 스타일 가변인자
int sum0(int count, ...) // 가변인자는 ...으로 표시한다.
{
    int result = 0;
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i)
    {
        result += va_arg(args, int); // 스택으로부터 int의 크기만큼씩 읽는다.
    }
    va_end(args);
    return result;
}

int main()
{
    cout << sum0(4, 10, 20, 30, 40) << endl; // 100
}

템플릿 가변인자

template<typename T>
T sum1(T value) // unpack에 마지막 인자가 남았을 때
{
    return value;
}

// C++ 스타일 가변인자
template<typename T, typename... Args>
T sum1(T value, Args... args) // args에 나머지 인자가 모두 들어가게 된다.
{
    return value + sum1(args...); // unpack
}

int main()
{
    cout << sum1(10, 20, 30, 40) << endl; // 100
}

템플릿 메타 프로그래밍

템플릿을 사용하여 컴파일 타임에 코드를 생성하고 계산을 수행하는 프로그래밍 기법이다. 런타임에 불필요한 계산을 피해서 실행속도를 향상시킨다.

Template Meta Programming (TMP)를 사용하지 않은 팩토리얼 함수

#include <iostream>

using std::cout;
using std::endl;

int factorial(int value)
{
    if (value == 1)
        return value;
    return value * factorial(value - 1);
}

TMP를 사용한 팩토리얼 함수

template<int N>
struct Factorial
{
    static const int value = N * Factorial<N - 1>::value; // 재귀
};

// 특수화를 통해 재귀 탈출
template<>
struct Factorial<1>
{
    static const int value = 1;
};

TMP를 사용하면 컴파일 타임에 결정되기 때문에 변수를 넣어 줄 수 없다.

TMP를 사용하지 않은 피보나치수열

int fib(int value)
{
    if (value <= 1)
        return value;
    return fib(value - 1) + fib(value - 2);
}

TMP 사용한 피보나치수열

template<long long N>
struct Fib
{
    static const long long value = Fib<N - 1>::value + Fib<N - 2>::value;
};

template<>
struct Fib<1>
{
    static const long long value = 1;
};

template<>
struct Fib<0>
{
    static const long long value = 0;
};

int main()
{
    cout << factorial(5) << endl;
    cout << Factorial<5>::value << endl; 
    // 120이 컴파일 타임에 계산이 된다, 변수를 입력값으로 넣어줄 수 없다

    cout << fib(6) << endl;
    cout << Fib<6>::value << endl;

    //cout << fib(50) << endl; // 느리다
    cout << Fib<50>::value << endl;
}

Ch 07. 타입

type traits

정적타임에 타입을 핸들링하는 방법

#include <type_traits>
#include <iostream>

using namespace std;

int main()
{
	cout << std::boolalpha; // bool을 true, false로 표현
	cout<<std::is_pointer<int>::value<<endl; // pointer라면 true를 리턴
	return 0;
}

is_pointer 구현

template<typename T>
struct is_pointer
{
    static const bool value = false;
};

template<typename T>
struct is_pointer<T*>
{
    static const bool value = true;
};

int main()
{
	cout << is_pointer<int*>::value << endl;
  cout << is_pointer<int>::value << endl; // 템플릿 특수화로 인해 true
}

is_pointer 활용 예시

template<typename T>
void foo(T t)
{
    cout << is_pointer<T>::value << endl;
} // 어떤 타입인지 알 수 있다.

int main()
{
    int num = 0;
    int* pNum = &num;

    foo(num);
    foo(pNum);
}

// std::is_pointer<int>::value = std::is_pointer_v<int>

add_pointer : 타입에 포인터를 추가한다.

int num = 0;
std::add_pointer<int>::type pNum = &num; // int* pNum = &num
foo(pNum); // true 

// std::add_pointer<int>::type = std::add_pointer_t<int>

add_pointer 구현

template<typename T>
struct add_pointer
{
    using type = T*;
};

template<typename T>
struct add_pointer<T&>
{
    using type = T*;
};

remove_pointer : 포인터를 제거한다.

std::remove_pointer<int*>::type n = *pNum; // int n
foo(n); // false

remove_pointer 구현

template<typename T>
struct remove_pointer
{
    using type = T;
};

template<typename T>
struct remove_pointer<T*>
{
    using type = T;
};

unscoped, scoped enum

enum Unscoped
{
    A, B
};

enum class Scoped : long long
{
    A, B = 10000000000
};

int main()
{
		cout<<static_Cast<int>(Scoped::B); // 오류 발생
		return 0;
}

타입을 알아내기 위해 underlying_type을 사용한다.

cout << static_cast<std::underlying_type<Scoped>::type>(Scoped::B) << endl;

ostream 오버로딩으로 간단하게 출력해 보기

template<typename T>
std::ostream& operator<<(std::ostream& os, const T& value)
{
    // underlying_type  을 통해 타입을 알아냄
    using t = std::underlying_type<T>::type;
    cout << static_cast<t>(value);
    return os;
}

scoped enum만 함수의 파라미터로 받기 위해 is_scoped를 구현

template<typename T>
struct is_scoped_enum
{
    // enum 인데 int로 변환이 되지 않으면 Scoped
    static const bool value = std::is_enum<T>::value && !std::is_convertible<T, int>::value;
    // enum이면서 int로 변환이 되지 않는 enum 이면 true
};

enable_if

std::enable_if<bool, T>::type
// bool 이 true 일 때만 T가 정의 된다. 정의 되지 않으면 다른 오버로딩 함수를 찾는다. 
// 이런 기법을 SFINAE(Substitution failure is not an erro) 라고 한다.

enable_if와 is_scoped를 이용하여 scoped enum만 받는 함수 오버로딩

// SFINAE(Substitution failure is not an erro) 이용
template<typename T, typename std::enable_if<is_scoped_enum<T>::value, int>::type = 0>
std::ostream& operator<<(std::ostream& os, const T& value)
{
    // underlying_type  을 통해 타입을 알아냄
    using t = typename std::underlying_type<T>::type;
    cout << static_cast<t>(value);
    return os;
}

RTTI

런 타임에 어떤 타입을 결정해 주는 메커니즘 Run Time Type Identification

#include <typeinfo>
typeid(T).name(); // 타입 이름 반환

RTTI는 기본적으로 켜져 있지만 설정에서 끄게 된다면 dynamic_cast나 typeid() 같은 RTTI를 사용하는 함수에서 오류가 날 수 있다. (런타임 중에 타입이 결정되는 경우)

RTTI의 타입정보는 가상함수 테이블 (V table)에 있기 때문에 가상함수가 없어서 V table이 만들어지지 않는다면 제대로 작동하지 않는다.