2015년 9월 9일 수요일

[C++11] Traits

Traits
: template 에 전달되는 type 의 속성에 접근하는 기법


printv 함수에 전달된 v가 pointer 일 경우,
주소 뿐만 아니라 값도 출력하려면 어떻게 해야 할까?
T가 포인터인지 아닌지를 판단할 수 있으면 되지 않을까?



먼저 T type을 인자로 받는 primary template 을 선언한다.
내부에는 enum 값을 초기화해서 구조체 외부에서도 접근이 가능하도록 한다.

우리는 T type이 포인터인지 아닌지를 구별하는 것이 중요하므로,
Partial specialization(부분 전문화)를 이용해서 T* type을 인자로 받는
template 을 선언한다.


위예제에서
primary template 은 false 를 반환
partial template 은 true를 반환할 것이다.

결과적으로 IsPointer<T>::value 는 전달되는 T type에 따라서
포인터 인지 아닌지 true or false로 나뉘게 되고
이에 따라 분기하여 처리할 수 있게 된다.

이것이 traits 기법의 핵심이다.

또한, main함수에서 foo(&n); 만 호출했다면
IsPointer<T>::value 의 값은 complie time 에 정해져 버린다.
따라서 else 블록 안의 cout << "Not Pointer!!" << endl; 코드는 컴파일 최적화에 의해 사라지게 된다.




만약 printv(&n); 코드 없이 printv(n); 만 있다면 error 가 발생하는 이유는 무엇일까?
printv(n); 을 호출하면
template<typename int> void printv(int v) 로 코드 생성
IsPointer<int>::value 는 false 로 정해진다.
결국 if(false) 로 코드 생성.
그리고 컴파일러는 최적화전에 컴파일을 해서 코드에 이상이 없는지 확인을 한다.
그런데, cout << v << ", " << *v << endl; 코드에서
*v 는 int의 역참조이기 때문에 error 가 발생한다.


C++ 컴파일러가 동작하는 원리를 살펴보면,
1. 사용된 함수 T의 타입이 결정되면 실제 함수 code 생성
2. 생성된 함수 컴파일
3. 실행되지 않는 부분(code)은 최적화를 통해 제거


하지만 만약 printv(&n); code 도 활성화되었다면?
위에서 처럼 컴파일러가 IsPointer<t>::value 를 false로 컴파일 하여도
if-else 구문은 runtime 에 결정되는 실제 동작이 결정되므로,
컴파일러는 if-else 구문에 어떤 것이 호출될지 알 수 없어서,
2개의 template code 를 모두 생성하게 된다.
따라서 int type인 v를 *v 로 역참조하는 error가 발생한다.

결론적으로, if-else 구문으로 T type이 포인터인지 아닌지를 판별하지 않고
function overloading 을 사용한다.

Function overloading : compile time에 결정되며,
실제 호출되지 않는 함수 템플릿은 인스턴스화 되지 않는다.
if-else : runtime에 결정




IsPointer<T>::value 의 반환값은 pointer 인지 아닌지에 따라 true or false 이다.
true or false 는 int 로 0, 1 인데
overloading 을 사용하려면 type 이 달라야 하는데
0, 1 은 둘 다 int 이므로 같은 type이라 overloading 을 할 수 없다.

YES?, NO?를 어떻게 작성해야 할 지 생각해보자.



그전에 type이 구분된다는 것은 어떤 의미인가?

0, 1은 같은 int type 이라
foo(0), foo(1)은 같은 함수를 호출한다.




int2type<0>, int2type<1> 은 다른 type이 된다.
컴파일러가 컴파일 시에 int2type<0>, int2type<1> 2개의 다른 type에 따른 코드를 만들게 되고
foo(t0), foo(t1) 은 다른 함수를 호출한다.

이론적으로, int2type은 전달되는 N에 따라서 무궁무진하게 다른 type이 생성될 수 있다.
이 int2type 에 사용된 기법을 토대로
다양한 데이터 타입과 값에 대해 다른 type을 가지는 코드를 만들 수 있다.




int2type<>의 타입에 IsPointer<T*>::value 을 전달함으로써,
true(결국 1), false(결국 0)가 서로 다른 타입이 되게 된다.
그러므로, printvImp 함수 template도 2가지로 나뉠 수 있게 되어
function overloading 이 되는 것이다.

또한 printvImp 함수 템플릿에 전달하는 int2type<1>, int2type<0> 에는
최적화를 위해 임시 객체를 사용하도록 변수 이름을 적지 않아야 한다.
그래야 컴파일러가 임시 객체를 쓰려고 하는 의도를 파악하고
최적화에서는 사용만 하고 없애버린다.



그렇다면 int 의 값에 따른 type 구분뿐만 아니라 모든 데이터 타입에 적용할 수 있지 않을까?




위에서 얘기했던 traits 관련된 모든 기법이 C++11/14에 표준화되어 있기 때문에
type_traits header를 포함하면 사용할 수 있다.





추가적으로 traits 기법을 이용해서 알고리즘을 작성할 때, 주의해야 할 점은
if-else 구문을 사용할 경우, 위 예제와 같이 *(asterisk) 를 통한 포인터 접근 표현이 불가능하다.
하지만 function overloading 을 사용할 경우, 가능하다.


이번에는 pointer가 아닌 array type도 생각해보자.


배열의 이름은 자기 자신의 시작 주소를 나타내고, 주소(&) 값은 arr이나 &arr이나 동일하다.
하지만, C++에서는 int* pArr1 = &arr; 코드에서 에러를 발생시킨다.

배열명은 배열의 시작 주소라고 답하는 것은 30점짜리 답안이다.
배열명은 배열의 첫 번째 원소 시작 주소를 의미하는 상수 포인터이다.
배열명을 사용하면, 배열의 전체 타입이 첫 번째 원소의 타입으로 축소 또는 퇴화(decay)된다.
하지만 배열을 template으로 전달하여 사용할 경우, 퇴화(decay)되지 않는다.

결국엔 arr과 &arr 의 차이는 타입에 있다.
arr : 배열의 시작 주소, 타입은 첫 번째 원소
&arr : 배열의 시작 주소, 타입은 배열 전체


인자로 들어오는 타입이 배열인지 아닌지를 구분하는 코드를 구현해보자








댓글 없음:

댓글 쓰기

[C++] meta programing

재귀 호출에 관해 template meta programming 을 적용한 예제를 살펴보자. #include using namespace std; int fact(int n){ if(n factorial 연산을 하는 일반적인 재귀 호출 함...