꽃미남 프로그래머 김포프가 창립한 탑 프로그래머 양성 교육 기관 POCU 아카데미 오픈!
절찬리에 수강생 모집 중!
프로그래밍 언어 입문서가 아닌 프로그래밍 기초 개념 입문서
문과생, 비전공자를 위한 프로그래밍 입문책입니다.
jobGuid 꽃미남 프로그래머 "Pope Kim"님의 이론이나 수학에 치우치지 않고 실무에 곧바로 쓸 수 있는 실용적인 셰이더 프로그래밍 입문서 #겁나친절 jobGuid "1판의내용"에 "새로바뀐북미게임업계분위기"와 "비자관련정보", "1판을 기반으로 북미취업에 성공하신 분들의 생생한 경험담"을 담았습니다.
Posted by 터너 (TerNer)

[이전 글]

The Modernest C++ #0 - 시작하며

The Modernest C++ #1 - 템플릿 메타프로그래밍 (Template Metaprogramming)

The Modernest C++ #2 - 타입을 가지고 놀아보기, type_traits

The Modernest C++ #3 - 수학 라이브러리

The Modernest C++ #4 - 컴포넌트와 템플릿 메타프로그래밍 - 上



컴포넌트와 템플릿 메타 프로그래밍 - 下


본 게시물에 첨부된 모든 이미지는 클릭시 확대됩니다. 참고하세요!


저번 시간에는 정적 멤버의 포인터를 활용하여 컴포넌트의 식별자(identifier)로 사용함으로서 오타 발생 방지, 속도, 생산성 면에서 획기적으로 도움을 받을수 있는 방법을 배웠습니다. 이로써 모든 작업이 끝난것 같지만 게임에 이용하기에는 아직 많이 허술합니다. 여러가지 방면으로 보강을 해줘야할 필요가 있습니다. 우선 문제를 직면하는 상황을 만들어서 그때 대처하는 방법을 알아보도록 합시다.





만일 컴포넌트 구조가 다음과 같은 상속 구조를 가진다고 할때,





위와 같은 코드는 널 포인트 참조에 의해 크래쉬가 일어날수 있습니다, 왜일까요? 상식적으로 이미지 렌더 컴포넌트는 렌더 컴포넌트를 상속했기 때문에 기본적으로 C++에서는 부모 컴포넌트로의 캐스팅이 자유롭습니다. 하지만 이미지 렌더 컴포넌트는 상위 클래스의 식별자가 아닌 자기 자신의 고유 식별자가 있으므로 렌더 컴포넌트의 식별자가 아닌 이미지 렌더 컴포넌트의 식별자로 저장이 되었기 때문에 상위 컴포넌트의 식별자로 컴포넌트를 가져오려고 해도 실패합니다. 또한 위와 같은 코드가 오류를 발생하게 되면 아래와 같은 상황에서 큰 문제가 발생할수도 있습니다. 





위 코드는 게임 내부에 존재하는 모든 게임 오브젝트에 대해서 순회하며 렌더링을 수행하는 코드 입니다. 모든 게임 오브젝트는 렌더링하는 타겟이 다를수 있습니다. 3D 메쉬, 문자열, 이미지등 렌더링 할수 있는 모든 상황이 존재하기 때문에 위와 같은 코드는 필수적으로 실행이 될수 있어야 합니다. 따라서 각자 렌더링하는 타겟에 따라 다른 컴포넌트 타입을 이용해서 컴포넌트를 가져오는건 설계 방법에 있어서 매우 비효율적이기 때문에 상위 클래스로 가져오는 것을 가능하게 해야합니다.


하지만 어떻게 할까요? 마땅한 방법이 떠오르지 않습니다. 저는 여기서 본격적으로 템플릿 메타프로그래밍을 활용해야 할 때라고 말씀드리고 싶습니다. ‘is_base_of’ 라는 메타함수를 이용하면 특정 타입이 다른 특정 타입의 부모 클래스인지를 확인 할 수 있습니다. 약간 감이 안오실수 있는데 예제와 함께 보여드리도록 하겠습니다.





보시는대로 is_base_of 메타함수는 상속관계를 추론할때 굉장히 유용하게 사용됩니다. 위 메타함수를 이용해서 컴포넌트를 삽입하는 과정에 있어서 부모 클래스를 판단하는 방법을 알아봅시다.





