원문 : How Do Those Funky Placeholders Work?


주의 : 허락받고 번역한 것이 아니므로 언제든지 내려갈 수 있습니다.

주의 : 번역이 개판이므로 이상하면 원문을 참조하세요.

주의 : 가독성을 높이기 위해서 발음이 비슷하거나 잘 알려진 용어는 한글로 표기합니다.


MPL 을 하다가 보면 가장 이해가 안 되는 것이 자리표 표현식( placeholder expression )이다. 아주 간단한 형태의 자리표 표현식같은 경우에는 그나마 이해가 가지만, 조금만 복잡해져도 정신이 안드로메다로 가 버린다.


"C++ Template Metaprogramming" 의 내용 중에 다음과 같은 코드가 있다. 이해가 가는가? 



나름 자리표 표현식에 대해서 이해했다고 생각했었는데, 필자도 쉽게 이해할 수가 없었다. 그래서 공부도 하는 겸해서 자리표 표현식과 관련한 글을 번역해 보기로 했다.




How Do Those Funky Placeholders Work?


요즘 C++ 표준 함수 binder 들은 정말로 사용하기 어렵다. Chris Gibson 은 Boost 의 대안의 비밀을 보여 준다. 그것은 더욱 낫고 마법같다.


새로운 코딩 기법에 대해서 배우고 그것을 작업하면서 사용하려고 할 때마다, 부서 전반에서 그것이 얼마나 빠르게 받아들여지는지 확인하는 것은 흥미롭다. 널리 전파되는 가장 빠른 것 중의 하나는 boost bind 라이브러리이다. 표준 파이브러리의 bind1st 나 bind2nd 적응자( adapter )들보다 훨씬 사용하기 수비고 훨씬 유연하다. 일단 재미있게 보이는 자리표( placeholder )를 이해하게 되면, bind 라이브러리는 사용하기에 우아하고 단순하다. 그리고 초기의 노력이 보상받게 된다. 당신의 코드는 깔끔해지며, 이해하기 쉬워지며, 빠르게 검토하기 쉬워지며, 빠르게 테스트할 수 있게 된다. 그것은 버그를 포함할 가능성이 더 적으며 아마도 더욱 효율적일 것이다.


내 경험상 평균적인 개발자들은, 물론 당신을 말하는 것은 아니고 나는 확실히 아니다, bind 라이브러리를 처음 사용할 때 아마도 너무 많거나 너무 적거나 잘못된 자리표를 사용할 것이다; 그리고 적어도 한 군데는 잘못된 위치에 그것들을 배치할 것이다. 이런 것들은 여러 페이지의 컴파일 에러들을 발생시킬 것이다. 그리고 나서 평균적인 개발자들은 그것이 너무 어렵다고 생각해서 템플릿은 악마의 조화라고 판단할 것이다. 그리고 그들은 매뉴얼을 읽는데 시간을 소비하지 않고, 그저 그것이 작동하기만을 원할 것이다. So they hand code a solution instead and decide to do it properly before review or release, which in all probability never happens.


그러므로 나는 bind 라이브러리가 직관적인지 그렇지 않은지 판단할 수 없다. 일단 이해하고 나면 직관적이다. but doesn't intuitive mean you don't need to get the hang of it? Can it be true that the more you use the bind library the more intuitive it gets?


일단 개발자들이 그것을 이해하고 나면 언제나 다음과 같이 질문한다. "그런데 그 멋진 자리표는 어떻게 작동하는 거죠?". 이상적인 세계에서는 당신은 라이브러리의 세부사항을 구현하는 것에 관심을 가지지 않을 것이며, 당신은 그냥 그것을 사용하면 될 것이다. 그러나 무대 뒤에서 무슨 일이 벌어지고 있는지 이해하는 것은 몇 가지 이유때문에 유용하다. 당신이 오타를 쳤을 때 평균적인 컴파일러가 제공하는 에러 메시지를 확인하면, 어떤 일들이 벌어지고 있는지 이해하는 데 큰 도움이 된다. 두 번째로 전문가에 의해 개발된 설계를 공부하는 것은 항상 유익하며 당신의 설계를 개선하는데 도움이 될 수 있다. 마지막으로 거의 대부분의 개발자들은 당연히 호기심이 많고, 자리표 구문을 마음에 들어 하면 보통 그것이 작동하는 방식에 대해 알기를 원한다.


이 기사에서는 bind 라이브러리의 자리표가 작동하는 방식에 대해 설명하려고 하는데, 매우 기본적인 bind 라이브러리를 만들 것이다. 그것은 그다지 대단하지 않을 것이지만, 당신의 궁금증을 만족시켜주기에는 충분할 것이라 생각한다. 예제들은 무엇보다도 명확성을 중시하며, 효율성, const 정확성, volatility 등과 같은 다른 이슈들은 고려되지 않는다. 함수 반환 형식들도 고려되지 않으며 void 라고 가정된다는 것에 주의하라.


A quick recap


당신이 widget 컬렉션을 가지고 있으며 당신은 각각에 대해 어떠한 동작을 수행하기를 원하고 있다고 가정하자; 당신은 단순히 다음과 같이 작성할 것이다 :



for_each 알고리즘은 단항 함수( unary function )를 세 번째 매개 변수로 취하는데, 그것은 범위 내의 각 반복자( iterator ) 당 한 번씩 호출되며, 참조해제된 반복자( 역주 : 반복자가 참조하고 있는 실제 값 )를 단항 함수의 단일 인자로 넘긴다( 아래를 보라 ). 우리의 대상 함수는 세 개의 매개 변수를 가지는데, 그것은 두 개의 문제를 가지고 있다 :


  • 단항 함수와 대상 함수의 매개 변수의 개수가 다르다.
  • 참조해제된 반복자는 대상 함수의 매개 변수 중의 어떤 것에라도 사상될 수 있다.



매개 변수의 개수가 다른 문제를 해결하는 것은 소프트웨어 공학의 근본 정리( Fundametal Theory of Software Engineering )을 적용하고 알고리즘과 대상 함수 간에 간접층( level of indirection )을 추가함으로써 수행된다. 간접층은 오브젝트의 형태를 취하며, 그것을 바인더( binder )라고 부르도록 하자. 바인더의 역할은 대상 함수의 인터페이스를 호출하는 알고리즘의 인터페이스를 받아들이는( adapt ) 것이다. 바인더는 for_each 에 의해 호출될 수 있다. 왜냐하면 그것은 단항 연산자 () 를 가지고 있기 때문이다. 그리고 바인더는 대상 함수를 호출할 수 있다. 왜냐하면 그것은 대상 함수에 대한 포인터와 두 개의 부가적인 인자를 생성자에서 공급받기 때문이다.


DoSomething 의 세 매개 변수들 중의 어떤 것이 widget 이어야 하는지 지정하는 것이 자리표가 오게 되는 곳이다. bind 라이브러리를 사용해서 우리는 이를 단순화할 수 있다 :



bind 함수는 네 개의 인자들을 받는다 : 대상 함수인 DoSomething 의 주소, Gadget 오브젝트, Gizmo 오브젝트, 자리표 _1. bind 에 대한 두 번째, 세 번째, 네 번째 인자의 순서는 대상 함수에 대한 인자의 순서와 같다는 데 주목하라. 자리표는 for_each 에 의해 단항 함수에 넘겨지는 인자의 위치를 지시한다. 만약 DoSomething 의 시그너쳐( signature )가 widget 을 두 번째 매개 변수로 기대했다면, 우리는 다음과 같이 작성해야할 것이다 :




















주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 2-6* :


처음에는 Boost 형식 특질 라이브러리가 제공하는 형식 산술이 완벽해 보이겠지만, 사실은 그렇지 않다. 그 라이브러리의 수단들이 포괄하지 않은 형식 범주들과 관계들, 변환들이 적어도 몇 개는 존재한다. 예를 들어, 그 라이브러리는 부호 있는 정수 형식에 대한 부호 없는 버전의 정수 형식을 얻을 수 있는 수단을 제공하지 않는다. 그런 빠진 부분들을 최대한 많이 찾아 볼 것. 각 특질 범주마다 적어도 하나는 있으며, 전체적으로 적어도 11 개는 있다. 빠진 특질들 각각의 인터페이스를 설계하고, 그런 특질이 필요함을 보여 주는 용례도 제시하라.




풀이 :


일단 이 문제를 풀기 위해서는 "형식 산술" 이 의미하는 바에 대해서 이해할 필요가 있다. 책에서는 "형식 산술" 을 다음과 같이 정의하고 있다.


[ 표 2.5 ] 에 나와 있는 메타 함수들은 근본적인 형식 계산들을 수행하는 메타 함수들이다. 지금까지 살펴 본 다른 형식 특질 메타 함수들과는 달리, 이들은 부울 값이 아니라 형식을 계산 결과로 낸다. 이들을 "형식 산술" 의 연산자들이라고 생각하면 될 것이다.


- C++ Template Metaprogramming, 2.7 메타함수의 정의, 48 페이지.


형식 산술을 위한 메타 함수의 리스트는 다음과 같다.



위의 형식 산술 연산자들은 modifier 나 qualifier 를 붙였다가 떼는 역할을 수행하고 있다. 일단 modifier 와 qualifier 의 개념에 대해서 알고 있어야 어떤 것이 가능하고 어떤 것이 불가능한지 판단할 수 있을 것이다. 다음 링크를 참조하라 : [ 번역 ]C++ Modifier Types.


위의 boost 메타 함수들은 modifier 와 관련한 형식 산술 연산자를 제공하고 있지 않다는 것을 알 수 있다.


1. add_unsigned< T >


unsigned modifier 를 추가한다.


용례 : 



이것을 직접 구현하려고 했을 때 일반적인 template 구현으로는 구현이 불가했다. 다음과 같은 template 은 에러를 발생시킨다.



const  같은 qualifier 는 "const T" 와 같은 부분 특수화가 가능했지만, 일반적인 modifier 는 그것이 불가능했다. 아마도 modifier 들은 정수형의 기본형에 대해서만 성립하는 것이기 때문에 그런 것이 아닌가 싶다. 예를 들어 unsigned std::vector 같은 표현은 성립하지 않는다. 혹은 [ 번역 ]C++ Modifier Types 에서도 언급하고 있듯이 unsigned 자체가 단축표기로 하나의 형을 나타내기 때문이 아닌가 싶다. 이 경우에는 add_unsigned< unsigned int + T > 의 형태가 되는게 아닌가 싶다.


어쨌든 이를 구현하기 위해서는 다음과 같이 모든 기본형에 대해서 부분 특수화를 하는 수밖에 없다. 아래는 int 에 대해서 부분 특수화를 한 예이다.



2. remove_unsigned< T >


unsigned modifier 를 제거한다.


용례 :



3. add_signed< T >


signed modifier 를 추가한다.


4. remove_signed< T >


singed modifier 를 제거한다.


5. add_long< T >


long modifier 를 추가한다.


6. remove_long< T >


long modifier 를 제거한다.


7. add_short< T >


short modifier 를 추가한다.


8. remove_short< T >


short modifier 를 제거한다.

















주의 : 공부하면서 정리한 것이기 때문에 오류가 있을 수 있습니다.


실습 과제 4-5 :


다음 함수 템플릿은 std::find 에 대한 "컨테이너 기반" ( 반복자 기반이 아니라 ) 인터페이스를 제공하기 위한 것이다.



현재 코드에서, container_find 는 const 컨테이너들에 대해서는 동작하지 않는다. 코드는 std::find 가 돌려 주는 const_iterator 를 Container::iterator 로 변환하려 하는데, 임의의 컨테이너 형식 x 에 대해 Container 는 const x 가 되기 때문에 컴파일이 실패한다. 이 문제를, container_find 의 반환 형식을 계산하는 작은 메타프로그램을 작성하여 해결하라.




풀이 :


