Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- A. Steed 2: Cruise Control
- 파머스테이블
- codejam
- 치명적 귀여움
- CodeJam 2017 Round 1B
- 카페
- 먹기좋은곳
- 스코티쉬 스트레이트
- CDJ
- 스테이크
- 발산역 근처 카페
- 냥냥
- 고양이
- 냥이
- coffee
- 발산
- 소호정본점
- 발산맛집
- 데이트
- 파버스
- 냥스토리
- 안동국시
- 고양이는 언제나 귀엽다
- 양재맛집
- 레스토랑
- 커플
- 부모님과
- RED CAT COFFEE X LOUNGE
- 스파게티
- 소호정
Archives
- Today
- Total
hubring
[EC++] 항목 48: 템플릿 메타 프로그래밍, 하지 않겠는가? 본문
항목 48: 템플릿 메타 프로그래밍, 하지 않겠는가?
템플릿 메타프로그래밍(template metaprogramming: TMP)
- 템플릿 메타프로그래밍 란?
- 컴파일 도중에 실행되는 템플릿 기반의 프로그램을 작성하는 일.
- C++ 컴파일러가 실행시키는, C++로 만들어진 프로그램.
- TMP 프로그램이 실행을 마친 후에 그 결과로 나온 출력물(템플릿으로부터 인스턴스화된 C++ 소스코드)이 다시 보통의 컴파일 과정을 거치는 것.
- 1990년 초 TMP개념이 발굴된 후 C++ 언어 및 표준라이브러리에 TMP를 용이하게 만드는 확장요소가 추가될 여지까지 생김.
- TMP의 강점
- TMP를 쓰면 다른 방법으로 까다롭거나 불가능한 일을 쉽게 할 수 있음
- C++ 컴파일이 진행되는 동안에 실행되기 때문에, 기존 작업을 런타임 영역에서 컴파일 타임 영역으로 전환할 수 있음.
- 일반적인 프로그램 실행 도중에 잡혀 왔던 에러들을 컴파일 타임에 찾을 수 있다는 점
- TMP를 써서 만든 프로그램이 효율적일 여지가 많다는 점
- 컴파일 타임에 이미 수행하여 실행 코드가 작아지고 실행시간이 짧아지며 메모리도 적게 잡음 (대신 컴파일 타임이 길어짐)
예제
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
if(iter가 임의 접근 반복자이다.){ //임의 접근 반복자에 대해서
iter += d; //반복자 산술 연산을 쓴다.
}
else{
if(d >= 0) { while (d--) ++iter; } // 다른 종류의 반복자에 대해서는
else { while (d++) --iter; } //++ 혹은 -- 연산의 반복 호출을 사용
}
}
- 반복자 종류 참고
- 위는 STL의 advance와 유사한 코드이다.
- 이 유사코드를 진짜 코드로 만들려면 아래 코드처럼 typeid를 쓸 수 있다. (지극히 밋밋한 c++ 적인 방법)
- 타입 정보를 꺼내는 작업을 런타임에 하겠다는 의미이다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
if(typeid(typename std::iterator_traits<IterT>::iterator_catetory)==
typeid(std::random_access_iterator_tag)){ //임의 접근 반복자에 대해서
iter += d; //반복자 산술 연산을 쓴다.
}
else{
if(d >= 0) { while (d--) ++iter; } // 다른 종류의 반복자에 대해서는
else { while (d++) --iter; } //++ 혹은 -- 연산의 반복 호출을 사용
}
}
- 항목 47에서 지적했듯이, 특성정보(traits)를 쓰는 방법보다 효율이 떨어진다.
- 타입 점검 동작이 컴파일 도중이 아니라 런타임에 일어나기 때문이며
- 런타임 타입 점검을 수행하는 코드는 어쩔 수 없이 실행 파일에 들어가야하기 때문이다.
- 특성정보 방법이 TMP이기 때문에 타입에 따른 if...else 처리를 컴파일 타임에 할 수 있었던 것이다.
- typeid 방법은 성능 외에도 컴파일 문제를 일으킬 수 있다.
std::list<int>::iterator iter;
...
advance(iter, 10); // 10개 원소 만큼 앞으로 옮기려 하지만 컴파일이 안됨
- 위의 코드를 컴파일러가 돌린다고 가정했을 때, 다음과 같은 advance가 생성될 것이다.
void advance(std::list<int>::iterator& iter, int d){
if(typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_catetory)==
typeid(std::random_access_iterator_tag)){
iter += d; // 에러!
}
else{
if(d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}
std::list::iterator
는 양방향 반복자이기 때문에+=
연산을 지원하지 못한다.- 하지만 지금은
typeid
점검이 실패하기 때문에+=
줄까지도 실행될 수가 없다. - 모든 코드가 제대로 되어 있는지 확인하는 일은 컴파일러의 책무이고
iter += d
부분은iter
가 임의 접근 반복자가 아닌 한 컴파일되지 않을 것은 자명하다. - TMP를 썼다면 주어진 타입에 따른 코드가 별로로 함수로 분리될 것이고, 각각의 함수는 자신이 맡은 타입에 대한 연산만 수행할 것이다.
- TMP는 그 자체가 튜링 완전성을 갖고 있는 것으로 알려져 왔다.
- 범용 프로그래밍 언어처럼 어떤 것이든 계산할 수 있는 능력을 갖고 있다는 뜻이다.
- 변수 선언도 되고, 루프도 실행할 수 있으며, 함수를 작성하고 호출하는 것까지도 된다.
- 단 이런것들에 필요한 구문요소가 "보통"의 C++에서 쓰이는 구문요소들과 꽤나 다른 모습을 갖고 있다.
- if...else 조건문을 나타내는 데는 템플릿 및 템플릿 특수화 버전을 사용한다.
- 프로그래밍 언어 수준으로 보면 이런 방법은 TMP 어셈블리라고 할 수 있다.
- TMP용 라이브러리도 많이 있다. (부스트의 MPL이 대표적 항목 55 참조)
TMP 동작 원리 예
- TMP의 동작 원리를 엿볼 수 있는 부분으로 루프가 있다.
- 반복(iteration)의미의 진정한 루프는 없기 때문에 재귀(recursion)를 사용해서 루프의 효과를 낸다.
- 단 이 재귀조차도 우리가 알고 있는 종류가 아니다.
- 실제론 재귀 함수 호출을 만들지 않고 재귀식 템플릿 인스턴스화(recursive template instantiation)를 하기 때문이다.
- 아래는 재귀 호출을 하는 기본적인 TMP 계승 계산방법이다.
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> {
enum { value = 1 };
}
Factorial<n>::value
를 참조함으로 n의 계승을 바로 얻을 수 있다.- 이 코드에서 루프를 도는 위치는 템플릿 인스턴스인
Factorial<n>
의 내부에서Factorial<n-1>
을 참조하는 곳이다. - 재귀를 끝내는 특수조건은 바로
Factorial<0>
이다. - Factorial 템플릿은 구조체 타입이 인스턴스화되도록 만들어져 있다. 그리고 이렇게 만들어진 구조체 안에는 value라는 이름의 TMP 변수가 선언이 되어 있다.
- 이것은 항목 2에서 말한 나열자 둔갑술(enum hack)이 쓰인 것이다.
- 재귀적으로 돌면서 템플릿 인스턴스화 버전마다 자체적으로 value의 사본을 갖게 되고 각각 value에는 만들어진 값이 담기게 된다.
- Factorial 템플릿은 다음과 같이 사용하면 된다.
int main(){
std::cout << Factorial<5>::value; // 런타임 계산 없이 출력
std::cout << Factorial<10>::value;
}
TMP 적용이 적합한 예
- 치수 단위(dimensional unit)의 정확성 확인
- 과학 기술 분야의 응용프로그램에서 치수 단위(질량, 거리, 시간 등)가 똑바로 조합되어야하는 것이 최우선
- 예를 들면 속도를 나타내는 변수에 질량을 나타내는 변수를 대입하면 에러
- TMP를 사용하면 프로그램 안에서 쓰이는 모든 치수 단위의 조합이 제대로 됐는지를 맞춰(컴파일 동안에) 볼 수 있다.
- 선행 에러 탐지(early error detection)에 TMP를 써먹을 수 있는 사례이다.
- 분수식 지수 표현이 지원이 된다. 이런 표현이 가능하려면 컴파일러가 확인할 수 있도록 컴파일 도중에 분수의 약분이 되어야한다. time^(1/2) 는 time^(4/8)과 똑같이 받아들여져야 한다는 점이다.
- 행렬 연산의 최적화
- operator* 등의 어떤 연산자 함수는 연산 결과를 새로운 객체에 담당 반환해야한다고 항목 21에서 이야기 했으며
- 항목 44를 보신 분은 squareMatrix클래스를 기억할 것이다.
typedef squareMatrix<double, 10000> BigMatrix; BigMatrix m1, m2, m3, m4, m5; ... BigMatrix result = m1 * m2 * m3 * m4 * m5; //행렬의 곱을 계산
- 곱셈 결과를 보통 방법으로 계산하려면 네 개의 임시 행렬이 생겨야 한다. (operator* 한번씩 호출할 때 반환 결과로 생성됨)
- 행렬 원소들 사이에 곱셈을 해야하므로 네 개의 루프가 순차적으로 만들어질 수 밖에 없다.
- 이런 비싼 연산에 TMP를 사용할 수 있다.
- TMP를 응용한 고급 프로그래밍 기술인 표현식 템플릿(expression template)을 사용하면 덩치 큰 임시 객체를 없애는 것은 물론이고 루프까지 합쳐 버릴 수 있다.
- 게다가 위에 써 놓은 사용자 코드에서 문법 하나 바꾸지 않고 적용할 수 있다.
- 맞춤식 디자인 패턴 구현의 생성
- 전략 패턴, 감시자 패턴, 방문자 패턴 등의 디자인 패턴은 그 구현 방법이 여러가지일 수 있다.
- TMP를 사용한 프로그래밍 기술인 정책 기반 설계(policy-based design)라는 것을 사용하면, 따로따로 마련된 설계상의 선택(정책)을 나타내는 템플릿을 만들어 낼 수 있게 된다.
- 이렇게 만들어진 정책 템플릿은 임의대로 조합되어 사용자의 취향에 맞는 동작을 갖는 패턴으로 구현되는 데 쓰인다.
- 예로 몇개의 스마트 포인터 동작 정책을 하나씩 구현한 각각의 템플릿을 만들어 두고, 이들을 사용자가 마음대로 조합하여 수백 가지의 스마트 포인터 타입을 생성할 수 있게 하는 것이다.
- 생성식 프로그래밍(generative programming)의 기초가 바로 이 기술이다. (자동화된 코드 생성을 사용하는 프로그래밍 방식)
TMP 단점
- 문법이 비 직관적이고 개발도구의 지원도 아주 미약하다.(디버깅 불가능)
- 비교적 최근 발견된 것으로 TMP의 프로그래밍 관례들도 아직 미약하다.
- 그럼에도 불구하고 컴파일 타임에 수행하는 장점으로 매력이 있으며 점점 내외적 지원과 관련 서적들이 늘어가고 있다.
- 특히 라이브러리 개발자라면 알아야할 프로그래밍이다.
이것만은 잊지 말자
- 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환하는 효과를 낸다.
- 따라서 선행 에러 탐지와 높은 런타임 효과를 줄 수 있다.
- 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있으며, 또한 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 쓸 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[EC++] 항목 3 : 낌새만 보이면 const를 들이대 보자! (0) | 2021.06.16 |
---|---|
[EC++] 항목 2 : #define을 쓰려거든 const, enum, inline을 떠올리자. (0) | 2021.06.12 |
[EC++] 항목1 C++를 언어들의 연합체로 바라보는 안목은 필수 (0) | 2021.06.11 |
[EC++] 독자 여러분 반갑습니다 (0) | 2021.06.11 |