뭔가 좀 길지만 천천히 읽어보신다면 충분히 이해가 가능하시리라 믿습니다. 추가된것은 is_base_of 메타함수를 이용하여 컴포넌트가 CRenderComponent를 상속한다면 그 컴포넌트의 식별자로 작업을 하는것일 뿐 기본적인 동작 과정은 이전 글의 내용과 동일합니다. 잘 작동하는지 테스트를 해볼 차례입니다.





아주 잘 작동합니다. 이제 자식 컴포넌트로 삽입을 해도 부모 컴포넌트로 가져올수 있게 되었습니다. 하지만 여기서 끝이 아닙니다. 위 예제에서는 CRenderComponent만 존재하지만 실제 프로젝트 상에서는 여러개가 될수 있습니다. 만일 부모 클래스가 기하 급수적으로 많아지는 상황에 iks_base_of를 계속 추가해주는 것은 해결책이 되지 못합니다. 코드의 길이가 길어질 뿐만 아니라 부모 컴포넌트를 추가해줄때마다 계속 분기 코드를 추가해줘야 하기 때문에 생산성에 큰 영향을 줄수 있습니다. 


아래의 경우를 한번 생각해봅시다 :





자, CPositionComponent 라는 컴포넌트가 생겼고 그 컴포넌트를 부모 컴포넌트로 하는 상대위치, 절대위치 컴포넌트가 생겼습니다. 이때 프로그래머는 위 방법을 이용한다면 is_base_of를 이용하여 CPositionComponent를 상속하는지를 알아보는 코드를 추가를 해줘야 합니다. 그럼 분기문이 두개가 되고 프로그래머는 혼란스러워 질수 있습니다. 어떻게 하면 컴포넌트를 추가 할때마다 분기문을 추가해주지 않고도 부모 컴포넌트의 식별자 값으로 저장할 수 있을까요?


추가적으로 여기서 문제점이 또 발생하게 됩니다. 우리가 이미지 렌더 컴포넌트를 게임 오브젝트에 삽입할때 부모 컴포넌트인 일반 렌더 컴포넌트의 식별자 값으로 삽입을 해줄수 있었습니다. 하지만 논리적으로는 말이 되지 않지만 프로그래머가 상대 위치 컴포넌트를 삽입한 후에 절대 위치 컴포넌트를 삽입하면 두 컴포넌트는 부모 컴포넌트의 식별자 값으로 저장이 되기 떄문에 절대 위치 컴포넌트가 상대 위치 컴포넌트를 덮어 씌우게 됩니다.


코드로 설명해보자면 아래와 같습니다 :





그렇다면 일반 위치 컴포넌트를 상속하는 모든 컴포넌트는 논리적으로도 한 오브젝트가 상대위치와 절대위치를 동시에 가진다는 것은 알맞지 않기에 (상대위치와 절대위치 두 위치가 다를수 있기 때문) 게임 오브젝트에 단 하나만 존재해야 하는 컴포넌트가 됩니다. 이제부터 이런 컴포넌트를 유니크 컴포넌트 (Unique Component) 라고 부르도록 하겠습니다.


어떻게 하면 분기문을 추가하지 않고 유니크 컴포넌트들을 쉽게 추가할수 있을까요? 우리는 컴포넌트 식별자 객체로 눈을 돌려야 합니다. 이전 시간까지는 아무런 멤버도 가지고 있지 않았지만 이제는 기능을 추가해줘야 하며 메타 함수를 직접 구현해봐야 합니다.





컴포넌트 식별자 객체에는 몇몇 멤버를 추가하였습니다. 객체를 정의한 타입을 저장하는 current_component_t 와 부모의 타입을 저장하는 parent_component_t, 마지막으로 유니크 컴포넌트인지 여부를 저장하는 is_unique 총 세가지를 추가하였습니다. 프로그래머가 특정 컴포넌트 자신과 그것을 상속하는 모든 컴포넌트에 있어서 유니크 컴포넌트로 만들어 주고 싶다면 _is_unique 매개변수로 true를 넘겨주면 될것입니다. 자, 이제 이걸 컴포넌트 마다 추가해주는 작업을 거쳐봅시다.