이 코드를 보고 "Container& 인데 왠 const 컨테이너가 들어 온다는 거지?" 라는 의문을 가질 수 있다. 하지만 저건 그냥 type 이 아니라 template 매개 변수라는 것에 주목해야 한다. Container 자체에 예를 들면 const vector< int > 로 선언된 것이 들어 올 수 있다.


일단 에러가 나는 상황을 재현해 보자. 



이 문제를 해결하는 것은 간단하다. 다음과 같은 메타 함수를 하나 만들어서 반환값이 들어갈 부분에 넣어 주면 된다. 의미를 이해하는 것은 크게 어렵지 않을 것이다.



여기에서 특이한 것은 mpl::identity 를 사용한다는 것이다. mpl::eval_if 는 말 그대로 메타 함수를 평가하는 것이기 때문에 mpl::identity 를 사용하지 않으면 그냥 return 값이 make_iterator< Container > 가 되어 버린다. make_iterator 를 메타 함수로서 실행해 주기 위해서는 mpl::identity 를 상속한 형태가 되어야 하며, 최종적으로는 make_iterator< Container >::type 을 실행함으로써  Container::const_iterator 나 Container::iterator 가 반환된다. make_iterator 가 결국은 mpl::identity 를 상속하게 되기 때문에 mpl::identity::type 을 실행한 것과 동일한 효과를 내는 것이다.


이해를 돕기 위해서 mpl::eval_if 가 실행되고 나서의 결과를 단순하게 표현한다면 다음과 같다.



전체 코드는 다음과 같다.





다른 사람의 구현도 살펴 봤는데 필자와 다르지 않았다.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_4-5


주의 : 공부하면서 정리한 것이기 때문에 오류가 있을 수 있습니다.


실습 과제 4-4 :


정수 연산자들과 형식 특질 라이브러리의 수단을 이용해서 다음의 합성 특질들을 구현하라.


is_data_member_pointer

is_pointer_to_function

is_reference_to_function_pointer

is_reference_to_non_const




풀이 :


이 문제는 논리 연산자들을 적절하게 사용해 메타 함수를 합성하는 방법을 알고 있는지 확인하는 문제이다.


일단 함수를 합성하기 위해서는 문제에 나온 함수를 각각의 ( 함수 ) 요소로 분해할 수 있어야 한다.


1. is_data_member_pointer


이 메타 함수는 다음과 같이 분해된다.


is_data_member_pointer = is_member_pointer && !( is_member_function_pointer )



그리고 나서 이 함수를 테스트하는데 사용할 class 를 정의해 보자.



그런데 여기에서 문제가 발생한다. 우리가 만든 메타 함수는 매개변수로 type 을 받아 들이므로 우리가 테스트하고 싶어하는 field 의 형식에 어떻게 접근해야 할지 알 수 없다는 것이다. 예를 들어 다음과 같이 테스트하는 것은 오류이다.



그래서 C++ 0x 부터 추가된 decltype 을 사용해 봤다. 그런데 다음과 같은 결과가 나온다.



결국 어떻게 테스트해야 하는지 알수 없어서 CPPTM Answers - Exercise 4-4 를 참조했다. 여기에서는 빈 클래스인 foo 를 선언하고, typedef 를 통해서 새로운 type 을 선언했다. 일단은 그렇게 테스트를 해 봤다.



나중에 member variable 에서 class 까지 포함하는 type 을 획득할 수 있는 방법에 대해서 알아 봐야 겠다.


2. is_pointer_to_function


이 문제는 함수 포인터의 본질에 대해서 얼마나 이해하고 있느냐를 확인하고 있다. 처음에는 다음과 같이 생각했다.


is_pointer_to_function = is_function || is_member_function_pointer


그런데 "왜 is_function 은 is_function_pointer 가 아닌 것일까?" 라는 의문이 들기 시작했고, 다른 사람의 구현을 살펴 보았다. 


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_4-4



사실 함수포인터는 그냥 함수라고 생각했었는데, 지금까지 아주 잘못된 생각을 하고 있다는 것을 알았다. 일반적인 변수와 유사하게 함수 자체도 변수처럼 동작하는 것이었다. 그래서 여기에서 함수 포인터에 대해 다시 짚어 볼 필요가 있었다. 다음은 함수 포인터의 예이다. 다음 링크에서 가지고 온 예이다 : Program as Data: Function Pointers



위의 예에서 마치 변수의 주소를 구하듯이 함수의 주소를 구하는 것을 볼 수 있다. 변수는 어떤 메모리 공간에 대한 이름이다. 그리고 그 이름은 어떠한 주소로 매핑된다. 위에서도 언급했듯이 변수와 마찬가지로 함수는 어떤 로직을 담고 있는 메모리 공간에 대한 이름이 되겠다. 그러므로 이것도 참조하거나 포인팅하는 것이 가능한 것이다. 특이한 점은 그 포인터 자체에 () 연산자를 걸어서 호출이 가능하다는 것이다. () 연산자에 대해서는 다음 글을 참조하기 바란다 : [ 번역 ] Function Call Operator ().


그런데 위의 구현은 한 가지 문제가 있다. mpl::and_ 의 두 번째 매개 변수에서 ::type 을 지정함으로써 지연 평가가 이루어지지 않는다는 것이다. 이를 지연 평가가 가능하게 고쳐 보려고 했는데 잘 되지 않았다. 이 부분도 연구해 볼 필요가 있다.


어쨌든 is_pointer_to_function 은 다음과 같은 표현이라고 할 수 있다.


is_pointer_to_function = is_pointer && ( is_function( remove_pointer ) )


그리고 위의 구현은 불필요하게 and_ 를 호출하고 있기 때문에 최종적으로는 다음과 같이 구현할 수 있다.



3. is_reference_to_function_pointer


점점 복잡해 지고 있다. 하지만 function 에 대해서 이해한 상황이기 때문에 그리 어려울 것은 없다. 앞에서 만들었던 is_pointer_to_function 을 재사용하고, boost::remove_reference 로 reference 를 제거해 주기만 하면 된다.


is_reference_to_function_pointer = is_reference && ( is_pointer_to_function( remove_reference ) )


코드는 다음과 같다.



4. is_reference_to_non_const


이 문제는 비교적 쉬운데 다음과 같이 표현할 수 있다.


is_reference_to_non_const = is_reference && !( is_const( remove_reference ) )


코드는 다음과 같다.



여기에서 의문을 가지는 사람이 있을 것이다. 왜 is_const 에서 remove_reference 를 해야 하는 것일까? 필자가 다음과 같이 test 했을 때 error 를 냈다.



아직까지 그 이유는 정확하게 모르겠지만 추측해 보면, boost 의 type traits 는 특정 순서대로 한정자를 제거해 가면서 type check 를 하도록 만들어져 있는 것 같다.


주의 : 공부하면서 정리한 것이기 때문에 오류가 있을 수 있습니다.


실습 과제 4-3 :


다음 코드에서 불필요한 인스턴스화를 제거하라.



수정된 메타함수들의 의미론이 수정되기 전과 동일한지를 점검하는 테스트들도 작성하라.




풀이 :


위의 문제들은 게으른 평가에 대한 이해도를 확인하는 문제이다. ::type 이나 ::value 가 불리는 순간 해당 템플릿은 인스턴스화된다. 그러므로 게으른 평가를 위해서는 메타함수들이 실제로 평가되지 않도록 ::type 이나 ::value 를 없애야만 한다. 물론 반드시 필요한데 호출하는 코드를 지워버린다면 에러가 나므로 주의해야 한다.


1. next_if 메타 함수.


일단 next_if 의 의미를 파악하자.


N 을 Predicate 함수에 적용한 결과가 true 이면

    mpl::next< N > 을 호출한 결과를 반환하고

그렇지 않으면

   N 을 그대로 반환한다.


mpl::next 는 다음과 같은 기능을 가지고 있다.


순차열 안의 다음 반복자( iterator )를 반환한다. [ 주의 : next 는 여러 개의 중복된 의미를 가지는데, 그것의 인자의 형식에 따라 다르다. 예를 들어 X 가 정수형 상수라면, next< X > 는 같은 형식의 증가된 정수형 상수를 반환한다. 이후의 명세는 iterator-specific 하다. 대안으로 선택할 수 있는 구문에 대한 세부적인 설명을 원한다면 관련 개념들의 문서를 참조하기 바란다. ]

http://www.boost.org/doc/libs/1_52_0/libs/mpl/doc/refmanual/next.html


'predicate' 는 '술어' 라는 뜻이기 때문에 mpl::if_ 의 조건을 결정하기 위한 서술문을 의미하는 메타 함수를 의미한다고 보면 될 것이다. 예를 들어 C++ 식으로 표현하면 다음과 같다; if( true == Predicate( N ) ){ ... }.


이를 테스트하기 위해서는 Predicate 를 평가해서 나온 ::type 이 가리키는 형식이 반드시 ::value 라는 멤버를 가지고 있어야만 한다. 그래서 다음과 같은 식으로 평가를 해 볼 수 있다. Predicate 의 형태를 보요 주기 위해서 메타 함수 클래스를 사용한 경우와 자리표 표현식을 사용한 경우의 두 가지 예제를 모두 보여 주겠다.



자 이제 이것을 게으른 평가가되도록 최적화해 보자.


현재 mpl::apply 메타 함수의 ::type 과 mpl::next 메타 함수의 ::type 이 불리고 있다. 책에서 배운 대로 mpl::identity 를 사용해서 제거해 보도록 하자.



안타깝게도 우리의 기대를 무참하게 짓밟아 버린다. 왜일까?


mpl::identity::type 은 그것의 매개변수로 들어 왔던 스칼라나 메타함수의 원형을 그대로 돌려 준다. 그러므로 아직까지는 mpl::identity< mpl::next< N > > 과 mpl::next< N > 이 평가되지 않은 상황이다. 이를 해결하기 위해서는 next_if2 : mpl::if_< ... >::type 으로 만들어 주거나 사용할 때 next_if2< ... >::type::type::value 를 통해 값을 가져와야 한다. 사용자들은 보통 ::type 을 한 번만 사용한다고 인지하고 있기 때문에 필자는 구조체 쪽에서 처리하는 방법을 택했다. 최종 코드는 아래와 같다.



사실 mpl::eval_if 를 사용하면 mpl::identity 를 사용할 필요가 없다. 물론 다들 알고 있겠지만 책에서 배운 내용을 점검해 보는 차원에서 위의 방식으로 풀어 보았다. 실제로 아래와 같이 매우 깔끔한 코드를 만들 수 있다.



2. formula 메타 함수.


mpl::not_equal_to 는 integral constant 에 대해서 적용되므로 N1 과 N2 가 integral constant 라는 것을 알 수 있다. 그러므로 formula 메타 함수는 다음과 같은 기능을 가지고 있다.


IF N1 과 N2 가 다르면

IF N1 이 N2 보다 크면

N1 에서 N2 를 뺀 결과 반환.

ELSE

N1 을 반환.

ELSE

N1 에 N1 에다 2를 더한 결과를 더한 결과 반환.


조금 복잡하기는 하지만 1 번과 크게 다를 것 없다. 일단 mpl::if_ 를 mpl::eval_if 로 바꾼다. 그리고 나서 mpl::eval_if 내의 메타 함수들에서 불필요한 ::type 을 제거한다.



주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 4-2 :


실습 과제 4-1 의 logical_or 와 logical_and 메타 함수들의 구현을, 인수들을 최대 5 개까지 받을 수 있도록 확장하라.




풀이 :


이 문제를 해결하기 위해서는 가변 개수의 매개변수들을 받을 수 있는 구조를 만들어야 한다. 


하지만 안타깝게도 템플릿에 가변 개수의 매개변수를 받을 수 있는 방법은 없다. 일단은 5 개의 매개변수를 받을 수 있는 logical_or 와 logical_and 를 만드는 데서부터 시작하자.


 

