template<int m, int n>
struct MyPow
{
static const long long result = m * MyPow<m, n - 1>::result;
};
template<int m>
struct MyPow<m, 0>
{
static const long long result = 1;
};
* factorial
template<int n>
struct MyFactorial
{
static const long long result = n * MyFactorial<n - 1>::result;
};
template <>
struct MyFactorial<1>
{
static const long long result = 1;
};
* enable_if
template <bool condition, typename T = void>
struct MyEnableIf {}; // condition이 false일때는 정의되지 않는다.
template <typename T> // condition이 true일때만 타입을 정의한다.
struct MyEnableIf<true, T> { using type = T; };
template <typename T>
typename MyEnableIf<std::is_integral<std::remove_reference_t<T>>::value, bool>::type
Multiply(T&& a, T&& b) { return a * b; }
* is_prime
// 나눠 떨어지면 재귀를 빠져나가도록 설계한다.
template <int n, int d>
struct Divisible
{
static const bool result = (0 == n % d) ? true : Divisible< n, d - 1 >::result;
};
// 2에서 브래이크를 걸어준다.
template <int n>
struct Divisible <n, 2>
{
static const bool result = (0 == n % 2);
};
template <int n>
struct MyIsPrime
{
static const bool result = (false == Divisible< n, n / 2 >::result);
};
template <>
struct MyIsPrime<2> // 2는 소수, n / 2 부터 나눠서 확인하기 때문에 예외처리
{
static const bool result = true;
};
template <>
struct MyIsPrime<3> // 3은 소수, n / 2 부터 나눠서 확인하기 때문에 예외처리
{
static const bool result = true;
};
템플릿 메타 프로그래밍(template meta programming, TMP)은 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 것을 말한다. 템플릿 기반 프로그램을 작성함으로써 컴파일러에게 프로그램 코드를 생성하도록 하여 컴파일 시점에 많은 것들을 결정하도록 만든다.
일반적인 프로그래밍 방식에서 실행 시점에서 대부분 수행한다면 템플릿 메타 프로그래밍 방식에서는 컴파일 시점에 많은 것들을 수행하여 실행 시점에서의 연산을 줄인다.
템플릿 메타 프로그램은 변수의 값이 한번 정해지면 변경할 수 없기 때문에 함수형 프로그래밍의 한 형태로 볼 수 있다. 많은 템플릿 구현에서 흐름 제어가 재귀를 통해 수행된다. 유사한 코드 중복을 피하는 일반화 프로그래밍과도 연관있다.
* 장점
템플릿 메타 프로그래밍의 장점, 템플릿의 장점은 다음과 같이 정리할 수 있다.
- 컴파일 시점에 연산을 하기 때문에 실행 시점에서 연산을 하거나 함수를 별도로 호출하는 시간을 아낄 수 있다. 또한 컴파일 시간에 확인하기 때문에 문제점을 더 빠르게 파악할 수 있다.
- 상수전파(constant propagation)이 더 잘 일어날 수 있다.
여기서 상수전파란 컴파일러의 실행시간 최적화 기법으로 컴파일 시 상수를 포함하는 연산이 계산될 수 있으면 계산하두어 코드를 줄이는 방식이다. 다음과 같은 코드가 있다고 가정할 때 y의 값은 컴파일 시 6으로 계산된다.
x = 3;
y = 2 * x;
- 정적 다형성을 구현할 수 있다.
동적 다형성(가상 함수)은 알맞은 가상함수를 호출하기 위해 vtable을 확인해서 적절한 함수를 호출하게 되며 이는 오버헤드로 이어진다. 정적 다형성을 구현하면 더 성능이 좋다. 일반적인 경우에는 부모 클래스에서 자식 클래스의 정보를 모르지만 템플릿을 활용하면 가능하다. 다음과 같이 구현된다.
template <typename Super>
class Base
{
public:
void doSomething(void)
{
// ...
static_cast<Super*>(this)->doSomethingImpl();
// ...
}
};
class Derived : public Base<Derived>
{
public:
void doSomethingImpl(void)
{
// ...
}
};
- 상수만 사용될 수 있는 문맥에 템플릿 정보를 활용할 수 있다. 예를 들어 배열의 원소 개수를 지정할 때는 무조건 상수의 값을 받을 수 있는데, 이에 템플릿을 사용할 수 있다. 이를 활용하면 힙에 할당하지 않고 스택에서 처리할 수 있다.
일반 가변인자 함수와 비교했을 때 가변인자 템플릿은 타입 안정성을 갖출 수 있다. 일반적으로 우리가 사용하는 printf의 경우 %d와 같이 어떤 형식으로 출력해야할 지 정해줘야 한다. 하지만 가변인자 템플릿은 해당 인자의 자료형을 알 수 있기 때문에 %의 정보만 있으면 된다.
가변인자 자체는 함수나 클래스의 구현에 쉽게 사용할 수 없다. 그래서 템플릿을 재귀호출을 통해 단계적으로 분할하면서 활용하는 경우가 있다. 이 때문에 가변인자 템플릿은 재귀로 구현되어 있는 경우를 볼 수 있다. 이를 C++17부터는 fold expression이라는 형식으로 매우 간단하게 표현할 수 있도록 지원하고 있다.
간단히 설명을 하면 print에 여러 인수를 넘겨주게 되면 컴파일러는 해당 인수를 받을 수 있는 함수를 찾아본다. 결국 가변인자 템플릿의 print함수가 선택되고 다시 비슷한 방식으로 재귀적으로 가변인자 템플릿의 함수가 호출된다. 최종적으로 인자가 없는 버전인 print()가 호출되면서 재귀를 탈출하게 된다.
C++에서는 이와 비슷하게 원하는 타입에 따라서 그 타입에 맞게 코드를 생성시켜주는 도구를 의미한다.
데이터 타입을 매개변수로 전달하면 서로 다른 데이터 유형에 대해 동일한 코드를 작성할 필요가 없어진다.
* 사용 이유
함수나 클래스를 처음부터 다시 작성하지 않고 다른 데이터 유형에 따라 생성되도록 해주기 때문에 프로그래밍이 편리하다. 예를 들어 특정 타입의 데이터를 정렬하는 sort라는 함수가 있다고 가정했을 때 각 타입에 대해 여러 함수를 오버로딩하는 것보다 매개변수를 템플릿 타입으로 결정하게 되면 훨씬 관리하기 편하고 오류를 줄일 수 있다.
방금 제시한 예시에서와 같이 템플릿은 주로 일반화 프로그래밍(generic programming)에 사용된다. 간단히 일반화된 클래스나 함수를 작성하기 위한 방법으로 사용된다고 할 수 있다.
일반화 프로그래밍에 대한 정의는 다음과 같이 할 수 있다.
일반화 유형이 다양한 데이터 타입에 대해 작동하는 알고리즘의 매개 변수로 사용되는 프로그래밍 접근 방식이다.
데이터 형식에 의존하지 않고 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식이다.
하지만 템플릿의 단점도 존재하는데 단점을 고려해서 상황에 적절한 판단 하에 템플릿을 사용해야 한다. 템플릿의 단점은 다음과 같다.
많은 컴파일러는 템플릿 중첩을 지원하지 않는다.
템플릿을 사용하면 모든 코드가 추상화되지 않고 노출된다.
일부 컴파일러는 템플릿에 대한 지원을 많이 하지 않는다.
템플릿 코드에서 오류가 감지되면 판단하기 어려운 오류 메시지가 표시된다.
템플릿으로 개발하는 것이 어려울 수 있다.
* 동작 방식
- 기본 동작 방식
템플릿은 컴파일 타임에 확장(결정)된다는 점에서 템플릿과 공통점을 지닌다. 하지만 매크로와는 다르게 컴파일러가 템플릿을 확장(결정)하기 컴파일러가 타입에 대한 체크를 한다는 점이다.
처음 소스 코드에는 함수/클래스 템플릿만 포함하고 있지만 컴파일된 코드에서는 여러 가지 같은 함수/클래스들로 확장된다.
- 종류
템플릿은 다음과 같이 크게 두 타입으로 나타난다.
함수 템플릿
클래스 템플릿
- 함수 템플릿
함수 템플릿은 단일 함수에 대한 템플릿을 의미하며 이는 여러 데이터 타입과 함께 동작한다. 예를 들어 sort, max, min과 같은 함수 템플릿이 여기에 속한다.
template <typename T> T FunctionName(T a, T b)
{
// ...
}
보통 다음과 같이 정의된다. T를 함수의 반환형이나 매개변수, 함수바디 등에서 활용할 수 있다. type에는 함수에서 사용 될 데이터 타입이 들어간다.
- 클래스 템플릿
클래스 템플릿도 함수 템플릿과 비슷하게 동작한다. 클래스 템플릿은 클래스가 데이터 타입에 관계 없는 것들을 정의할 때 유용하다.
예를 들어 컨테이너 클래스의 경우 내부 동작방식은 동일하지만 안에 들어가는 개체의 타입만 다르기 때문에 클래스 템플릿을 활용하면 매우 효율적이다.
template <typename T>
class Array
{
public:
Array(T arr[], int s)
{
ptr = new T[s];
size = s;
for (int i = 0; i < size; i++)
{
ptr[i] = arr[i];
}
}
void print()
{
for (int i = 0; i < size; i++)
{
cout << " " << *(ptr + i);
}
cout << endl;
}
private:
T* ptr;
int size;
};
- 템플릿 특수화(Template Specialization)
템플릿 특수화는 특수한 경우에 대해서 코드를 다르게 작성하여 다른 동작을 수행할 수 있게 만들어주는 기능이다. 특정 유형에 대해서 다른 동작 방식을 정의함으로써 템플릿을 통해 수행될 수 있게 지원할 수 있다. 특수화 버전은 해당 타입의 함수를 호출했을 때 일반적인 템플릿 정의보다 우선 시되어 먼저 체크하게 된다.
//A generic sort function
template<typename T>
{
//code to implement quick sort
}
//Template specilization:A function
//specialized for char data type
template<>
void sort<char>(char arr[],int size)
{
//code to impletement counting sort
}
비교 함수로 커스텀화할 수 있는 알고리즘 : std::sort, std::nth_element, std::lower_bound
std::unique_ptr, std::shared_ptr를 위한 삭제자
콜백 함수
인터페이스 적응 함수
일회성 호출을 위한 문맥 국한적 함수
* 동작 방식
- 용어 정리
* 람다 표현식(lambda expression)
: 하나의 표현식으로 소스 코드의 일부이다.
* 클로저(closure)
: 람다에 의해 만들어진 실행시점 객체이다. 캡처 모드(capture mode)에 따라 클로저가 갈무리된 자료의 복사본을 가질 수도 있고 그 자료에 대한 참조를 가질 수도 있다.
* 클로저 클래스
: 클로저를 만드는 데 쓰인 클래스를 말한다. 각각의 람다에 대해 컴파일러는 고유한 클로저 클래스를 만들어 낸다. 람다 안의 문장들은 해당 클로저 클래스의 멤버 함수들 안의 실행 가능한 명령들이 된다.
void Lambda_Test(void)
{
int x = 3;
// c1은 람다에 의해 만들어진 클로저의 복사본
auto c1 = [x](int y) { return x * y; };
// c2는 c1의 복사본
auto c2 = c1;
}
- 간단한 동작 방식
람다 표현식은 컴파일러가 하나의 클래스(클로저 클래스)를 자동으로 작성해서 그 클래스의 객체(클로저)를 생성하게 만든다.
* 기본 캡처 모드(default capture mode)
C++11의 기본 캡처 모드(default capture mode)는 다음과 같이 두 가지가 있다.
참조에 의한(by-reference) 캡처 모드
값에 의한(by-value) 캡처 모드
기본 참조 캡처 모드에서는 참조가 대상을 잃는 문제가 있을 수 있고, 기본 값 캡처 모드에서는 자기 완결적(self-contained)이지 않을 수 있다.
- 기본 참조 캡처 주의 사항
참조 캡처를 사용하는 클로저는 지역 변수 또는 람다가 정의된 범위에서 볼 수 있는 매개변수에 대한 참조를 가지게 된다. 하지만 다음과 같은 코드에서는 참조가 대상을 잃는 문제가 발생한다.
std::vector<std::function<bool(int)>> Filters;
void AddDivisorFilter(void)
{
// ...
int divisor = Compute();
// 참조 캡처를 사용하는 과정에서 divisor에 대한 참조가 대상을 잃는다.
//Filters.emplace_back([&](int value) { return (value % divisor) == 0; });
Filters.emplace_back([&divisor](int value) { return (value % divisor) == 0; });
}
void Lambda_CaptureMode(void)
{
std::vector<std::function<bool(int)>> filters;
AddDivisorFilter(filters);
// 추가 하는 과정에서의 참조 대상 divisor는 지역변수이기 때문에 여기서는 유효하지 않다.
filters[0](2);
}
람다에 의해 생성된 클러저의 수명이 그 지역 변수나 매개변수의 수명보다 길기 때문에 클로저 안의 참조는 대상을 잃는 현상이 발생한 것이다. [&], [&divisor] 어떤 형식을 사용하여도 마찬가지이다. 하지만 [&divisor]과 같이 명시적으로 써주면 람다의 클로저의 유효성을 좀 더 직관적으로 판단할 수 있다. (divisor가 람다의 클로저 이상으로 오래 살아있어야 한다고 직관적으로 알 수 있다.)
- 기본 값 캡처 주의 사항
위의 참조 캡처의 문제점을 해결하기 위해 다음과 같이 기본 값 캡처 모드를 사용할 수 있다.
std::bind의 첫 인수는 호출 가능한 객체이고, 나머지 인수는 그 객체에 전달할 값들을 나타낸다.
std::bind에 전달된 모든 인수의 복사본들을 포함한다.
람다가 생성한 클로저 또한 복사되어 바인드 객체에 저장된다.
C++11에서는 C++14에서 처럼 클로저 안으로 객체를 이동 생성하는 것이 불가능하기 때문에 바인드 객체에 저장한다.
바인드 객체에 저장된 객체는 클로저에 참조로 전달한다.
복사된 클로저와 바인드 객체의 수명은 동일하기 때문에 바인드 객체 안의 객체들을 마치 클로저 안에 있는 것처럼 취급할 수 있다.
std::bind는 전달된 인수에 대하여 왼값은 복사 생성된 객체, 오른값은 이동 생성된 객체를 가지고 있다.
람다로부터 만들어진 클로저 클래스의 operator(), 모든 자료 멤버는 const이다.
바인드 객체 안의 이동 생성된 data 복사본은 const가 아니다. 만약 data를 변경해야한다면 클로저의 operator함수를 non-const함수로 변경할 필요가 있다. 이는 람다를 작성할 때 mutable키워드를 붙여서 작성함으로써 실현된다.
*람다의 매개변수와 auto : 일반적 람다(generic lambda)
람다 표현식의 매개변수에도 auto 키워드를 사용할 수 있다. 이를 일반적 람다(generic lambda)라고 한다.
일반적인 auto를 사용한 람다 표현식은 다음과 같다.
auto f1 = [](auto x) { return normalize(x); };
만약 완벽 전달을 수행하려면 보편 참조 형식으로 바꿔주면 된다. 다만 매개변수를 다른 곳으로 넘겨줄 때 템플릿에서의 보편참조와 같이 std::forward<T>를 사용하지 못한다. (T라는 것을 알지 못하기 때문에)
하지만 그냥 자료형 그대로 넘겨주어도 문제가 없다. 참조 축약 규칙에 의해 오른값(int&& 등)을 넘겨주어도 오른값(int&& 등)을 돌려받는다.
auto f2 = [](auto&& x) { return normalize(std::forward<decltype(x)>(x)); };
여기서 많은 가변 매개변수를 받고 싶다면 다음과 같이 수정하면 된다.
auto f3 = [](auto&&... xs) { return normalize(std::forward<decltype(xs)>(xs)...); };
*람다 vs std::bind
결론부터 말하자면 람다가 가독성, 편의성 등 모든 면에서 뛰어나다고 볼 수 있다.
예시로 어떤 지점에서 한 시간 후부터 30초간 소리를 내는 함수가 있다고 가정한다.
enum class Sound { Beep, Siren, Whistle };
using Time = std::chrono::steady_clock::time_point;
using Duration = std::chrono::steady_clock::duration;
void SetAlarm(Time t, Sound s, Duration d) { /* ... */ }
void Lambda_Bind(void)
{
auto setSoundByLambda = [](Sound s)
{
using namespace std::chrono;
using namespace std::literals;
SetAlarm(steady_clock::now() + 1h,
s,
30s);
};
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;
auto setSoundByBind =
std::bind(SetAlarm,
steady_clock::now() + 1h,
_1,
30s);
}
람다와 바인드를 사용해 구현하면 위와 같이 구현된다.
- 람다는 바인드에 비해 명확하고 가독성이 좋다.
바인드는 자리표(placeholder)라는 것을 통해 setSoundByBind의 첫인수를 SetAlarm의 두번째 인수로 전달하는 역할을 한다. 바인드 객체를 통해 인수가 어떤 SetAlarm의 어떤 매개변수로 들어가는지 신경써야한다. 람다는 간단히 봐도 정확하게 파악할 수 있다.
한가지 예를 들어보자면 다음과 같다.
void Lambda_Bind(void)
{
int lowVal = 3;
int highVal = 10;
auto betweenByLambda = [lowVal, highVal](const auto& val)
{ return lowVal <= val && val <= highVal; };
using namespace std::placeholders;
auto betweenByBind =
std::bind(std::logical_and<bool>(),
std::bind(std::less_equal<int>(), lowVal, _1),
std::bind(std::less_equal<int>(), _1, highVal));
}
- 바인드 사용 시, 바인드 시점에 값이 결정되지 않도록 지연시킬 필요가 있다.
위의 코드에서 람다는 setSoundByLambda를 호출할 때 시간이 계산되어 한 시간 후 부터 30초간 알람이 울린다. 하지만 바인드의 경우 std::bind를 호출할 때 시간이 계산되어 이 시점기준으로 한 시간 후 30초간 알람이 울리게 된다. 이를 방지하기 위해서 다음과 같은 코드를 작성해야한다.
auto setSoundByBind =
std::bind(SetAlarm,
std::bind(std::plus</*steady_clock::time_point*/>(),
std::bind(steady_clock::now),
1h),
_1,
30s);
- 바인드 사용 시, 함수의 이름 밖에 알지 못하기 때문에 중복적재된 함수를 적절하게 선택할 수 없다.
이를 명시적으로 std::bind에 알려줘야 한다.
void SetAlarm(Time t, Sound s, Duration d) { /* ... */ }
// 중복적재 추가
void SetAlarm(Time t, Sound s, Duration d, int factor) { /* ... */ }
void Lambda_Bind(void)
{
// ...
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundByBind =
std::bind(static_cast<SetAlarm3ParamType>(SetAlarm),
std::bind(std::plus</*steady_clock::time_point*/>(),
std::bind(steady_clock::now),
1h),
_1,
30s);
}
- 바인드 사용 시, 함수 포인터를 통해 호출이 일어나기 때문에 컴파일러가 인라인화할 가능성이 더 낮다.
- 바인드 사용 시, 매개변수의 저장 방식(std::bind에서 매개변수로 받은 값들을 저장할 때)에 대해 알아야한다.(값으로 전달된다.) 람다에서는 캡처 방식에 따라서 명확하게 확인할 수 있다.
- 바인드 사용 시, 바인드 객체를 호출 시 인수의 전달 방식을 알야아 한다.(참조로 전달된다.)
바인드는 C++11에서만 가끔 쓰이는 경우가 있는데(C++14에서는 쓰이지 않는다.) 쓰이는 경우는 다음 두 가지이다.
f를 직접호출하면 컴파일러는 호출 지점에 함수에 전달된 인수들의 형식들과 f에 선언된 매개변수들의 형식들을 비교해서 호환 여부를 파악하고 필요에 따라 적절한 암묵적 변환을 수행해서 호출을 성사시킨다. 위의 코드에서는 {1,2,3}으로부터 임시 std::vector<int> 객체를 생성해서 f의 매개변수 input에 묶는다.
fwd를 통해 간접호출하게 되면 호출 지점에서 전달된 인수들과 f에 선언된 매개변수를 직접 비교할 수 없다. 컴파일러는 fwd에 전달되는 인수들의 형식을 연역하고 연역된 형식들과 f의 매개변수 선언들과 비교한다. 과정에서 다음 조건 중 하나라도 만족되면 완벽 전달이 실패한다.
fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 연역하지 못한다.
fwd의 매개변수들 중 하나 이상에 대해 컴파일러가 형식을 잘못 연역한다. 잘못 연역하였을 때 그 형식으로 fwd의 인스턴스를 컴파일하지 못하거나 컴파일은 되지만 직접 호출할 때와 다르게 행동하는 결과를 가져온다.
여기서 문제는 std::initializer_list가 될 수 없는 형태로 선언된 함수 템플릿 매개변수에 중괄호 초기치를 넘겨준다는 것이다. 간단히 말하면 {1,2,3}의 형식을 컴파일러가 연역하는 것이 금지되어 fwd의 매개변수의 형식을 연역할 수 없는 뜻이다.
이를 방지하기 위해 auto 변수를 중괄호 초기치로 초기화하고 넘겨주면 된다.
auto input = { 1,2,3 };
fwd(input);
- 널 포인터를 의미하는 0 또는 NULL
0이나 NULL을 널 포인터로서 템플릿에 넘겨주려 하면 컴파일러가 이를 포인터 형식이 아닌 정수 형식으로 연역하기 때문에 문제가 생긴다. 해결책은 0또는 NULL 대신 nullptr을 사용하면 된다.
- 선언만 된 정수 static const 및 constexpr 자료 멤버
어떤 컴파일러에서는 static const, static constexpr 자료 멤버가 선언만 되어있고 정의가 되어있지 않을 때 해당 변수의 주소값을 취한다면 해당 변수에 대한 저장소가 따로 마련되어있지 않아 링크가 실패하는 경우가 있다.(정의가 없어서)
보편 참조 역시 참조를 사용하기 때문에 내부적으로 포인터와 비슷하게 동작하고 있다. 따라서 보편 참조 템플릿 함수 호출에 실패하는 경우가 있다. 해결 방법은 정의를 해주면 된다.
- 중복적재된 함수 이름과 템플릿 이름
함수 포인터를 매개 변수로 받는 함수로 중복적재된 함수를 완벽 전달하려고 할 때 문제가 생긴다.
int InputFunction(int a) { return a; }
int InputFunction(int a, int b) { return a; }
void f(int (*fp)(int)) {}
template<typename... Ts>
void fwd(Ts&&... params)
{
f(std::forward<Ts>(params)...);
}
void Test(void)
{
f(InputFunction);
// 형식이 없다 > 형식연역 불가능
//fwd(InputFunction);
}
그냥 직접 호출하는 것은 f의 선언에서 매개변수 형식과 일치하는 것을 찾으면 되기 때문에 적절한 중복적재 버전을 선택하여 넘겨줄 수 있다. 하지만 fwd 함수에 넘겨줄 때는 호출에 필요한 형식에 관한 정보가 없어서 어떤 중복적재 버전을 선택할지 결정하지 못한다.
이를 해결하기 위해서 전달하고자 하는 중복적재나 템플릿 인스턴스를 명시적으로 지정하면 된다. 다음과 같다.
using InputFunctionType = int (*) (int);
fwd(static_cast<InputFunctionType>(InputFunction));
- 비트필드
특정 빌트필드 직접적으로 지칭하는 방법은 없으며 임의의 비트들을 가리키는 포인터를 생성하는 방법은 없다. 따라서 비트필드를 인수로 받는 임의의 함수는 그 비트필드 값의 복사본을 받게 해주면 된다.