모든게 완벽하지만 하나 문제가 생겼습니다. 최상위 컴포넌트는 부모 컴포넌트가 없는데 무엇을 추가해줘야 할까요? 우리는 C/C++ 뿐만 아니라 다른 언어를 사용할때도 아무 값도 정해주고 싶지 않을때나 값이 비어있다는 것을 명시해주기 위하여 null, NULL, nullptr, nil등을 이용해왔습니다. 타입에서도 그것이 가능한데 단순하게 빈 타입을 하나 정의해주면 됩니다. 






이제 최상위 컴포넌트에 있어서 부모 컴포넌트가 없으므로 null_t로 정해줬으며 모든게 완벽합니다. 이제 갖가지 기능을 하는 메타 함수를 제작해볼 차례입니다. 우리는 위에서 배운 내용으로 따르면 is_base_of 메타함수를 이용하여 대상 컴포넌트가 유니크 컴포넌트일때 부모 컴포넌트의 식별자 값을 구해와서 저장을 했습니다. 대상 컴포넌트가 유니크 컴포넌트인지 검사하는 메타 함수를 먼저 제작해봅시다.


우선 위에 새롭게 구현한 컴포넌트 식별자 객체에서 세번째 컴포넌트 매개변수로 true를 넘겨주면 자신과 자신을 상속하는 모든 컴포넌트가 유니크 컴포넌트가 된다고 언급 했었습니다. (유니크 컴포넌트 여부 자체를 상속하는 의미) 그럼 특정 컴포넌트의 부모 컴포넌트를 계속 따라 올라가면서 is_unique 멤버가 true로 설정된 컴포넌트 식별자를 가진 컴포넌트를 찾으면 될 것입니다. 





자, 내용이 비교적 난해할수 있으실텐데 하나하나 들여다보는 시간을 가져봅시다. 일단 컴포넌트 타입 하나를 매개변수로 받아와서 코드를 간략하게 하기 위하여 그 컴포넌트의 식별자 객체와 부모 컴포넌트를 얻어와서 typedef로 저장해둡니다. 그 다음 std::_If 메타함수를 이용하여 분기를 해줍니다. 분기의 내용은 이렇습니다 :


  1. 타겟 컴포넌트의 식별자 객체에 유니크 컴포넌트 여부를 나타내는 값이 true로 설정되있는지 확인

  2. 만일 true로 설정되있다면 타겟 컴포넌트는 유니크 컴포넌트 이므로 true_type를 리턴 (true와 동일)

  3. false로 설정되어있다면 타겟 컴포넌트의 부모 컴포넌트를 타겟 컴포넌트로 설정한후 메타 함수를 다시 실행 (재귀 호출)


위와 같은 과정을 거쳐서 부모 컴포넌트를 계속 쫓아 올라가면서 식별자 객체의 is_unique 멤버가 true로 설정 되있는지 여부를 모두 확인한 뒤, 하나라도 true로 설정되어 있다면 거기서 재귀 호출을 멈추고 타겟 컴포넌트가 유니크 컴포넌트임을 반환하게 됩니다. 그리고 템플릿 특수화를 이용하여 부모 컴포넌트를 계속 쫓아 올라가다가 null_t를 만나게 되면 강제적으로 false를 반환하게 됩니다.





아주 잘 작동하는 것을 확인할수 있습니다. 자 이제 컴포넌트가 유니크 컴포넌트임을 확인하는 방법까지 알았으니 유니크 컴포넌트가 되도록 해준 컴포넌트, 그러니까 컴포넌트 식별자 객체에 유니크 컴포넌트 여부를 설정한 컴포넌트를 얻어오는 방법만이 남았습니다. 예를 들면 이미지 렌더 컴포넌트로 따지면 일반 렌더 컴포넌트가 되겠죠? 이는 is_unique_component의 구현방식과 동일합니다. 재귀적인 호출을 해주면서 식별자 객체의 is_unique 멤버가 true로 설정된 컴포넌트를 반환해주기만 하면 됩니다.



 


get_unique_component의 작동 방식은 아래와 같습니다 :


  1. 타겟 컴포넌트의 식별자 객체에 유니크 컴포넌트 여부를 나타내는 값이 true로 설정되있는지 확인

  2. 만일 true로 설정되있다면 타겟 컴포넌트는 유니크 컴포넌트 이므로 타겟 컴포넌트를 리턴

  3. false로 설정되어있다면 타겟 컴포넌트의 부모 컴포넌트를 타겟 컴포넌트로 설정한후 메타 함수를 다시 실행 (재귀 호출)