그런데 ( 2 개 이상인 ) 가변 개수의 매개변수를 받기 위해서는 어떻게 해야 할까? 이를 위해서는 템플릿 매개변수에 initializer 를 설정하는 방법이 있다. 사용자가 매개변수를 입력하지 않더라도 자동으로 기본값이 들어 가도록 하는 방법이다. none_ 이라는 구조체를 기본값으로 설정하고, 만약 그 구조체가 입력되면 연산을 종료한다고 가정했다.



이제 logical_and_impl 구현해야 하는데, 이것을  인자가 5 개인 것부터 2 개인 것까지 구현을 한다. 그리고 logical_and_impl 가 다음 매개변수들을 위해 실행되어야 할지를 판단하기 위해 mpl::eval_if 를 사용한다. 다음 매개변수들을 위해 실행되지 않는 경우는 두 가지이다; 현재 조건이 false 일 경우와 매개변수가 none_ 일 경우이다.


매개변수가 none_ 일 경우를 위해서 부분특수화를 구현했다. 그리고 mpl::eval_if 는 게으른 평가를 수행하므로 4-1 의 문제가 제시한 조건을 만족한다.


다음은 소스 코드이다. 그런데 logical_or 는 귀차니즘 때문에 구현하지 않았다. 비슷한 방식으로 구현하면 된다.



다른 사람의 구현도 살펴 보자.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_4-2


주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 4-1 :


mpl::or_ 와 mpl::and_ 를 본 딴, logical_or 와 logical_and 라는 이름의 이항 메타함수들을 구현하라. 그리고 실습 과제 4-0 의 테스트들로 그 구현을 시험하라.




풀이 :


이 문제는 게으른 평가( lazy evaluation )에 대한 이해를 얼마나 하고 있는지를 확인하는 문제이다. 게으른 평가가 되기 위해서는 ::type 이나 ::value 가 명시적으로 지정되면 안 된다.


필자는 부분 특수화를 통해서 이 문제를 해결했다. 자세한 사항은 주석을 참조하라.



아래 링크에 있는 다른 사람의 구현도 살펴 보기 바란다.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_4-1


주의 : 공부하면서 정리하는 것이기 때문에 오류가 있을 수 있습니다.


실습 과제 4-0 :


mpl::or_ 와 mpl::and_ 메타 함수에 대해, 부울 표현식의 평가 단축 행동을 시험하는 테스트를 작성하라.




풀이 :


이 문제는 "평가 단축 행동"의 의미를 이해하고 있는지를 묻고 있다. 책에서는 다음과 같이 표현하고 있다.


내장 연산자 &&, || 과 마찬가지로, mpl::and_ 와 mpl::or_ 는 "평가 단축" 행동을 가진다는 점 역시 알아둘 필요가 있을 것이다.

- C++ Template Metaprogramming, 4.2 정수 래퍼들과 연산들, 91 페이지.


"평가 단축" 이라고 하는 것은 앞의 결과가 false 면 뒤의 결과를 평가하지 않는다는 것이다. 예를 들어 "A && B" 는 둘 다 true 일 때만 true 이므로 && 연산자의 앞의 피연산자인 A 가 false 이면 B 는 볼 필요도 없이 && 의 결과는 false 이다. 이를 통해 평가 비용을 단축할 수 있다.


이를 테스트해 보기 위해서는 bool 을 반환하는 두 개의 메타 함수가 필요하다. 책에서 많이 나오고 있는 boost::is_scalar 를 첫 번째 메타 함수로 만들고, 항상 컴파일 에러를 내는 Error 라는 메타 함수를 만들자. 그리고 나서 mpl::or_ 와 mpl::and_ 의 두 번째 매개변수로 넣는다.



주의 : 공부하면서 정리한 내용이기 때문에 오류가 있을 수 있습니다.


실습 과제 3-7* :


다음 구성물들의 의미론은 무엇일까?



독자의 답에 도달한 단계들을 보이고, 독자의 가정들을 확인하는 테스트 코드를 작성하라. 라이브러리의 행동이 독자의 분석과 일치하는가? 그렇지 않다면, 실패한 테스트들을 분석해서 실제의 표현식들의 의미론을 파악해 볼 것. 그리고 독자의 가정들이 왜 달랐는지, 또 독자가 생각하기에 어떤 행동이 더 합당한지와 그 이유를 말해 보라.




풀이 :


이 역시 쉬운 듯하면서 절대 쉽지 않은 문제이다. 지금까지 배웠던 람다 표현식에 대해서 얼마나 이해하고 있는지를 검사하는 문제이다. 게다가 근거까지 명확하게 대야 하기 때문에 별표가 안 붙을 수 없다.




typedef mpl::lambda< mpl::lambda< _1 > >::type t1;


mpl::lambda 메타 함수는 자리표 표현식을 위한 메타 함수 클래스를 만들어 내는 역할을 하고 있다고 알고 있었다. 그리고 결국에는 메타 함수 자체를 반환한다고 생각했다. 


_1 은 arg< 1 > 이라는 메타 함수 클래스이고 _1 로 평가되기 때문에 첫 번째 lambda 에서 _1 을 반환하고, 두 번째 lambda 에서 다시 _1 을 반환해 t1 은 _1 을 반환한다고 생각했다. 그래서 예상한 결과는 다음과 같았다.



그러나 이는 전혀 평가되지 않는 문장을 만들어 냈다. 아래 에러와 같이 그냥 문자열처럼 해석해 버린다.


error C2338: ( boost::is_same< t1::type, _1 >::value )


생각해 보니 t1 이라는 것 자체가 어떤 메타 함수 클래스를 나타내기는 하지만, 실제로 어떤 형식을 넣어 특수화하지 않으면 아무런 의미도 없겠다는 생각이 들었다. 그래서 다음과 같이 고쳐 봤다. 자리표 표현식으로부터 메타 함수 클래스를 생성해 주는 것이기 때문에 apply 를 호출해 보았다.



위의 코드 역시 평가되지 않았다.


결국 기본부터 다시 생각해 보기로 했다. 


mpl::lambda 의 의미부터 다시 파악해 볼 필요가 있다. mpl::lambda 는 자리표 표현식이나 메타 함수 클래스를 받아서 메타 함수 클래스를 만들어 준다. 만약 boost::add_pointer 라는 메타 함수를 위한 메타 함수 클래스를 만들기 위해서는 다음과 같이 해야 한다.



자 그러면 아무런 메타 함수나 넣어서 호출할 수 있게 해주는 메타 함수를 만들기 위해서는 어떻게 해야 할까? 다음과 같은 형태를 생각할 수 있다.



??? 자리에는 람다 표현식( 자리표 표현식이나 메타 함수 클래스 )이 들어 와야 할 것이다. 메타 함수 자체는 일급 메타 자료가 아니기 때문이다. 그런데 공급될 것이 메타 함수인지 람다 표현식인지 알 수 없기 때문에, 모든 가능성을 생각하면 무엇이 들어 오든 람다 표현식으로 만들어 줄 필요가 있다. 그러므로 다음과 같이 표현할 수 있다.



위 코드는 메타 함수나 람다 표현식을 메타 함수 클래스로 만들어 주는 메타 함수 클래스를 정의한 것이라고 할 수 있다. 이를 실제로 사용해 보도록 하겠다.





typedef mpl::apply< _1, mpl::plus< _1, _2 > >::type t2;


이것은 mpl::apply 에 의해 내부의 mpl::plus 를 평가한 결과를 외부의 mpl::apply 에 적용하는 메타 함수로 보인다. 일반적으로 mpl::plus 에 들어 오는 것은 수치 래퍼일 것이기 때문에 그것의 ::type 은 자기 자신을 반환할 것이다. 결국 이는 mpl::plus 와 동일한 결과를 낼 것이다.



one 과 two 를 mpl::plus 의 _1 과 _2 에 할당하고 나면 mpl::int_< 3 > 이라는 결과가 나올 것이고, 그것이 다시 mpl::apply 의 _1 에 들어 가서 그것을 평가하면 그 자신을 반환한다.




typedef mpl::apply< _1, std::vector< int > >::type t3;


일단 std::vector 가 메타 함수인지를 확인해 봐야 한다. 그런데 std::vector 는 type 이라는 내포된 형식을 가지고 있지 않았다. 그걸 봤을 때 메타 함수는 아니라고 생각했으며, 이를 인스턴스화하면 에러가 날 것이라 생각했다.


하지만 실제로 해 봤을 때 다음과 같은 코드가 유효했다.



그런데 책에 이미 내용이 나와 있었다. 


자리표에 대해 아직 이야기하지 않은 세부사항이 하나 있다. MPL 은 보통의 템플릿들을 메타 프로그램에 통합시키기 쉽게 만들기 위한 하나의 특별한 규칙을 사용한다. 어떤 규칙이냐 하면, 모든 자리표들이 실제 인수들로 치환된 후의 템플릿 특수화 X 에 내포된 ::type 이 존재하지 않는다면, 그 람다의 결과는 X 자체라는 것이다.

- C++ Template MetaProgramming, 3.5.3 람다와 비 메타함수 템플릿, 77 페이지.


역시 책만 읽고 "아 그렇구나" 하고 넘어가면 안 된다는 점을 다시 한 번 느끼게 됐다.




typedef mpl::apply< _1, std::vector< _1 > >::type t4;


이것은 어떤 형식을 받아 vector 를 만들어 주는 메타 함수이다.





typedef mpl::apply< mpl::lambda< _1 >, std::vector< int > >::type t5;


std::vector< int > 을 입력으로 mpl::lambda< _1 > 가 지정하는 메타함수를 실행해 주는 메타함수라고 생각했다. 하지만 실제 해 보니 t5 에는 apply 라는 메타 함수가 존재하지 않고 그냥 std::vector< int > 와 같았다.



좀 이해할 수 없는 결과였다. 그런데 가만히 생각해 보니, 내부에 있는 mpl::lambda< _1 > 이라는 것이 평가되면 그냥 _1 이 되기 때문에 결국에는 t3 와 같은 결과가 되는 것이었다. 




typedef mpl::apply< mpl::lambda< _1 >, std::vector< _1 > >::type t6;


이것 역시 내부의 mpl::lambda< _1 > 이 _1 로 평가되면서 t4 와 같은 결과를 낸다.





typedef mpl::apply< mpl::lambda< _1 >, mpl::plus< _1, _2 > >::type t7;


이것 역시 내부의 mpl::lambda< _1 > 이 _1 로 평가되면서 t2 와 같은 결과를 낸다.





typedef mpl::apply< _1, mpl::lambda< mpl::plus< _1, _2 > > >::type t8;


mpl::lambda< mpl::plus< _1, _2 > > 는 mpl::plus< _1, _2 > 로 평가되고, 이것은 mpl::apply 의 _1 에 그대로 들어 가기 때문에 최종적으로는 mpl::plus< _1, _2 > 만 남는다.





다른 사람의 해석도 살펴 보기 바란다.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_3-7

주의 : 공부하면서 정리한 것이므로 오류가 있을 수 있습니다.


실습 과제 3-6 : 


twice 와 같은 기능을 가진 람다 표현식을 작성하라. 힌트 : mpl::apply 는 메타함수임.




풀이 :


mpl::apply 메타 함수는 메타함수를 템플릿 매개 변수로 받아 호출해 주는 예제이다. 예를 들어 boost::add_pointer 는 그냥 메타 함수이기 때문에( 일급 메타 자료가 아니기 때문에 ) 다른 메타 함수의 매개변수로 설정될 수 없다. 이를 가능하게 하기 위해서는 람다 표현식을 만들어야 한다.


람다 표현식은 다음과 같다.


람다 표현식( lambda expression ). 간단히 말하면, 람다 표현식은 호출 가능한 메타자료이다. 어떤 형태이든 호출 가능한 메타자료라는 것이 없으면 고차 메타함수는 불가능하다. 람다 표현식은 두 가지 형태인데, 하나는 메타함수 클래스이고 또 하나는 자리표 표현식이다.

- C++ Template MetaProgramming, 3.6 세부사항, 79 페이지.


