The Modernest C++ #3 - 수학 라이브러리
[이전 글]
The Modernest C++ #1 - 템플릿 메타프로그래밍 (Template Metaprogramming)
The Modernest C++ #2 - 타입을 가지고 놀아보기, type_traits
수학 라이브러리
이전 글까지 우리는 템플릿 메타프로그래밍을 이용하여 컴파일 타임에 타입을 확인하고, 관리하는 역할을 가지는 메타 함수를 작성해보면서 기본기를 익히는 시간을 가졌습니다. 하지만 천만 다행이게도 템플릿 메타프로그래밍의 위력은 여기서 끝이 아닙니다. 그저 타입만을 가지고 연산할수 있는게 아니라 우리는 이제 C++ 의 비형식 템플릿 매개변수를 눈여겨봐야 할 차례가 왔습니다.
<사진 1 - is_true 메타 함수>
우리는 저번 시간에 배운대로 보자면 <사진 1>의 is_true 메타 함수는 템플릿 매개변수로 bool 데이터 타입의 값을 받고 그를 이용해서 템플릿 매개변수 expr로 전해진 값이 true 인지 컴파일 타임에 감별하는 간단한 메타 함수임을 알수 있습니다. 여기서 우리가 알수 있는건 템플릿 매개변수로 타입이 아니라 값(value) 또한 전달이 가능하다는 내용입니다. 이를 비형식 템플릿 매개변수라고 하는데 제한적으로 정수형 데이터만 전달이 가능합니다, 문자열이나 실수형 데이터는 안되기 때문에 이 점을 유의 하셔야 합니다.
제목을 보시면 가늠하실수 있으시겠지만 우리는 이제 이 비형식 템플릿 매개변수를 이용하여 다양한 수학 연산을 하는 메타 함수를 작성하는 시간을 가질겁니다. 물론 이 수학 라이브러리는 런타임에서 연산될 수 없습니다. 그 이유로는 런타임에 받는 기본적인 매개변수가 아닌 컴파일 타임에 상수로 받는 매개변수이기 떄문에 컴파일 타임에 확인이 가능한 상수 연산만 가능합니다.
분명 컴파일 타임에만 가능하기 때문에 변수 사이의 연산이 불가능한 점 때문에 이점을 많이 가지지 못하는 기능이라고 생각하실수도 있으실수 있습니다. 하지만 우리는 제한된 측면에서라도 프로그래밍을 하는것이기 때문에 일반 프로그래밍 언어와 유사한 재귀 함수와 같은 코드 테크닉을 구현할수 있습니다. 또한 그렇기 때문에 팩토리얼과 같은 비교적 고급 연산도 수행할 수 있습니다.
설명은 이쯤 하도록 하고 직접 코딩을 해보는 시간을 가져볼까요?
<사진 2 - add 메타 함수>
add 메타 함수는 두개의 정식 데이터를 받아와서 더하는 아주 기초적인 메타 함수입니다. 어떠신가요? 이런식으로 subtract (뺄셈), multiply (곱셈), divide (나눗셈) 또한 구현이 가능하시겠다면 앞으로의 내용도 이해하는데 그렇게 어렵지는 않으실겁니다. :), 하지만 우리는 이런 메타 함수들은 그냥 부호 하나만 써주면 되기에 그렇게 메리트가 없습니다. 더욱 고급 함수를 구현해보도록 할까요?
다음은 거듭제곱을 구하는 함수를 구현해볼까 합니다. 사용 방법은 최대한 C++의 수학 라이브러리와 동일하게 작성해볼까 하는데 뭔가 조금 생각해봐야 합니다. C++ 의 pow 함수는 밑과 지수를 매개변수로 받습니다. 그 말은 아래와 같은 방식으로 코딩하면 절대로 안된다는걸 뜻합니다.
<사진 3 - power 메타 함수의 잘못된 예>
<사진 3>의 power 함수는 지수에 어떤 수가 들어올지 모르므로 잘못된 구현입니다. 만약 exponent 매개변수에 '3' 이라는 값이 들어오면 base 를 3번 거듭해서 곱해주는 연산을 처리해야 하는데 위 방법으로는 구현을 할 수 없습니다. 템플릿 특수화를 쓰면 어느정도 할수 있겠지만 좋지 않은 생각입니다. exponent 매개변수는 데이터 타입이 int 이므로 경우의 수가 굉장히 많기 때문입니다.
그럼 어떻게 해야할까요? 우리는 여기서 재귀함수와 같은 매커니즘을 구현해야 합니다. 단순히 base를 여러번 곱하는게 아닌 말 그대로 재귀하는 메타 함수의 호출을 구현하는 것입니다. 해답은 가까운곳에 있습니다. 바로 우리가 C++ 언어로 재귀 함수를 작성하는 것과 똑같습니다. 그저 똑같은 함수를 계속 호출해주기만 하면 됩니다.
<사진 4 - power 메타 함수의 올바른 예>
우리는 power 함수의 정의(라인 9 ~ 라인 13)를 보면 value 의 값을 base의 power<base, exponent - 1>::value 곱 으로 대입해주는 것을 볼수 있습니다. 그 어떤수라도 0승은 1이기 때문에, 템플릿 특수화를 이용해 exponent 메개변수가 0 일때 value는 1로 고정되도록 하였습니다 (라인 15 ~ 라인 19). 메타 함수가 무한 루프에 빠지지 않도록 1 씩 빼주면서 계속 연산을 하고, 마침내 exponent가 0일때 1을 곱해주면서 연산이 끝나게 됩니다. 아래와 같은 연산 과정을 가지게 되는 것입니다. :
> power<2, 2>::value;
> 2 * power<2, 1>::value;
> 2 * 2 * power<2, 0>::value;
> 2 * 2 * 1
> 4
이런식으로, 우리는 거듭제곱을 구하는 간단한것 같지만 복잡한 메타 함수를 작성하였습니다. 이런식으로 다른 함수들도 구현할 수 있습니다.
<사진 5 - factorial 메타 함수>
<사진 6 - fibonacci_sequence 메타 함수>
지금까지 올렸던 자료들에서 보실수 있으시듯이 템플릿 메타프로그래밍은 아이디어만 있다면 그 활용성이 무한하기 때문에 유연성이나 속도면에서 굉장히 유리하다고 생각됩니다. 하지만 계속 말씀드리는 단점인 런타임에서의 실행은 불가능하기 때문에 불편함이 다분할수 있습니다. 하지만 연산을 단 한번만 수행하여 중요한 연산을 하기에 앞서 상수 배열을 선언하거나 하는 유용한 작업을 할수 있습니다.
- 추가 팁 -
1. 위 함수들은 재귀 함수의 특성을 띄고 있습니다. VS2015 (VC++ 2015) 에서는 재귀 횟수가 일정 범위 이상으로 넘어가면 아래와 같은 에러가 발생하게 됩니다. (다른 컴파일러는 어떨지 모르겠네요)
fatal error C1202: 재귀 형식 또는 함수 종속성 컨텍스트가 너무 복잡합니다.
이런 점을 감안해서 메타 함수를 재설계 하거나 조절하는 등의 작업을 미리 하시는게 크게 유용하게 작용할것입니다.
2. 일일히 표준 출력 스트림에 결과 값을 출력하는 방법을 이용하는 대신 마우스를 올리면 결과값을 확인하실 수 있습니다. (VS2015 기준)
만약 메타 함수 호출이 에러를 유발하는 경우 <error-constant> 를 보여주게 되니 에러를 컴파일 이전에 확인하는데 큰 도움이 될수 있겠죠?