이 메타함수도 is_unique_component와 같이 부모 컴포넌트를 계속 따라 올라가면서 식별자 객체의 멤버 값을 확인하고 맞다면 그를 반환하도록 하고, null_t를 만나면 메타함수가 더이상 처리를 하지 않고 null_t를 반환하도록 하였습니다. 이 메타함수가 잘 동작하는지 확인해볼까요?





테스트를 위하여 컴포넌트의 이름을 가져오는 컴포넌트의 이름을 가져오는 getComponentName 이라는 함수를 모든 컴포넌트에 정의해줬습니다. 이로서 컴포넌트가 유니크 컴포넌트인지 확인하고, 유니크 컴포넌트 여부 값이 설정되있는 컴포넌트를 구해오는 메타함수까지 모두 구현해줬습니다. 하지만 이것을 그냥 게임 오브젝트 객체에서 사용하기에는 분기문을 또 써줘야 하므로 모든 연산을 컴파일 타임에 밀어 넣는것은 불가능해질 수 있습니다.


get_unique_component와 is_unique_component를 이용하여 알아서 컴포넌트를 삽입하고 가져오는데에 적절한 컴포넌트를 가져오는 메타 함수를 제작해보도록 하겠습니다 :





뭔가 내용이 너무 길어져서 나름대로의 들여쓰기를 적용해봤습니다. 내용은 아주 단순한데, 만일 타겟 컴포넌트가 유니크 컴포넌트라면 타겟 컴포넌트를 유니크 컴포넌트로 만들어주는 컴포넌트를 가져오고 그렇지 않다면 자기 자신을 가져오게 됩니다. 코드는 정말로 복잡한데 정말로 간단하죠? (...) 이제 이것을 게임 오브젝트의 코드에 넣어주고 활용하는 예시를 보여드리도록 하겠습니다.





프로그래머는 해당 컴포넌트가 유니크 컴포넌트 여부를 따지지 않고 그저 메타함수에 타입만 넘겨주고 반환된 값으로 식별자 값을 얻어오기만 하면 알아서 적절한 컴포넌트 식별자 값을 찾아줍니다. 분기문을 쓸 필요도 없고 문자열이나 열거자등을 사용하여 생산성에 영향을 미치지 않고도 쉽게 구할수 있게 되었습니다.






위에서 사용된 getComponentName은 정적 멤버 함수였으나, 테스트를 위하여언더바(_)를 붙인 저 함수는 가상 멤버 함수로 정의 하였습니다. 개발중에 문제는 얼마든지 발생할수 있지만 컴포넌트 기반 개발을 이용하기 위한 모든 준비는 끝이 났습니다. 이로서 두 강좌에 걸친 컴포넌트와 템플릿 메타프로그래밍을 결합해보는 강좌를 마치도록 하겠습니다.


이번 강의에서 썼던 모든 내용을 올리도록 하겠습니다. 꼭 보시고 어디든 사용하셔도 좋으니 한번 코드를 분석해보시는 것도 좋을거라 생각합니다. 추가적으로 매크로를 이용하여 구현하였으니 꼭 코드를 확인해보시고 매크로를 이용하여 생산성까지 향상시키는 방법을 배워보시기 바랍니다.



Main.cpp






항상 제가 언급하는 것이지만 서도 템플릿 메타프로그래밍은 많은 이점을 줌과 동시에 많은 약점 또한 존재합니다. 가독성이 아마 가장 큰 요인으로 작용할 것입니다. 이 때문에 템플릿 메타프로그래밍을 프로젝트에 도입할지를 망설이고 계신 분들이 많을거라 생각됩니다.


제가 말씀드리고 싶은것은 템플릿 메타프로그래밍은 장단점을 모두 갖추고 있기 때문에 쉽게 결정하기 힘들다고 생각되며 가독성과 같은 단점을 감수할수 있다면 선택하는 것도 크게 도움이 될거라 생각하고 있습니다. 가장 좋은것은 상황에 맞게 사용 여부를 결정하는게 좋겠죠?


다음 강좌는 다시금 템플릿 메타프로그래밍에 대한 개념을 익혀보도록 하겠습니다.

반응형
,
Posted by 터너 (TerNer)

[이전 글]

The Modernest C++ #0 - 시작하며

The Modernest C++ #1 - 템플릿 메타프로그래밍 (Template Metaprogramming)