책에서 메타함수 클래스를 만드는 것과 메타함수 전달을 하는 법에 대해서 설명을 많이 하고 있다. 여기에서 굳이 이 문제를 내는 이유는 비관입적으로 메타함수들을 호출하는 법을 제대로 알고 있는지에 대해서 확인하는게 아닐까 싶다.


필자같은 경우에는 자리표 표현식을 사용해 이를 람다표현식으로 표현해 보았다.


자리표 표현식( placeholder expression ). 람다 표현식의 한 종류로, 자리표들을 이용해서 즉석의 부분적 메타함수 적용과 메타함수 합성을 가능하게 한다. 이 책 전반에서 나오겠지만, 이러한 기능은 필요한 바로 그 자리에서 기초적인 메타함수들로 거의 모든 종류의 좀 더 복잡한 형식 계산을 구성할 수 있게 한다.

- C++ Template MetaProgramming, 3.6 세부사항, 79 페이지.



다른 사람들은 어떻게 구현하고 있는지 살펴 보기 바란다.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_3-6


주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 3-5 :


3.1 절의 차원 해석 코드에는 한 가지 문제가 남아 있다. 힌트 : 다음 코드를 시험해 볼 것.



이 문제를 이번 장에 나온 기법들을 이용해서 고쳐 보라.




풀이 :


m * a 가 만들어 내는 차원 정보는 순차열 내의 값은 동일하지만 같은 형식이라고 할 수 없다. 그러므로  + 연산자와 - 연산자에서 복사 생성자를 만들때 했던 것처럼 형식 검사를 해 줘야 한다.


하는 김에 * 연산자와 / 연산자에서 이 장에 나온 자리표 표현식을 사용해 보았다.



주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


"C++ Template MetaProgramming 실습 과제 3-4 풀이" 와 "[ 번역 ] CPPTM Answers - Exercise 3-4" 에도 불구하고 아직도 이해가 안 가는 사람들이 많을 것이라 생각한다. 번역 글에서 논쟁하는 것을 살펴 보고 있어도 뭔가 머리에 쏙 들어 오는 것이 없을 것이다. 그냥 이해가 가는 사람이 있다면 존경하고 싶다.


필자도 그래서 책을 몇 번이나 다시 보고 여러 가지 코드를 테스트해 보았다.


일단 다음과 같은 코드를 컴파일해 보자.



뭔가 오류가 주저리 주저리 나오는 데 우리가 주목할 것은 마지막 "with[ ... ]" 에 나와 있는 내용이다.


1>d:\mywork\templatemetaprogramming\consoleapplication\consoleapplication.cpp(24): error C2039: 'type' : 'boost::mpl::apply<F,T1>'의 멤버가 아닙니다.

1>          with

1>          [

1>              F=twice<int *,int> *,

1>              T1=twice<int *,int>

1>          ]


위의 오류는 처음 apply 가 평가될 때, 즉 내부 twice 가 평가될 때의 오류를 의미하고 있다. twice 메타 함수의 다음 코드 부분이다.



원래는 mpl::apply 가 적용되고 나면 boost::add_pointer< T > 가 실행되면서 그것의 내포된 type 에 접근해야 하는데, 그것이 실패하면서 "boost::mpl::apply< F, T1 > 의 멤버가 아닙니다" 라는 오류를 뱉어 낸다. 왜 그럴까?


"F = twice< int*, int >*" 가 들어 오고 "T1 = twice< int*, int >" 가 들어 왔다. 아무래도 이것만 봐서는 좀 이해가 안 된다.


그래서 자리표 표현식을 다음과 같이 바꿔 호출해 보았다.



다음과 같은 오류를 낸다.


1>d:\mywork\templatemetaprogramming\consoleapplication\consoleapplication.cpp(24): error C2039: 'type' : 'boost::mpl::apply<F,T1>'의 멤버가 아닙니다.

1>          with

1>          [

1>              F=twice<int *,int> *,

1>              T1=int

1>          ]


"T1 = int" 로 평가되고 "F = twice< int*, int >*" 라고 평가된 것을 볼 수 있다. 즉 내부 twice 의 boost::add_pointer 가  메타 함수 클래스로서 평가되는 것이 아니라, 그냥 메타 함수로 평가되어 버렸다는 것을 알 수 있다.


정리하자면 "F = twice< add_pointer_f, int >*" 식으로 들어 와야 하는 것이, boost::add_pointer< int >::type 으로 평가되어 버렸기 때문에 "F = twice< int*, int >*" 가 되어 버린 것이다( boost::add_pointer< int >::type 은 int* 이기 때문이다 ).


그래서 우리가 mpl::lambda 메타 함수를 사용해 이 자리표 표현식을 메타 함수 클래스로 만들어 주지 않으면 컴파일 에러가 나는 것이다.


원문 : http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_3-4

정보 : C++ Template MetaProgramming 실습 과제 3-4 의 풀이를 잘 이해할 수 있는 보충 자료라 생각해 번역해 봅니다. 논쟁이 참 재밌고 도움이 됩니다.

주의 : 번역이 개판이므로 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.

주의 : 가독성을 높이기 위해 잘 알려지거나 우리말과 발음이 비슷한 용어는 한글로 표기합니다.


CPPTM Answers - Exercise 3-4







명시적인 lambda 호출이 왜 필요한지 설명해 줄 수 있는 사람이 있나요? 나는 twice 안에 내포된 apply 가 add_pointer 자리표 표현식을 다룬다고 생각했는데, GCC 3.3.3 은 다음과 같이 하면 컴파일 에러를 내네요 :



아는 사람? -- Matt Brecknell




3.3 절은 왜 그것이 동작하지 않는지에 대한 좋은 설명을 제공합니다 - Ariel




Matt : 3.3 절은 책의 이전 절에서의 twice 메타 함수가 자리표 표현식을 직접적으로 사용하지 않는 이유에 대해서 설명합니다. 그러나 그리고 나서 3.3.2 절은 mpl::apply 에 대한 첫 번째 인자는 모든 lambda 표현식이 될 수 있다고 이야기합니다( 자리표로 만들어진 것들을 포함해서 말이죠 ). 당신의 twice 정의는 mpl::apply  를 사용해서 인자로 넘어 오는 메타 함수를 호출하기 때문에, 나는 mpl::lambda 를 사용하지 않고도 자리표 표현식을 사용할 수 있다고 기대했습니다. 예를 들어 나는 다음 코드가 당신의 twice 정의를 사용해 동작할 것이라고 믿습니다( 비록 나는 그 시점에 MPL 이 가능한 컴파일러를 사용하고 있지 않아서 검사해 볼 수 없었지만 말이죠 ) :



I'm currently working on the theory that the problem with my version of the nested invocation of twice might be something to do with the way the outer twice is evaluating placeholders in the inner twice, but I haven't figured it out yet. -Matt




당신의 이전 코드 예제에서, 먼저 안쪽의 twice 를 생각해 보세요. 그리고 두 번째 인자를 생각해 보세요. 그것은 단지 _1 입니다. 그것은 안쪽 twice 에서는 특별한 의미가 없습니다. 그러므로 안쪽 twice 의 결과는 _1**  입니다. 안쪽 twice 는 메타 함수이지, lambda 표현식이 아닙니다. 그러므로 당신은 mpl::apply 를 적용할 수 없습니다. 만약 mpl::lambda 를 boost::add_pointer 에 사용하게 된다면, 안쪽 twice 는 그것을 lambda 표현식을 생성하는데 사용할 것이고, 그 표현식은 mpl::apply 로 넘겨질 수 있습니다. 그리고 결국 바깥쪽 twice 에 넘어 갈 수 있습니다. 적어도 내가 생각하기에는 그것이 작동 방식입니다( 나는 Dave 가 이를 명확하게 설명할 수 있는지 궁금하군요 ) - Ariel




만약 내가 직접적으로 twice<> 를 ::type 과 함께 평가했다면, 나도 안쪽  twice<> 가 _1** 로 평가될 것이라는 데 동의합니다. 그렇지만 나는 그렇게 하지 않았기 때문에, 나는 바깥쪽 twice<> 가 안쪽 twice<> 를 자리표 표현식으로 취급할 것이라고 생각했습니다( 그리고 당신이 제시했듯이 그냥 메타 함수로 취급하지 않을 것이라 생각했습니다 ). 그러므로 나는 안쪽 twice<> 안의 mpl::apply 가 boost::add_pointer< _1 > 을 메타 함수 클래스로 변환하고, 반면에 두 번째 인자( bare _1 )를 바깥쪽 twice<> 에 대해 대체( substitute )하기 위해 남겨둘 것이라 기대했습니다.그것이 내가 _1 을 두 자리에 모두 사용한 이유입니다. 그것들이 서로 다른 문맥에서 서로 다른 것들을 가리키고 있음에도 불구하고 말이죠.  In any case, I don't see why your reasoning would apply to my formulation, without also applying to yours. 무엇보다도 당신은 당신의 버전에서 "_" 자리표를 같은 위치에 가지고 있습니다. 단 하나의 주요한 차이는 당신이 boost::add_pointer< _ > 를 mpl::lambda<> 안에 래핑했다는 것 뿐입니다. 나는 mpl::lambda 가 안쪽 twice<> 로 하여금 바깥쪽 twice<> 를 위한 "lambda 표현식을 생성"하도록 만든다는 것에 대한 당신의 근거에 동의할 수 없습니다 : 만약 안쪽 twice<> 가 자리표 표현식이라면, 그것은 이미 lambda 표현식입니다( 책 3.6 절 ). 명백히 내 근거가 다소 잘못되기는 했지만, 나는 여전히 당신이 제시한 근거들을 이해할 수 없습니다. - Matt




나는 안쪽 twice 가 자리표 표현식이라고 받아들였습니다. 그리고 나서 바깥쪽 twice 는 ( 각 mpl::apply 를 위한 ) 형식을 위해 두 자리표들을 대체합니다. mpl::lambda 를 사용하는 것은 boost::add_pointer 를 위한 자리표가 대체되는 것을 막습니다. 이것이 정확히 어떻게 동작하는 지는 잘 모르겠군요 - Ariel.




twice< twice< add_pointer< _1 >, _1 >, int > 가 작동하지 않는 이유는 다음과 같습니다. 명확한 영역 특수화를 하지 않으면 mpl::apply< twice< add_pointer< _1 >, _1 >, int >::type 은 다음과 같습니다.



이것은 매우 잘못 되었습니다. http://thread.gmane.org/gmane.comp.lib.boost.devel/115924 에서 진행중인 명시적 영역지정( scoping )을 사용하면, 제대로 작동하는 한 줄 표현은 다음과 같습니다.



명확히 이해하는데 도움이 되었으면 합니다. People/Aleksey Gurtovoy.


주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 3-4 :


twice 를 그 자신에 대해 사용해서 T 를 T**** 로 변환하라.




풀이 :


처음에 한 구현은 다음과 같다.



하지만 생각해 보니 위와 같이 구현하면 3-3 의 문제를 푸는 것과 다를 것이 없었다. 여기에서 의도하는 것은 twice 라는 메타 함수를 자신의 메타 함수로 사용해 f( f( x ) ) 와 같은 식으로 사용해서 호출하는 방법을 알고 있는 지를 물어 보는 것이라는 생각이 들었다.


즉 다음과 같은 형태여야 한다.



일단 메타 함수를 파라미터로 넘기기 위해서는 lambda 나 placeholder 를 사용해서 이를 일급 메타 함수로 만들어 줄 필요가 있다는 것까지는 쉽게 생각할 수 있었다. 하지만 이 부분부터가 어렵다. 즉 고차 메타 함수에 대한 이해를 제대로 하고 있는지를 확인하는 문제라 생각한다. 만약 제대로 이해하고 있지 못하다면 책을 다시 한 번 살펴 볼 필요가 있다. 필자도 다시 봤다 ㅠㅠ. 특히 "3.3 자리표 다루기" 를 유심히 보기 바란다.


