모던 C++ 디자인이라는 책은 봐도봐도 새로운 내용이 나온다. 그책에서 건진내용중에 하나인데, 클래스가 상속을 받을때 virtual table이 그냥 생기는것이 아니라 virtual 함수가 있을때만 생긴다는것 - '단위전략'은 CRTP를 쓰기위해 다중상속을 활용하는데, 부모클래스에 virtual 함수가 있으면 부모마다 vtable이 생겨 오버헤드라는 이야기 - 이다.
나는 소멸자를 virtual로 만들지 않고 상속받을때의 무시무시함을 겪은적이 있다. 그래서 소멸자는 항상 virtual - 이것이 상속받을이유가 없는 클래스라도 - 로 하곤 했었는데, 그럴때마다 vtable을 만들거라 생각하니 찝찝했다. 그렇다고 상속을 허용하면서 소멸자를 virtual로 안하는것은 정말 귀신피하려다 호랑이 만나는꼴 당할수 있는일이라, 상속을 못받게 하는방법을 찾아보았다.
몇가지 좋은정보들을 찾았는데, 전부 비얀할아버지가 작성한것을 기반으로 하고 있고 응용버전으로 birdkr님의 템플렛버전이 있었다. 그런데 gcc에서는 friend T는 안된다. - 사실, friend T는 표준 C++ 문법이 아니다.
C++에서 C# 형태의 스트링 포메팅을 지원하는 로거를 만들다보니 정규표현식을 지원하는 라이브러리가 필요하게 되었다. 유명한 boost 에 정규표현식 지원 라이브러리가 있었지만 여러프로젝트의 기반이 되는 프로젝트에 포함시킬 생각이어서 코드상태로 프로젝트에 포함될정도 작아야했다.
sf에서 뒤지다가 딱 필요한 정도의 기능만 구현되어 있는 t-rex라는 라이브러리를 발견했다. zlib 라이센스에 파일도 헤더파일, cpp파일 2개만 있으면 쓸수 있을정도로 작았고 사용방법도 아주 간단했다.
세미콜론에 비교적 안정적인 path splitter(앞뒤로 세미콜론이 있을수 있고, 세미콜론사이에 여러개의 세미콜론이 들어가도 분리가능하고 맨 끝은 세미콜론이 없어도 나누어줄수 있는) 를 요 라이브러리로 구현하면 다음과 같다.
C++ 템플릿 특화중에 특이한 사실중에 하나는, Inner Class의 완전특화가 C++표준이 아니라는것이다. 그래서 Inner Class를 완전특화에서 사용하면 gcc에서는 컴파일이 안된다.
회사에서 매주 월요일 점심시간 짜투리에 CodeDojo 라는 이름으로 Programming Challenges 책의 문제를 푼다.(하긴, 이것도 월요일날 학교를 가게되어서 앞으로는 하지 못할것 같다.) 문제들은 전형적인 ACM 문제유형으로 인풋이 주어지면 문제를 해결한 적절한 아웃풋을 만들어내야하는 식이다.
참여횟수가 늘어나면 인풋을 적절히 일반화를 해보고자 하여 템플릿을 사용하게 되었는데, 이를테면 인풋의 값을 string 형태로 보관한다거나 혹은 int 형태로 보관할수 있게 만들고자했다. 여기에 보통 '0 0'를 입력하면 입력을 종료한다는 조건이 붙는데 stirng형이면 zero가 char형의 '0'가 될 것이고 int 형이면 zero가 int형의 0이 되도록 하기 위해 템플릿 완전(명시적) 특화를 사용하려 하였다.
그런데 VS에서 멀쩡히 컴파일되던 녀석이 테스트 봇 - gcc를 사용한다 - 에서 컴파일이 안되어서 여기저기 알아보니 클래스 내의 Inner Class는 템플릿의 명시적 특화가 '확장이 아닌 표준을 따르면' 지원되지 않는덴다. 그래서 꼼수로 생각한게 부분특화를 사용하고 템플릿 인자에 기본인자를 사용하였더니 gcc나 VS에서 문제없이 컴파일이 되었다.
회사에서 네트워크 관련 라이브러리에서 CRTP를 활용할 수 있는 예가 있어서 만들어봤던 코드인데, 회사내의 정책이 바뀌어서 쓸모가 없어진 비운의 코드이다. 자랑할겸(-_-) 소스정리 해보았다. CRTP에 대해서는 예전글 참조
네트워크나 스크립트 엔진등에서 함수를 문자열을 통해 호출하는 경우가 많다. 예를들면
이런식으로 callFunc라는 메쏘드를 통해 "key"와 연결된 함수를 호출한다. 이렇게 하기 위해 일반적으로 두가지 방법을 사용한다. "key"에 연결된것이 static member 함수여서 map 자료형에 함수포인터를 담아놓거나 혹은 callFunc자체를 virtual로 두고 자식클래스 단에서 그것을 구현하는 경우다. 전자의 경우는 널리 사용되는 방법인데 멤버에 대한 접근성이 많이 부족하고 후자의 경우는 자식클래스단에서의 구현 알고리즘이 똑같은데 - 문자열에 대응하는 멤버함수를 연결 - 멤버함수마다 자료형이 달라서 어쩔수 없이 자식클래스 마다 따로 구현해야되는 문제때문에 많이 쓰이지는 않는다.
후자의 문제를 해결하기 위해 접근했던 방법이, 부모가 알고리즘을 가지고 자식의 자료형을 부모가 알고 있게 하면 어떻게 할수 있지 않을까 였고, 부모가 자식의 자료형을 알고 있게 하기 위해 CRTP를 사용하였다.
구현 소스를 보면 일단 호출 인터페이스가 있고
그것을 상속받은 CRTP의 중간객체가 존재한다.
몇가지 신기한 자료형이 나오는데, 메범함수 포인터 자료형은( (static_cast<T*>(this)->*(itr->second))(); ) C++의 재미있는 자료형중 하나이다. 뭐랄까.. 좀 심오한 자료형인데,
알렉산더레스쿠(-_- 맞나..)의 Modern C++ Design에 그에 대한 자세한 설명이 있다.(일반화된 Command
Functor 설명부분에 있었던듯 하다.)
그리고 본격적으로 상속받아 기능을 구현하는 클래스들이 있고 그 클래스들을 활용하는 메인루프가 있을것이다.
Effective STL에서 봤던건데, 구간 insert나 assign이 더 좋게 구현이 가능하기 때문에 그걸 즐겨쓴다. vector에서 assign은 STL 튜토리얼/레퍼런스 가이드에서 나와있듯이 { clear(); insert(begin(), first, last);}와 같다. (Visual Studio 2005에서는 {erase(begin(), end()); insert(begin(), first, last);} 형태로 구현되어 있다.)
쉽게 말해 assign 할때는 clear가 필요 없다.
한가지더. vector는 operator= 가 재정의 되어있다. 즉, a = b (a와 b는 벡터) 라고 해도 된다. 다만, Visual Studio에서의 operator=는 메모리크기 재조정이 들어가서 약간의 오버헤드가 들어간다.
개인적으로 연산자 오버로딩을 무지 싫어하나 이것들이 잘 구현된 라이브러리들은... 역시 편하다-_- end의 행렬클래스도 연산자 오버로딩을 하려고, 다른 행렬클래스들을 살펴보았는데, 허걱.=이 붙은 계열의 오버로딩은 모두 참조를 리턴하는게 아니던가! 궁금하던 차에 사수격인 회사동료분께 물어봤더니, 아주 명쾌한 해설을 해주셨다.
operator=를 오버로딩해서 호출이 되면, lhs = rhs 꼴은 lhs.operator=(rhs) 형태로 된다. 만일 a=b=c를 수행한다고 하면, a.operator=( (b.opeator=(c) ) 가 된다. 그런데, a=b=c 는 Primative Type을 기준으로 a는 b와 같은 값이 되어야 하고 그렇게 되기 위해서 b.operator=(c)는 b를 참조형으로 던져야 한다. 또한 a = a (a=b)=c일 경우도 있을수 있으므로(멍청하게 저렇게 쓰지는 않겠지만, 참조형으로 엮여서 저렇게 된다는지) const를 붙이지 않는 참조형으로 반환해야 한다.
라이브러리를 만든다는것은 정말 쓸모 없는 재발명일수도 있지만, 배우는 입장에서는 무척이나 좋은 경험같다.
템플렛의 인자에는 3가지 형태가 있다.
type, non-type, template
type은 흔히 typename T (혹은 class T)로 쓰는것이고, non-type은 컴파일타임시에 알수있는 상수형 값을, 그리고 template은 바로 밑에 포스팅한 TTP를 의미한다.
아래의 예를 보면 non-type형의 템플렛 인자가 어떤것인지 명확히 알수 있다.
비 형식 템플릿 인자는 정수형인 int 와 long을 인자로 할수 있으며, float이나 double은 인자로 사용할수 없다.
이 template non-type argument를 이용한 재미있는 예가 kldp에 있다.