The Modernest C++ #2 - 타입을 가지고 놀아보기, type_traits

The Modernest C++ #3 - 수학 라이브러리

 


컴포넌트와 템플릿 메타프로그래밍 - 上


본 글은 끼로님의 컴포넌트 기반 개발 디자인 기법을 보고 영감을 얻어 템플릿 메타프로그래밍을 각색한 방법을 설명하는 글입니다.


우리는 이전 시간까지 템플릿 메타프로그래밍의 전반적인 내용을 배웠습니다. 템플릿을 이용하여 수학적 연산도 했으며, 타입을 검사하고 조작하는 기능을 다뤄보면서 이점이 무엇인지를 알게 되었고 반대로 템플릿 메타프로그래밍만의 가독성과 같은 약점도 알게 되었습니다. 하지만 우리는 이 강력한 기능을 좀 더 유용하게 이용할수 있어야 합니다. 글을 쓰는 저나 독자 분들이나 게임을 개발하시는 분들이기에 게임을 개발하는 입장에서 템플릿 메타프로그래밍을 어떻게 써야 할까 많은 고민을 했었습니다.


그래서 떠오른 부분이 바로 오브젝트 설계 방법에 있었습니다, 우리는 게임 내부에서 어떤 행동을 하거나 보여지거나 하는 모든 오브젝트들을 어떻게 구현해야할지에 대한 최선책이 필요했고 수 많은 방법이 있겠지만 저로써는 컴포넌트 기반 개발에 초점을 맞춰 지금까지 개발을 해왔었습니다. 컴포넌트 기반 개발은 채택하기 충분한 이점도 있지만 그에 맞는 약점도 분명히 존재합니다. 우리는 그 약점들을 템플릿 메타프로그래밍으로 보완해볼수는 없을까? 하고 의문을 던져보아야 합니다.



거의 모든 컴포넌트 기반 개발의 객체 구조는 이렇습니다. 게임 오브젝트 객체는 컴포넌트들을 map 컨테이너와 같은 메모리 공간에 저장하고 필요할때마다 꺼내 쓰게 됩니다. CBaseComponent는 모든 컴포넌트의 최상위 부모 객체이며 이와 같은 컴포넌트를 ‘최상위 컴포넌트’ 라고 부르도록 하겠습니다. C++ 언어의 특성상, 모든 컴포넌트 타입에 맞는 컨테이너는 구현이 불가능하기 때문에 우리는 이것들을 최상위 컴포넌트의 포인터 형식으로 저장해야 합니다. (e.g. CBaseComponent*)


모든게 완벽해보입니다, 컴포넌트는 필요에 따라 꺼내 쓰면되고 집어 넣기만 하면 충분하지만 문제가 발생합니다. 우리는 어떻게 무수하게 많을수도 있는 이 컴포넌트들을 어떻게 구분하는가가 문제입니다. 우리는 컴포넌트를 게임 오브젝트에서 가져오기 위하여 컴포넌트를 구분할때 쓰는 값을 앞으로 ‘식별자 (identifier)’ 라고 부르도록 하겠습니다. 식별자는 문자열이 될수도 있으며 그 어떤 모든 상수값이 될수 있습니다. 그저 같은 값이 아니라 컴포넌트 마다 충돌되지 않도록 다르게 값을 부여해주면 됩니다.



많은 분들이 이런식으로 게임 오브젝트에서 컴포넌트를 집어넣고, 가져오시기도 합니다. 하지만 이 방법들에는 분명한 문제점이 있습니다. 바로 오타가 발생할수도 있다는 점, 실행 속도가 부담이 된다는 점과 더불어 생산성(productivity)에서 크게 약한 모습을 보여줍니다. 


문자열의 경우에는 스펠링이 틀린 경우 컴파일러에서 에러로 처리해주지 않기 때문에 프로그래머가 쉽게 알아차리지 못할수도 있습니다. 또한 문자열은 값 자체를 컨테이너 내부에 저장하기 때문에 그를 비교할때나 해싱하는 과정에서 부하가 발생할수 있으며 특히 열거자를 이용하면 새로운 컴포넌트 객체를 설계하거나 잘못 설계하여 컴포넌트 자체를 제거할때 매번 열거자에 값을 추가/삭제 해줘야 한다는 점 때문에 생산성에 큰 영향을 미치기까지 합니다.