일단 답부터 말하자면 다음과 같이 호출해야 한다.



위의 코드를 좀 더 이해하기 쉽게 분해하면 다음과 같다.



3.3 절에서도 이야기하고 있듯이 boost::add_pointer 는 메타 함수 클래스가 아니다. 메타 함수 클래스는 다음과 같은 정의를 가지고 있기 때문이다.


메타함수 클래스는 공개적으로 접근할 수 있는 apply 라는 이름의 내포된 메타함수를 가진 클래스이다.

- C++ Template Metaprogramming, 3.1.4, 64 페이지.


즉 apply 를 평가하려는 시점에 boost::add_pointer< _1 >::apply 가 존재하지 않으므로 컴파일에 실패하게 된다. 


그러므로 우리는 mpl::lambda 를 사용해 add_pointer 를 메타 함수 클래스로 만들어 줘야만 한다. mpl::lambda 가 메타 함수 클래스를 만들어 주는 원리에 대해서는 mpl::lambda 의 원리에서 다루고 있으니 참조하기 바란다.


이 시점에서 의문을 품는 사람들이 있을 것이다. 필자도 그랬다. 왜 실습과제 3-3 은 메타 함수 클래스가 아닌 add_pointer 를 그냥 호출할 수 있는 것일까? 



그 이유에 대한 의문을 외쿡인들도 가졌던 것 같다. CPPTM Answers - Exercise 3-4 에 보면 거기에 대한 질문과 답변이 있으니 꼭 참조하시기 바란다.




추가 : 필자는 위의 링크를 보고 제대로 이해를 못해 나름대로 답을 찾으려고 노력했고 그 내용을 정리해 보았다. C++ Template MetaProgramming 실습 과제 3-4 풀이 보충을 참고하라.

주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 3-3 :


twice 를 두 번 사용해서 T 를 T**** 로 변환하라.




twice 는 어떠한 메타 함수와 형식을 받아 두 번 실행해 주는 고차 메타 함수이다. twice 에 boost::add_pointer 메타 함수를 넣어 두 번 실행하는 문제이다.



자리표 표현식을 사용해서 고차 메타 함수를 호출할 수 있는지를 확인하는 문제인 것 같다.

주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 3-2 :


transform 을 이용해서, vector_c< int, 1, 2, 3 > 를 요소 ( 1, 4, 9 ) 를 가진 형식 순차열로 변환하라.




풀이 :


이것은 boost::mpl::multiplies 메타 함수를 사용해 해결한다.



주의 : 공부하면서 정리한 것이기 때문에 오류가 있을 수 있습니다.


실습 과제 3-1 :


transform 을 이용해서, vector_c< int, 1, 2, 3 > 를 요소 ( 2, 3, 4 ) 를 가진 형식 순차열로 변환하라.




풀이 :


각 요소에 1 씩을 더하는 것이 이 문제의 핵심이다. 


3 장을 총정리하는 관점에서 boost 의 메타 함수를 사용하면 다음과 같이 쉽게 문제를 풀 수 있다.



하지만 이런 식으로 풀라고 문제를 낸건 아니지 않을까 하는 찝찝함이 남기는 한다. 


여기에서 주의할 것은 isSame 과 isSame2 를 구하기 위해서 boost::is_same 메타 함수를 사용해서는 안 된다는 것이다. vector_c 메타 함수가 불릴 때마다 새로운 type 이 생성되는 것이기 때문에 boost::is_same 을 사용하면 모조리 false 가 나온다. 요소가 같은지를 검사하기 위해서는 mpl::equal 메타 함수를 사용해야만 한다.


주의 : 공부하면서 정리한 것이므로 오류가 있을 수 있습니다.


실습 과제 3-0 :


1.4.1 절에 나온 binary 템플릿에, N 에 0 과 1 이외의 숫자가 들어 있을 때 binary< N >::value 가 컴파일 오류를 일으키도록 하는 오류 점검을 BOOST_STATIC_ASSERT 를 이용해서 추가하라.




풀이 :


일단 원래 구현을 살펴 보자.



다음과 같이 BOOST_STATIC_ASSERT 를 추가할 수 있다.



전체 코드는 다음과 같다.



너무 단순해서 맞는 건지 떨떠름하긴 하다.

주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


C++ Template Metaprogramming 책의 [ 3.2. 고차 메타함수들 ] 을 보면 다음과 같은 문장이 있다.


메타함수들을 "일급( first-class ) 메타자료" 로 만듦으로써, transform 으로 거의 무한하게 다양한 연산들( 앞의 예에서는 곱하기와 나누기 )을 수행할 수 있게 된다.


여기에서 first-class metadata 의 용어 설명이 제대로 나와 있지 않다. 그래서 googling 을 해 봤는데도 제대로 된 용어 설명을 찾을 수 없었다.


그런데 googling 을 하다가 보니 비슷한 용어 설명을 찾을 수 있었다.


What is 'first class object'?


first-class object 는 동적으로 생성, 파괴될 수 있거나 인자로 넘겨질 수 있는 것을 의미한다.


예를 들어, 모든 C++ 의 object 들도 first-class object 이다. 그것들은 생성자나 소멸자 메서드를 사용해서 생성되거나 파괴될 수 있으며, 함수의 인자로서 전달될 수 있다. 그러나 C++ 의 함수와 메서드는 first-class 가 아니다. 왜냐하면 그것들은 실시간에 생성될 수 없기 때문이다.


많은 함수적 언어( functional language )에서 함수는 first-class object 이다. 왜냐하면 그것들은 다른 함수의 인자로서 넘겨질 수 있으며 런타임에 동적으로 생성되기 때문이다.


C++ 에서 class 는 first-class 가 아니다 -- 일부 언어에서( 예를 들어 LISP 의 CLOS ) 그것들이 first-class 임에도 불구하고 말이다( 왜냐하면 그 언어들에서는 class 가 object 이다 ).


출처 : http://www.catalysoft.com/definitions/firstClassObject.html


위의 설명으로 보았을 때, meta function 이 meta function class 에 대한 직접적인 정의 없이 placeholder 기능을 통해서 meta function 의 parameter 로 취급될 수 있는 상황을 가리키는 것 같다. 즉 placeholder 는 meta function 을 meta function class 와 같은 first-class object 로 변환해 주는 역할을 하는 것이다.



원문 : http://www.drdobbs.com/cpp/extracting-function-parameter-and-return/240000586

주의 : 번역이 개판이므로 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.

주의 : 가독성을 높이기 위해 잘 알려진 용어 및 발음이 비슷한 용어는 한글 표기합니다.


C++ 에서 함수 매개 변수와 반환값 추출하기


C++ 의 메타프로그래밍( metaprogramming ) 기능과 generic 프로그래밍을 사용하면, 매개변수 형식에 대한 우아한 파서를 생성하는 것이 가능하다. 명령줄 해석기( command-line interpreter ), 파서( parser ), 구문 검사기( syntax checker )들이 그런 응용프로그램들의 일부이다.


메타프로그래밍은 점점 더 대중적이 되어 왔으며 이제는 현대 C++ 프로그래밍의 필수적인 부분이다. 형식 정보를 이용하면, generic 프로그래밍을 구현하고 컴파일 시점 최적화 및 검사를 수행하는 것이 가능해 진다. 이 기사에서 나는 템플릿 메타프로그래밍을 사용해 함수에서 형식 정보를 추출하고 이 기법을 사용하는 응용프로그램의 예를 보여 줄 것이다.


가능한 실용적인 응용프로그램들 중 하나를 설명하기 위해서, 나는 기본적인 명령어 해석기나 REPL 과 같은 기능을 수행하는 오픈 소스 응용프로그램을 생성했다. 새로운 명령은  함수 포인터를 제공함으로써 등록된다. 이 명령 클래스는 입력 문자열을 평가하고 입력 매개변수를 분석( parse )하고( 매개 변수의 개수와 형식을 검사하고 필요하면 변환함 ), 명령을 실행하고, 결과를 제공하기 위한 충분한 정보를 가지고 있다.


여기 새로운 명령을 등록하는 코드의 일부가 있다; 이 경우 새로운 함수를 등록한다 :



( 다른 예제들은 src\cmd_test.cpp 에 연결된 파일들로부터 찾아 볼 수 있다 ) 명령줄에 앞에서 언급한 코드를 넣은 후에, 당신이 다음 문자열을 실행하려고 시도하면 : add 5 4, 명령어 해석기는 자동으로 그것을 분석하고, 부가적인 작업을 수행하고, 결과를 출력한다. 유효하지 않은 매개변수들을 넣으면, 그것은 무엇이 잘 못 되었는지 알려 줄 것이다 :


input : "add not a number"

output : "add : bad arguments, usage RV : INT add INT INT;"


Figure1 내 test 응용프로그램의 스크린샷.

Figure 1 : 실시간에 인자에 대한 형 검사를 하는 것을 보여 주는 REPL 구현. 


구현.


이 구현에서 나는 Andrei Alexandrescu 에 의해 소개된 type list 의 개념을 사용할 것이다.


여기에 type list 를 구현하는 기본 클래스가 있다 :



Type list 를 생성하기 위해, 나는 다음과 같은 방식으로 이 클래스를 사용한다 :



NullType 은 특별한데 "종료자( terminator )" 형식이라 불린다. 이는 list 의 끝을 찾는데 도움을 주며 NullType{} 이라고 단순하게 정의될 수 있다.


이제 내가 쉽게 모든 함수 형식들( 반환 형식 및 입력 매개변수 )을 열거하는 type list 를 생성할 수 있다고 가정하면, 나는 자동 형식 검사 및 문자열로 받은 데이터들에 대한 변환을 수행할 수 있을 것이다. 그래서 만약 매개변수들이 ( REPL 에서 처럼 ) 문자열로 전달되면, 그것들은 자동으로 그것들을 검사하기 위한 어떤 도구를 필요로 한다. 이를 위한 가장 쉬운 방법은 무엇일까?


나는 다음과 같이 하는 것이 가장 쉬울 것이라고 생각한다 :



이제 이 클래스를 사용해서 다음과 같이 질의를 다룰 수 있다 : pcmd->run( "5 10" );


그 클래스 자체는 매개변수들을 검사하고 매개변수들이 올바를 때 호출을 수행할 것이다. 이 기사에서 내가 문자열을 분석하고 그것을 해당 형식으로 변환하는 연산에 대해서 다루고 있지 않지만, 당신은 내 샘플 프로그램에서 그것의 예를 찾아볼 수 있다. 거기에서는 응용프로그램을 위한 디버그 콘솔을 구현함으로써 지금 설명하고 있는 기술에 대한 응용프로그램을 설명한다.


이제 자동으로 type list 를 생성하기 위해서 createCmd 를 구현하는 방식에 대해 살펴 보자. 이 함수를 구현하기 위해서 나는 템플릿을 사용할 것이다. 다음은 함수의 선언이다 :



여기에서 name 은 함수에 대한 포인터 앞의 첫 번째 매개변수로 전달된다. 이 함수를 구현하는 것은 직관적이다 : 이는 그 작업을 수행할 실제 클래스에 대한 래퍼( wrapper )로서 사용된다.


다음은 단일 매개변수 버전을 위한 구현이다( 매개변수를 가지고 있지 않은 함수들을 위해서 void 일 수 있다 ). 나는 그냥 cmdCreator 클래스에 모든 매개변수를 전달할 뿐이다.



두 개 이상의 매개변수들을 위한 구현은 다음과 유사하다 :



이제 cmdCreator 클래스를 만들어 보자.


선언은 매우 단순해 보인다 : template struct cmdCreator;


다음은 입력 매개변수가 하나 이거나 없는( void ) 함수를 위한 구현이다 :



당신도 봐서 알겠지만, static 함수인 createCmd 는 우리 시스템의 심장이다. 그것은 template 의 매개변수들에 기반해 올바른 방식으로 type list 를 생성한다. 설명을 위한 목적으로 여기에 두 개의 매개변수를 가진 함수를 구현한다 :



CmdBase 에 대한 포인터를 반환하는 동안 TypeList 클래스를 생성한다는 것에 주목하라. 나중에 보여 주겠지만, 나는 CmdBase 로부터 TypeList 를 상속해서 다형성( polymorphism )의 이점을 취했다.


이제 올바른 TypeList 를 생성할 방법을 가지고 있다. 그런데 이것을 어떻게 적용해야 할까?


앞의 예제를 고려해 보자. 먼저 자동 형식 검사를 추가하기 위해서 TypeList 를 확장하라.


단순함을 위해 우리가 이미 분석된 입력 문자열을 가지고 있고, 매개변수들의 리스트를 생성했고, 그 매개변수들은 Variant 클래스로 래핑( wrapping )되어 있다고 가정하자. Variant 클래스는 유용한 개념이다. 부가적인 정보들을 알고자 한다면 Boost.Variant 나 다른 구현들을 참고해 볼 수 있다. 다음은 형식 검사를 수행하는 방법을 보여 준다 :



또한 나는 TypeList< R, NullType > 클래스를 위한 특수화를 정의해야만 한다. 왜냐하면 우리는 위의 함수에서 재귀가 끝나기를 원하기 때문이다.



나는 NumEI 라는 도우미 템플릿 클래스를 하나 더 소개했는데, 이는 type list 안에서 type index 를 위한 카운트를 유지한다. 이것은 선언은 다음과 같다 :



다음은 구현이다 :



봐서 알겠지만, 그것은 자신의 Tail 형식에 대한 반복( recursion )을 사용한다.



이제 run() 메서드를 구현하자. 나는 executor 라는 도우미 템플릿 클래스를 사용할 것인데, 그것은 cmdCreator 클래스를 위한 기술과 동일한 기술을 적용할 것이다. 그러나 먼저 나는 run() 메서드를 제공할 것이다. 할 일은 올바른 특수화를 제공하는 executor::run 메서드를 호출하는 것이다.



executor 클래스는 다음과 같이 선언된다 : template struct executor;


첫 번째 템플릿 매개변수 T 는 함수 포인터인데, 그것은 TypeList 클래스에 의해서 전달된다. 두 번째 템플릿 매개변수는 함수 형식들의 개수이다. 보면 알겠지만, 나는 myIndex 라는 컴파일 시점 상수를 사용하는데, 그것은 NumEl 도우미 클래스의 도움으로 계산됐다.


두 개의 매개변수를 위한 구현은 다음과 같다 :



여러 개의 매개변수들을 위한 구현은 매우 직관적이다.


보면 알겠지만, 나는 myfp 를 사용하는데, 그것은 함수에 대한 포인터이다. 그러나 나는 어떻게 이 형식을 얻는 것일까? cmdCreator 에서 나는 함수 포인터를 TypeList 의 두 번째 매개변수로서 전달한다. 우리의 type list 는 그것을 위한 어떠한 템플릿 매개변수도 가지고 있지 않은데, 어떻게 우리는 것을 생성할까? 이를 위해 나는 템플릿에 기반한 기교( trick )을 사용한다.


그것이 작동하는 방식을 이해하기 위해서 소스 코드를 좀 더 파 보자. TypeList 가 생성되는 지점으로 돌아가면( cmdCreator::createCmd ), 우리는 실제로 필요로 하는 모든 정보를 가지고 있다 : R, T1, T2 등. 우리는 단지 TypeList 를 R (*fptr )( T1, T2, ... ) 으로 변환하는 어떤 방법이 필요할 뿐이다.


내가 이를 변환하기 위해서 사용하는 도우미 클래스들은 TypeConv( TypeConv1, TypeConv2 등. 제공된 형식의 개수만큼 )이다.


종료( terminal ) 클래스들을 위한 가장 단순한 특수화로부터 시작할 것이다. 왜냐하면 그것이 이 아이디어를 가장 명확하게 보여 주기 때문이다. 이 클래스는 단지 하나의 typedef 만을 가지며, 그것이 다이다 :



하나의 매개변수를 가지는 함수들은 다음과 같이 보인다 :



이제 TypeConv 의 generic 버전을 실험해 보자 : 



이 코드는 기본적으로 typedef 를 재귀적인 방식으로 생성한다. TypeConv1 의 첫 번째 매개변수는 TypeList 의 Head 형식이며, 두 번째는 TypeConv 의 템플릿 매개변수 U 이다.그것은 TypeList 로서 재형식화( reformatted )되었다. 다음은 TypeConv1 클래스이다 :



이 코드는 ( 위의 것과 비슷해 보이는 ) TypeConv2 를 호출한다. 예를 들어 U 가 NullType 이었다면, 종료 버전의 특수화가 사용된다. 그렇지 않으면 일반적인 접근이 사용되며 U 가 NullType 이 될 때까지 재귀가 계속된다.


그러한 클래스들을 구현하기 위해, 나는 TypeList 클래스에다가 단지 두 줄의 코드만을 추가할 수 있다 :



그렇다!


나는 분명히 함수의 형식을 위한 다른 템플릿 매개변수들을 추가할 수 있지만, 우리가 이미 가지고 있는 정보들을 사용해 같은 결과를 획득하기 위해서 템플릿 메타프로그래밍을 사용하는 방법을 보여주고 싶었다. 이 "형식 변환"은 다른 목적으로 사용될 수도 있다.


같은 방식으로  나는 함수에 대한 사용 정보를 자동으로 출력해 주는 기능을 추가할 수 있다. 하나의 가능한 구현이 아래에 나와 있다 :



나는 여기에서 같은 접근법을 사용했다 : Tail 형식에 대한 재귀 호출. Type2String 은 단지 type 을 위한 문자열 이름을 반환하는 단순한 클래스일 뿐이다. 나는 그것을 여기에서 보여 주지는 않을 것이다. 왜냐하면 그것의 구현은 매우 단순하지만, 당신은 첨부한 코드에서 전부 볼 수 있기 때문이다.


요약을 위해 TypeList 클래스에 대한 전체 코드를 다음과 같이 제공한다 :



개선 사항.


반환값을 사용하면 어떻게 해야 할까? 그것은 당신에게 달려 있다. 나는 단지 그것을 디버그 콘솔 출력에다가 찍기만 한다. 그래서 사용자들은 실행된 명령의 결과를 볼 수 있을 것이다. If you try to use the output value, recall that it might be void.


Another thing you can add is a compile-time check for function parameters to be of a fundamental class, if you would like to constrain them.


결론.


함수를 위한 형식 추출을 제공함으로써, 이제 나는 다양한 요구를 위해 함수 형식 매개변수들을 점검하기( inspect ) 위한 유연한 메커니즘을 가지게 되었다.


이 기사에서 제공되는 코드는 ( 많은 게임들이 그런 것처럼 ) 디버그 콘솔을 생성하기 위해 적용되었다. 그리고 여기에서 코드를 찾아 볼 수 있다 : http://dconsole.googlecode.com




Sergii Biloshytski 는 임베디드 프로그래머로서 경력을 시작했지만, 길을 바꿔서 게임 프로그래머가 되었다. Ubisoft 에서 그는 Blazing Angles, From DUST, Assassins Creed 등과 같은 타이틀들과 관련한 작업을 해 왔다.

주의 : 공부하면서 정리한 내용이기 때문에 오류가 있을 수 있습니다.


우리의 이전 실습 과제에서는 array, function 등에 대해서 다루지 않았다. 이번 실습 과제에서는 그러한 기능을 보충해서 기본형에 대해서는 거의( ? ) 완벽한 type_descriptor 를 만들고, 그것에 대한 서술문을 출력하도록 하겠다.


일단 이전의 우리 구현에 따르면 array 나 function 과 같은 형식을 다루기 위해서는 remove_xxx meta function 이 존재해야 할 것이다. array 야 remove_bounds 와 같은 meta function 이 있지만, function 을 제거하기 위한 remove 류의 meta function 은 없다. 결국 우리는 function 과 관련한 복합 형식을 분석하기 위해서는 부분 템플릿 특수화에 의존해야 한다는 결론에 도달하게 된다.


그래서 이전의 구현을 다음과 같이 수정했다. Template 부분 특수화를 사용한 버전이다. 아래 구현을 잘 살펴 보면 크게 어렵지 않을 것이라 생각한다. 그리고 함수를 특수화는 귀찮아서 parameter list 가 두 개인 것까지만 구현했다.



위의 코드를 실행하면 다음과 같은 결과가 나온다.



특이한 점은 함수의 parameter list 에 array 표현이 넘어 가게 되면 그것이 pointer 로 취급된다는 것이다.


주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 2-4 :


과제 2-3 의 해답을, Boost 형식 특질 라이브러리를 사용하지 않고 작성하라. 그리고 두 해답들을 비교하라.




이건 RTTI 의 기능을 사용해 간단하게 구현할 수 있었다.




그러나 안타깝게도 참조 정보가 전부 날라가는 것을 알 수 있다. 왜 이 정보가 날라가는지 검색을 해 보니 다음과 같은 답변을 찾을 수 있었다.


이는 typeid 가 작동하도록 제안된 방식이다. 당신이 typeid 를 reference type 의 type-id 에 적용할 때, type_info object 가 referenced type 을 참조한다.

ISO/IEC 14882:2003, 5.2.8 / 4 [ expr.typeid ] :

When typeid is applied to a type-id, the result refers to a type_info object representing the type of the type-id. If the type of the type-id is a reference type, the result of the typeid expression refers to a type_info object representing the referenced type. If the type of the type-id is a class type or a reference to a class type, the class shall be completely-defined. Types shall not be defined in the type-id.

출처 : http://stackoverflow.com/questions/5151875/typeid-doesnt-return-correct-type


그리고 변수에 대한 const 도 날라갔다는 것을 알 수 있다. 왜 이 정보가 날라가는지 검색을 해 보니 다음과 같은 답변을 찾을 수 있었다.


그것들은 같은 type 이 아니지만, typeid 연산자는 const 와 volatile 을 벗겨 낸다.

From section 5.2.8 [ expr.typeid ]

The top-level cv-qualifiers of the glvalue expression or the type-id that is the operand of typeid are always ignored.

출처 : http://stackoverflow.com/questions/8888960/why-typeid-returns-that-int-and-const-int-are-same-types


주의 : 공부하면서 정리한 것이기 때문에 오류가 있을 수 있습니다.


실습 과제 2-3 :


형식 특질 수단들을 이용해서, 그 인스턴스를 스트림에 넣었을 때 템플릿 매개변수의 형식 이름을 출력하는 type_descriptor 클래스 템플릿을 작성하라( 실행시점 형식 정보( RTTI )로 같은 효과를 얻을 수는 없다. C++ 표준의 18.5.1 [ lib.type.info ] 에서의 문단 7 에 따르면, typeid( T ).name() 이 의미 있는 결과를 돌려 준다는 보장이 없기 때문이다 ).



type_descriptor 의 템플릿 매개변수가 char, short int, int, long int 로만 구성된 복합 형식이라고 가정해도 좋다.




삽질 : 


이 문제를 풀기 위해서는 앞의 실습 과제들에서 했듯이 복합 형식을 분리할 수 있어야 한다. 그런데 굳이 모든 템플릿 특수화를  할 필요는 없다고 생각할 수 있다. 왜냐하면 문제에서는 "형식 특질 수단들을 이용해서"라고 했기 때문에 boost 의 meta function 들을 사용하면 된다고 생각했다.


그러나 boost 에는 is_integral 과 is_float 은 있지만 아쉽게도 is_char, is_short 같은 것은 존재하지 않는다. 그러므로 특수화를 하지 않고서는 이 문제를 해결할 수 없다.