우리는 어떤 방법을 이용해야 오타 방지, 속도 개선, 생산성 면에서 이득을 볼수 있을까요? 우리가 지금까지 배웠던 내용인 템플릿을 이용하면 세마리 토끼를 한번에 잡을수 있습니다. 


 

어떤가요? 컴포넌트를 얻어올때 이용한 불필요한 매개변수를 더이상 쓸 필요가 없어졌으며 사소한 것일지도 모르지만 리턴 값을 자동으로 맞춰주게 됩니다. 기존의 문자열이나 열거자를 이용하는 방법에서는 버그 발생 가능성등의 이유로 최상위 컴포넌트를 리턴해야 했지만 위 코드에서는 CFooComponent를 반환하게 됩니다. 무엇보다 이 방법은 새로운 컴포넌트를 설계할때마다 새로운 값을 추가해 줄 필요가 없게 되었으며, 문자열 처럼 스펠링을 틀려도 컴파일러가 잡아주기 때문에 굉장히 우아한 방법이라고 할수 있습니다.


하지만 타입 그 자체를 식별자로 이용할까요? 타입을 정수로 바꿀수는 없고 마땅한 방법이 생각나질 않습니다. 우리는 이럴때마다 꼼수(?)를 이용해줘야 합니다. 바로 정적(static) 멤버를 이용해주면 됩니다. 정적 멤버는 인스턴스당 하나씩 존재하는 데이터가 아니라 타입당 하나씩 존재하는 유니크한 데이터이기 때문에 이를 이용해주면 됩니다. 어떻게 이용할지 감이 잘 안잡히실수도 있습니다. 아래의 코드를 살펴봅시다 :



자 코드만 보셔도 감이 오시리라 믿습니다, 컴포넌트 마다 정적 멤버로 컴포넌트 식별자 객체를 가지고 있도록 하였습니다. 그리고 가장 중요한 부분인 컴포넌트의 식별자를 컴포넌트 식별자 객체의 주소 값으로 대체하였습니다. 컴포넌트를 집어넣는 게임 오브젝트의 입장에서는 어차피 템플릿이니 그저 정적 함수를 호출해주는 것만으로도 고유 식별자를 얻어올수 있게 되었습니다. 



게임 오브젝트 객체 내부에서 이런식으로 컴포넌트의 식별자를 얻어와서 컴포넌트를 집어넣고 가져오게 됩니다. 이 코드에서는 매개변수로 문자열을 받지 않으므로 속도 상에서 이득을 볼수 있었습니다. 만일 컴포넌트가 아닌 엉뚱한 객체를 템플릿으로 넘겨준다고 해도 컴포넌트 식별자 객체가 없을테니 오류가 발생할수 밖에 없을겁니다.


자, 우리는 이 글에서 컴포넌트 기반 개발에 템플릿을 이용하면 얼마나 매끄럽고 우아한 코드가 나오는지 볼수 있었습니다. 다음 글에서는 컴포넌트 식별자 객체를 조금 더 템플릿 메타프로그래밍을 이용하여 재미있는 기능들을 만들어보려 합니다. 기대해주세요!







반응형
,
Posted by 알 수 없는 사용자

 

 

 

 

 Source.zip

 

 

반응형
,
Posted by 알 수 없는 사용자

 

 

SimpleGame.zip

본 파일은 이 연재의 소스 코드입니다.

 

반응형
,
Posted by 터너 (TerNer)

[이전 글]

The Modernest C++ #0 - 시작하며

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> 를 보여주게 되니 에러를 컴파일 이전에 확인하는데 큰 도움이 될수 있겠죠?



반응형
,
Posted by 알 수 없는 사용자

 

 

 

무기 및 Source파일.zip

 본 파일은 무기와 본 연재에 대한 소스코드가 들어 있습니다.

캐릭터와 달리 무기를 임포트 하실때는 Create Physics Asset 항목을 체크 해제 해 주시길 바랍니다.  

혹 체크 해제 안으시고 넣으셨을 경우에는 그냥 해당 폴더에 Physics asset을 삭제해서 지워 주시면 됩니다,

본 연재 이후 몇 주간 또 동영상 작업을 해야 하겠군요, 몇주 뒤에 뵙겠습니다. 동영상 하나 제작 하는데 시간 오래 걸리네요,^^;

 

반응형
,