그리고 복합형식인 경우와 복합형식이 아닌 경우를 구분할 수 있어야 한다. 그렇다면 우리는 몇 가지 패턴을 생각해 볼 수 있다.


  • T
  • const T
  • T const
  • T*
  • const T*
  • T const*
  • T* const
  • T&
  • const T&
  • T const&
  • T& const


그런데 이 각각의 경우에 과제에서 제시한 4 가지 형식이 결합되면 경우의 수는 ( 4 X 11 = ) 44 이며, 그에 대응해 특수화를 해야 한다. 문제에는 나와 있지 않지만 참조까지 고려하면 장난이 아니다. 여기에서 const T 와 T const 가 같고, const T* 와 T const*, const T& 과 T const& 이 같기 때문에 이를 배제하면 경우의 수는 ( 4 X 9 = ) 36  이다. 그러나 아직까지도 포인터의 참조라든가 이런 조합도 있기 때문에 특수화로는 답이 안 나온다.


어쨌든 일단은 두 개가 template 에서도 같은 복합형식으로 인지되는지 확인부터 해 봤다.



일단 다행히도 같은 형식으로 취급되었다.


이제 어떻게 하면 특수화를 줄여 볼 수 있을까라는 고민으로 이어졌다. 한참을 고민하다가, 한글을 완성형으로 하면 개수가 많아지고 조합형으로 하면 개수가 줄어들 듯이 그런 방법을 사용하면 어떨까 하는 생각이 들었다. 그러나 어떻게 하면 조합할 수 있을지 상상이 안 가서 결국엔 포기하고 googling 을 하고 말았다.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_2-3


link 를 보는 순간 문제를 잘 못 이해하고 있었다는 생각이 들었다. 문제에서는 다음과 같은 형태로 출력하라고 되어 있다.



다음과 같은 형태가 아닌 것이다. 



계속해서 meta function 의 type 이나 value 를 가져다 쓰는 예제를 보고 있다가 보니 완전히 착각한 것이다. 사람의 고정관념이라는 것은 참 무섭다는 생각이 든다. 뭐 구현을 보면 문제를 제대로 이해하고 있더라도 엄청난 노가다를 하던가 제대로 풀지 못했을 것 같기는 하다. 가끔 template metaprogramming 을 사용해 문제 해결을 잘 하는 사람들은 천재가 아닐까라는 자기비하적인 생각도 든다.


풀이 : 


어쨌든 함수 호출 순서대로 설명하면 가장 먼저 정의할 것은 "<<" 연산자에 대한 overloading 이다. 이 연산자는 별건 없고 type_descriptor::print() 함수를 호출해 준다.



그 다음에는 type_descriptor template class 를 만들어야 한다. 그 안의 print() 함수는 boost 의 meta function 들을 사용한다. 일단 형식을 출력하는 순서를 정해야 한다. 그런데 각각의 형식이나 한정자들은 마치 차원이 다른 것처럼 동작하고 있기 때문에 tree 를 타고 내려 가듯이 재귀함수처럼 구현한다. 주의할 것은 다음 단계로 넘어 가기 전에 한정자나 포인터, 그리고 참조를 제거하고 항상 제일 앞에 단일 형식이 존재하는 것처럼 취급한다는 것이다.



전체 코드는 다음과 같다.




위의 링크에 들어가 보면 지금 구현이 가진 한계에 대해서 지적하고 보충하는 댓글들이 있다. 한 번씩 읽어 보면 도움이 더 많이 될 것이라 생각한다. 


여기에서는 단순한 형태에 대해서만 다루고 있다. 이것을 배열과 함수에 대해서 확장하기 위해서는 "실습 과제 2-5 풀이"를 참조하라.

주의 : 공부하면서 정리한 내용이므로 오류가 있을 수 있습니다.


실습 과제 2-2 :


boost::polymorphic_downcast 함수 템플릿은 포인터를 다형적 객체로 하향 변환하기 위한 static_cast 의 점검 버전을 구현한다.



릴리스 모드에서는 assert 가 사라지며, polymorphic_downcast 는 보통의 static_cast 만큼이나 효율적이 될 수 있다. 형식 특질 수단들을 이용해서, 이 템플릿을 포인터와 참조 인수 모두를 허용하도록 구현하라.





풀이 : 


이 문제는 함수 template 의 특수화에 대한 부분에서 시작한다. 문제에서 포인터와 참조 인수를 모두 허용하도록 하라고 했기 때문에, 이 문제를 풀 때 처음에는 아무 생각없이 함수 template 을 부분 특수화할 것이다.


하지만 함수 template 은 부분 특수화를 할 수 없다. 그러므로 overloading 을 통해 pointer 버전과 reference 버전을 만들어야 한다. 나머지는 기존에 우리가 했듯이 template 부분 특수화를 사용해서 처리하면 된다.


여기에서 한 가지 특이한 점은 boost::remove_reference 와 boost::remove_pointer 를 호출한다는 것이다. Target 은 pointer 나 reference 인 복합형식인 반면에 Source 는 복합 형식이 아니기 때문에, 두 형식을 가지고 boost::is_base_and_derived 를 호출하면 반드시 false 를 반환하게 된다. 그러므로 pointer 나 reference 를 복합형식에서 벗겨 내야 하는 것이다.


아래 코드를 잘 살펴 보면 크게 어려운 부분은 없을 것이다.



다른 사람들의 풀이를 보려면 다음 link 를 참조하라.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_2-2


주의 : 공부하면서 정리하는 것이기 때문에 오류가 있을 수 있습니다.


실습과제 2-1 :


아래의 예처럼, 임의의 복합 형식 c 를 첫 번째 인수로 취하고 c 에 있는 모든 형식 x 를 형식 y 로 치환하는 삼항 메타함수 replace_type< c, x, y > 를 작성하라.



둘 이하의 인수들을 가진 함수 형식만 지원해도 된다.




풀이 : 


이 문제의 요점은 "특정 type 을 찾고, 그것을 다른 type 으로 바꾸기" 이다. 원하는 type 들이 template parameter 로 넘어 오게 된다. 이 문제를 푸는 것은 두 개의 단계로 나뉜다.

  1. 복합 형식 c 에서 x 형식을 찾아 내기.
  2. 형식 x 를 형식 y 로 바꾸기.


사실 복합 형식이 아닌 것을  변환하는 것은 그리 어렵지 않게 할 수 있다.



그렇지만 복합 형식은 어떻게 해야 할지 감이 잘 안 올 것이다. Template 특수화 중에서는 복합 형식에 대한 부분 특수화 표현이 있던 것을 기억할 것이다. 예를 들어 어떤 template 이 pointer 형에 대해서 특수화되기를 기대할 때 다음과 같은 식으로 표현했다.



마찬가지로 우리는 배열을 표현할 수 있다.



그런데 _tMain 의 위의 code 를 실행하면 두 번째 문장( 우리 실습 과제 중 하나인 문장이다 )에서 false 를 반환한다. 그 이유는 이 부분 특수화가 기본형을 가진 array 에 대해서만 동작하기 때문이다. 두 번째가 true 를 반환하게 하기 위해서는 다음과 같은  부분 특수화를 더 만들어 줘야 한다.



하지만 이 code 를 build 하면 주석에 나와 있는 것처럼 살포시 error 가 나 준다. 그 이유는 "type * [ N ]" 이라는 부분에서 compiler 가 '*' 를 pointer 형 선언이 아니라 곱하기 연산자로 인식해 버리기 때문이다. 그래서 "typedef 가 끝났는데 ';' 가 없다"고 주장하는 것이다.


이 문제를 해결하기 위해서는 typedef 의 첫 번째 type 을 pointer 형으로 만들어 주면 된다.



Build 해서 실행하면 둘 다 true 가 나오는 것을 알 수 있다.


이러한 code 를 보고 있다 보면, template 특수화라는 것이 정말 "개노가다" 작업임을 알 수 있다. 그 때문에 boost 같은 library 들은 macro 를 엄청나게 많이 사용하고 있는 것 같다. 실제 hpp 파일을 열어 보면 대부분이 macro 로 되어 있음을 알 수 있을 것이다.


마지막으로 함수와 관련한 meta function 을 살펴 보자. 여기에서 한 번 또 막히게 되는데, 이 부분만 이해하면 meta function 에 대해 더 많은 감이 오게 된다. 현재 예제의 함수는 두 개의 type 을 가진다. 하나는 return type 이고, 하나는 parameter type 이다. 이렇게 함수를 분해해 놓으면 좀 더 쉽게 형태를 이해할 수 있다.



함수를 위한 부분 특수화의 특이한 점이라고 하면 return_type 과 parameter_type 을 가지고 있다는 점이다. 책에서는 이를 blob pattern 이라고 회피해야 한다고 했는데, 이 경우에는 정말 어쩔 수가 없다. 물론 이것도 함수를 만들어 주는 meta function 을 만들면 어떻게 될 것 같기도 하다.


( * )는 이름없는 함수의 pointer 형식이다. 이를 typedef 하기 위해서 "typedef return_type ( *type )( param_type )" 과 같은 표현을 썼다. 잘 모르는 사람은 함수 포인터로 검색해 보기 바란다.


책에 있는 실습 과제를 푼 최종 code 는 다음과 같다. boost 같은 library 들을 보며 어떻게 하면 단순화할 수 있을 지 연구해 볼 필요가 있을 것 같다.



주의 : 저만의 풀이이기 때문에 오류가 있을 수 있습니다.

실습 과제 2-0 : 


T 가 참조 형식이면 T 를, 그렇지 않으면 T const& 를 돌려주는 단항 메타함수 add_const_ref<T> 를 작성하라. 그리고 그 메타함수를 시험하는 프로그램도 작성하라. 힌트 : 결과를 검사할 때 boost::is_same 을 사용할 수 있다.




풀이 :


처음에 문제를 이해하기는 했지만 어떻게 해야 할지 감도 안 잡혔다. add_const 는 쉽게 상상이 가능했다.



왜냐하면 meta function 은 다음과 같이 정의되기 때문이다.


메타함수는 공개적으로 접근할 수 있는 type 이라는 이름의 내포된 결과 형식을 가진,

  • 모든 매개변수들이 형식인 클래스 템플릿

또는

  • 클래스

이다.

- 출처 : C++ Template Metaprogramming, 정보문화사.


그런데 조건이 걸리면서 문제가 생겼다. 조건이 걸렸기 때문에 type 을 반환하기 위해서 뭔가 내부적으로 또 다른 내포된 class template 을 사용하기는 해야 할 것같다고 생각을 했다. 하지만 그게 어떤 형태인지 책을 좀 뒤져 봐도 상상이 잘 안 갔다.


일단 reference 인지 판단을 어떻게 해야 하는지부터가 감이 오지 않았다. 특히나 분기문을 어떻게 구현해야 할지 감이 잘 안 왔다. 그래서 다른 사람들은 어떻게 했는지 googling 을 해 봤다.


http://www.crystalclearsoftware.com/cgi-bin/boost_wiki/wiki.pl?CPPTM_Answers_-_Exercise_2-0


보는 순간 "아!"하고 감이 왔다. Compile 시점에 판단을 수행해야 하기 때문에 이는 template 부분 특수화를 통해 이루어지고 있었다. 부분 특수화에 대해서 잘 모른다면, 이전에 번역한 문서인 Template Specialization and Partial Template Specialization 을 참조하기 바란다.


그 중에서 type 이 아니라 실제 value 를 가지고 특수화하는 부분을 이용할 수 있다. 아래 code 를 보면 XXX_impl 라는 식으로 suffix 가 붙은 부분이 있는데, 잘은 모르겠지만 내부적으로 ( 분기문처럼 ) 복잡하게 처리하는 경우에는 그런 suffix 를 붙이는 것 같아서 필자도 붙여 봤다. 나중에 정확한 용례를 알게 되면 소개하도록 하겠다.



필자는 나름대로 구현을 해 본 건데, 위의 링크를 보면 다른 사람의 구현도 참조할 수 있다. 자세히 안 봐서 잘은 모르겠지만, 나름 논쟁이랑 자랑질도 있는 것 같았다. 관심 있는 사람은 직접 구현도 해 보고 비교도 해 보면 좋을 것 같다.


이 과제를 하다가 느낀 거지만 boost 의 meta function 들을 분석해 보는 것도 꽤 좋은 학습이 될 것 같다. 앞으로 하나씩 분석해서 올려 봐야겠다.


결과는 다음과 같다.




원문 : http://www.cprogramming.com/tutorial/template_specialization.html

주의 : 번역이 개판이므로 원문을 참조하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.

정보 : 가장 아래에 있는 관련 기사들도 같이 보시면 좋을 것 같습니다.


Template Specialization and Partial Template Specialization


Template Specialization


Template 을 가지고 작업을 하는 많은 경우에, 당신은 모든 가능한 data type 을 위한 범용적인 버전의 template 을 작성할 것이며, 그것을 그대로 둘 것이다 -- 모든 vector 는 아마도 정확히 같은 방식으로 구현될 것이다. Template 특수화라는 아이디어는 기본 template 구현을 덮어 써( overriding ) 특정 type 을 다른 방식으로 다루는 것을 의미한다.


예를 들어 대부분의 vector 들이 주어진 type 에 대해 배열로서 구현될 것이지만, 당신은 약간의 메모리를 절약하기로 결심하고, bool 의 vector 들을 vector 내의 하나의 요소( entry )가 각 bit 와 연관되는 integer 의 vector 로서 구현할 수 있다. 그래서 당신은 다음과 같은 두 개의 개별적인 vector class 들을 가질 수 있다. 첫 번째 class 는 다음과 같다.



그러나 그것이 bool 일 때 당신은 실제로는 이렇게 하고 싶지 않을 것이다. 왜냐하면 대부분의 system 들이 1 bit 가 필요함에도 불구하고 boolean type 을 위해 16 bit 나 32 bit 를 사용하기 때문이다. 그러므로 우리는 integer 배열로 data 를 표현함으로써 boolean vector 의 모양을 약간 다르게 만들어야 한다. 그것의 bit 들은 우리가 수동으로 조작하게 된다. ( bit 를 직접적으로 조작하는 것과 관련해 더 알고 싶다면, bitwise operators and bit manipulation in C and C++ 을 참조하라. )


이렇게 하기 위해 우리가 template 과 유사한 것을 가지고 작업하지만 이번에는 template 의 매개변수( parameter ) 가 비어있을 것이라는 것을 지정할 필요가 있다 : 



그리고 그 class 의 이름 뒤에는 특화된 type 을 넣는다 : class className< type >. 이 경우에는 그 template 이 다음과 같다 :



만약 특화된 버전의 vector class 가 범용 vector class 보다 더 다양한 interface( public method 집합 )를 가지고 있다면, 그것은 완벽하게 합리적일 것이다 -- 비록 둘 다 vector template 이지만, 그것들은 어떠한 interface 나 code 도 공유하지 않는다.


이 경우에 특화를 하는 가장 중요한 이유는 더 공간을 효율적으로 이용하는 구현을 허용하는 것이었음을 짚고 넘어 갈 필요가 있다. 그러나 당신은 이것이 쓸모가 있는지에 대해 생각해볼 수 있다 -- 예를 들어, 당신이 어떤 type 에 의존하는 templated class 에 부가적인 method 를 추가하고 싶지만 다른 type 에 대해서는 그렇게 하고 싶지 않은 경우이다. 예를 들어, 당신이 상속을 더 좋아하기는 하지만, 당신은 각 요소의 non-integer 부분을 반환하는 method 를 가지는 double 의 vector 를 가지고 있을 수 있다. 그러한 부가적인 기능을 가지지 않는 double 의 vector 가 존재하지 않도록 막을 특별한 이유는 없다. 그러나 만약 당신이 그것이 쟁점이라고 느끼고 그것을 막고자 한다면, 당신은 template 특수화를 사용해서 그것을 수행할 수 있다.


어떤 template 을 특수화하기를 원하게 되는 다른 경우는 당신이 template 을 저장하고자 하는 class 의 collection 에 구현되지 않은 어떤 동작( behavior )에 의존하는 template type 을 가지고 있을 경우이다. 예를 들어 당신이 > 연산자가 정의되어 있을 것을 요구하는 templated sortedVector 를 가지고 있고, 누군가에 의해서 작성된 class 집합은 overload 된 연산자를 포함하고 있지는 않지만 비교를 위한 함수를 가지고 있다고 한다면, 당신은 template 을 특수화해서 그 class 들을 개별적으로 다룰 수 있다.


Template Partial Specialization


부분 template 특수화는 위에서 기술한 전체 특수화와 비슷한 동기를 가진다. 그러나 이번에는 하나의 특정 type 을 위해서 class 를 구현하는 것이 아니라, 여전히 다른 매개변수화( parameterization )를 허용하는 template 을 작성하는 데서 끝날 것이다. 즉 당신은 하나의 기능( feature )을 특수화하는 template 을 작성하지만, 여전히 그 class 의 user 는 template 의 일부로서 다른 기능들을 선택할 수 있도록 한다. 예제를 통해 이를 좀 더 확실하게 하자.


Vector 의 개념을 확장하는 아이디어로 돌아가 보자. 그러면 우리는 sortedVector 를 가지고 있다. 이것이 어떻게 보일 것인지 생각해 보자; 우리는 비교를 위한 방법을 필요로 할 것이다. 좋다; 만약 연산자가 정의되어 있다면 그냥 > 를 사용하고, 그렇지 않다면 특수화를 한다. 그런데 이제 우리가 정렬된 vector 안에 object 에 대한 pointer 를 가지고 있기를 원한다고 가정해 보자. 우리는 pointer 의 값으로 그것들을 정렬할 수 있을 것이다. 단지 표준 > 비교를 수행하기만 하면 된다( 우리는 오름차순으로 정렬되는 vector 를 가지게 될 것이다 ).



이제 위의 반복문에서 우리는 T type 의 요소들 사이에서 직접 비교를 수행하고 있다는 것을 알게 될 것이다. 대부분의 경우에는  괜찮다. 그러나 아마 pointer 주소가 아니라 실제 object type 상에서 정렬되는 것이 더 말이 될 것이다. 그렇게 하기 위해서 당신은 code 를 다음과 같은 line 을 가지도록 작성해야할 필요가 있다 :



물론 그것은 non-pointer type 에 대해서는 작동하지 않는다. 여기에서 우리가 하고 싶은 것은 그 type 이 pointer 인지 non-pointer 인지 여부에 의존하는 부분 특수화이다( 당신은 pointer 의 multiple level 을 가지고 싶을 수도 있다. 하지만 그냥 단순하게 하자 ).


모든 pointer type 을 다루는 부분적으로 특수화된 template 을 선언하기 위해서, 우리는 이 class 선언에 다음을 추가할 것이다 :



여기에서 주목해야 할 두 개의 syntax point 가 있다. 첫 번째는 우리 template 매개변수 리스트가 여전히 매개변수의 이름을 T 라고 짓고 있지만, 그 선언은 class 의 이름 뒤에 T* 로 붙는다; 이는 컴파일러에게 모든 type 의 pointer 에 대해 더 범용적인 template 대신 이 template 을 연결해 달라고 요청하는 것이다. 두 번째는 T 가 이제 pointer 로 접근하는 type 이라는 것이다; 그것 자체는 pointer 가 아니다. 예를 들어 당신이 sortedVector< int* > 를 선언할 때, T 는 int type 을 참조할 것이다! 이는 당신이 그것을 별표( * )가 붙은 type 이 있을 때 그것을 매칭해 주는 형식이라고 생각하면 이해가 쉽다. 이는 당신이 구현을 할 때 좀 더 주의를 해야 한다는 것을 의미한다 : vec_data 는 T** 이다. 왜냐하면 우리는 pointer 로 구성된 동적 크기의 배열을 필요로 하기 때문이다.


당신은 sortedVector type 이 실제로 이렇게 동작기를 원하는지 궁금해 할 수 있다 -- 결국 만약 그것들을 pointer 의 배열로 삽입한다면, 당신은 그것들이 pointer type 에 의해 정렬될 것을 기대할 것이다. 그러나 이렇게 하는 것에는 실질적인 이유가 존재한다 : 당신이 object 에 대한 배열을 위해 memory 를 할당할 때, 반드시 각 object 를 생성하기 위해 기본 생성자가 반드시 호출되어야만 한다. 만약 기본 생성자가 존재하지 않는다면( 예를 들어 모든 object 가 생성되기 위해서는 어떤 data 를 필요로 한다면 ), you're stuck needing a list of pointers to objects, 그러나 당신은 아마도 그것들이 실제 object 들 자신이 정렬되는 것처럼 정렬되기를 원할 것이다.


그건 그렇고 당신은 template 매개변수에 부분 특수화를 수행할 수도 있다 -- 예를 들어 만약 당신이 fixedVector type 을 가지고 있는데 그것이 ( 동적 memory 할당 비용을 피하기 위해서 )  class 의 사용자가 저장될 type 과 vector 의 길이를 모두 지정할 수 있도록 허용한다고 하면, 다음과 같은 코드를 볼 수 있게 될 것이다 :



그리고 나서 당신은 다음과 같은 구문으로 boolean 들을 부분적으로 특수화할 수 있을 것이다 :



T 가 더 이상 template 매개변수가 아니기 때문에 그것은 template 매개변수 리스트 밖에 있으며, length 만 남게 된다는 데 주의하라. 그리고 그 length 는 ( 범용 template 선언을 할 때는 이름 뒤에 아무것도 지정하지 않는 것과는 다르게 ) 이제 fixedVector 의 이름으로 나타난다. ( 그건 그렇고 template 매개변수가 non-type 이라는 것을 보고 놀라지는 마라 : 그것은 완벽하게 유효하며, unsigned 와 같은 integer type 인 template 인자들을 가지기 위해 유용하게 쓰이기도 한다. )


최종 구현 세부사항이 부분 특수화까지 왔다 : 컴파일러는 완벽하게 범용적인 type 들에 대한 조합, 어떤 부분 특수화, 그리고 완전한 특수화가 존재할 때 어떤 특수화를 선택할까? 일반적인 경험 법칙( rules of thumb )은 컴파일러가 가장 명확한( specific ) template 특슈화를 선택할 것이라고 이야기한다 -- 가장 명확한 template 특수화는 다른 template 선언들에 의해서 받아들여질 수 있는 template 인자들을 가지고 있지만, 같은 이름을 가진 다른 template 들이 받아들일 수 있는 인자를 모두 받아들일 수는 없는 template 을 의미한다.


예를 들어 만약 sortedVector< int* > 가 memory 위치에 의해 정렬되기를 원한다고 할 때, 당신은 sortedVector 에 대한 전체 특수화를 생성할 것이다. 그리고 sortedVector< int* > 를 선언하면 컴파일러는 pointer 에 대해 덜 명확한 부분 최적화를 선택할 것이다. int* 는 전체 특수화와만 일치하지만 double* 과 같은 다른 pointer type 와는 일치하지 않기 때문에 그것이 가장 근접하게 특수화된 것이다. 반면에 int* 는 명백히 다른 template 들이 받아들이는 매개변수가 될 수 있다.




Related articles.

C++ Templates Quiz : 당신의 template 지식을 테스트해 보세요.

Templated classes in C++ : templated class 에 대한 배경 정보.

Templated functions : 함수 template 에 대해 더 많이 배워 보세요.


'Programming > C++' 카테고리의 다른 글

[ 번역 ] Universal CRT 소개  (0) 2015.12.28
[ 번역 ] C++ Modifier Types  (0) 2013.01.02
[ 번역 ] Function Call Operator ()  (0) 2012.12.24
[ 번역 ] explicit( C++ )  (0) 2012.12.24
[ 번역 ] C++ Metadata - Part 1, Singletons and Lookup  (0) 2012.10.31

+ Recent posts