정보공간_1

[2기 광주 조영진] C++ Template기초와 MetaProgramming 본문

IT 놀이터/Elite Member Tech & Talk

[2기 광주 조영진] C++ Template기초와 MetaProgramming

알 수 없는 사용자 2012. 8. 18. 06:28

안녕하세요.

광주멤버십 21-1기 조영진입니다. 반갑습니다.

제가 소개할 내용은 이번에 안드로이드 프레임워크 분석을 위해 

C++ 교육을 들으면서 배운 내용의 일부인 template에 대한 내용입니다.


1. template이란?

객체지향이라는 개념에 대해서 처음 배우실 때 "클래스는 붕어빵 틀이고 객체는 붕어빵이다."

라는 문구를 보신 적이 있으실텐데요~

template도 마찬가지로 붕어빵 틀인데 이 붕어빵 틀에서 만들어지는 붕어빵은 객체가 아니라 "코드" 입니다.

좀 더 자세한 예를 들면서 설명해 볼게요.

C언어로 개발을 하다가 두 정수를 하나로 더해주는 일을 해야 했다고 생각해 보죠.

그런데 이 일을 프로그램 내에서 굉장히 많이 한다고 생각해 보면 많은 사람들이

아! 함수로 쓰면 되겠구나 라고 생각을 하실겁니다. 안하시면.. 뭐 어쩔수 없죠 ㅜㅠ

함수로 만들었다는 가정 하에.. 그런데 처음에는 정수에 대해서만 더하기를 할 줄 알았는데

개발을 진행하다보니 실수에 대한 더하기도 필요하게 되었네요?

자 그럼 여기서 개발자가 선택할 수 있는 여지는?

제가 생각하기엔 두가지가 떠오르네요.

첫번째 "어차피 정수는 실수에 포함될 수 있으니까 그냥 실수버전만 사용하자."

두번째 "아니야 정수연산은 실수연산보다 빠르니까 정수가 필요한 부분에선 정수연산만 하는게 좋아.

귀찮지만 정수버전과 실수버전 함수를 따로만들자..ㅠㅠ"

여러분의 선택은 무엇인가요?

저는 여기서 두번째를 선택할 겁니다. 그래야 계속해서 설명을 할 수 있으니까요.

자 위에서 간단해서 언급은 따로 안했지만 두 정수를 더하는 함수와 두 실수를 더하는 함수는

사실 잘 살펴 보실 필요도 없이 그냥 보기만 해도

리턴 타입과 함수 인자의 타입을 제외하고는 생긴게 똑같습니다!!

결국 똑같은 코드를 컨트롤 C, V를 하거나 다시 입력하거나.. 아무튼 중복되는 코드를 작성해야 했었습니다.

그래서 중복되는 코드를 반복해서 작성하지 않도록 다양한 방법을 사용했었는데요.

과거 C언어에서는 다음과 같은 방법으로 동일한 코드를 생성해주는 방법을 사용했었습니다.

위 매크로에서 사용된 ##은 두개의 문자열을 이어주는 역할을 하는 키워드 입니다.

MAKE_FUNC(int)라는 매크로는 결국

int addint(int a, int b) { return a + b; } 라는 코드로 변환이 되고

MAKE_FUNC(double)라는 매크로는 

double adddouble(double a, double b) { return a + b; } 로 변환이 되는 것입니다.

add라는 함수를 만드는데 사용할 타입만 정해주면 

자동으로 나머지 부분을 채워주는 일종의 붕어빵 틀을 만든것이죠.

C언어에서는 동일한 이름의 함수를 중복해서 정의할 수 없기 때문에 add 뒤에 type까지 적어서

서로 다른이름의 함수로 만들었지만 C++에서는 함수의 이름이 같더라도 

인자의 개수나 타입이 다르면 서로 다른 함수로 생각해주는 함수 오버로딩이라는 기능이 추가되면서

위에서 사용한 코드생성기가 더욱 간단해지고 유연해지기 시작합니다.

위와 같이 동일한 이름의 함수를 사용할 수 있게 된 것이죠.

하지만 아직까지 각각의 add가 어떤 타입에 대해서 필요한지를 MAKE_FUNC라는 매크로를 통해서

명시적으로 컴파일러에게 알려주어야 했습니다.

여기서 더 나아간 형태가 template이라고 생각하시면 됩니다.

개발자가 명시적으로 타입을 적어주지 않더라도 컴파일러가 해당하는 함수를 호출하는 시점에서

함수에 필요한 타입을 자동으로 찾아서 코드를 생성해 주는 기능 이 기능이 바로 template 입니다.

위의 add 함수를 template 버전으로 변경하면 아래와 같이 됩니다.

이제 컴파일러가 add 함수를 호출하는 시점에서 넘어오는 인자들의 타입을 보고 위의 MAKE_FUNC와 같이

각각의 타입에 대해서 함수를 자동적으로 생성해서 오브젝트 파일에 추가를 해 주게 됩니다.

위쪽의 add는 인자가 int형이기 때문에 int add(int a, int b) 라는 함수를 생성하게 되고

아래쪽의 add는 인자가 double형이기 때문에 double add(double a, double b) 라는 함수를 생성하게 됩니다.

하지만 가장 처음에 살펴본 MAKE_FUNC와 같이 사용할 add 함수의 인자를 저희가 직접 정해주지 않아도

똑똑한 컴파일러가 알아서 아 이때 int형으로 사용했구나 라고 코드를 보고 분석해서 자동적으로

int형과 double형 함수에 대한 "코드"를 생성해 줍니다.


2. Template Specialization(템플릿 전문화)

대부분의 경우에는 + 연산자를 통해서 정상적인 결과를 도출해 낼 수 있지만

인자가 char* 인 경우에는 그냥 +만 했다가는 큰일나죠^^;;

물론! string을 쓰시면 그냥 + 하시면 됩니다만.. 이번에도 설명을 위해서!

배열로 표현된 문자들을 더하기 위해서는 내부적으로 문자들을 더해서 새로운 문자열을 만들어야 합니다.

이 때 사용하는 기능이 template specialization(템플릿 전문화) 라고 하는 기능입니다.

위와 같이 char *에 대해서는 별도로 처리하도록 컴파일러에게 요청을 해 주어야 합니다.

사실 똑똑한 컴파일러라고 표현은 했지만 컴파일러는 해준 말만 알아먹는 바보나 다름없거든요^^;;


3. 타입 유추

위에서 사용한 예제들은 모두 반환 타입과 인자 2개의 타입을 모두 T라고 명시를 해 주었기 때문에

이 template에서 생성할 수 있는 함수는 반환 타입과 인자들의 타입이 모두 같은 경우에만 생성할 수 있습니다.

위와 같은 경우에는 리턴 타입은 double이고 첫번째 인자는 char형, 2번째 인자는 double형으로 되어 있어서

3개의 타입이 동일하지 않기 때문에 컴파일에러가 발생하게 됩니다.

여기서 사실 논리적으로는 'a'는 정수 97과 동일하고 2.4 + 97 = 99.4 가 나오면 문제 없는 코드이긴 한데

사실 저 의도로 저런 코드를 작성했는지 아닌지는 개발자는 알지만 컴파일러는 몰라요~

그래서 다짜고짜 에러를 뱉어내는 것이랍니다.

서로 타입이 다른 경우에도 연산이 가능한 경우라면 연산을 하고 싶다! 라고 컴파일러에게 알려줘야 합니다.

위와 같이 하면 2가지의 타입에 대해서 매치가 되도록 컴파일러는 함수를 생성해 줍니다.

하지만 코드를 자세히 보시면 리턴 타입은 첫 번째 인자의 타입과 같게 되어 있습니다.

따라서 함수를 위와 같이 정의하시면 항상 첫번째 인자에 double 형이 와야만 제대로 된 결과가 나옵니다.

이러한 문제점을 해결하기 위해 현재 Visual Studio 2010과 GCC 4.7 버전에서 일부분만 지원하고 있는

C++ 의 최신 표준인 C++11 에서는 auto와 decltype이라는 두가지 키워드를 제공합니다.

auto는 컴파일 시점에 우변에 오는 수식의 결과를 토대로 좌변의 타입을 유추해는 키워드이고

decltype은 확정되어있지만 눈에 보이지 않는 타입을 외부로 표출시켜주는 키워드 입니다.

다음 예를 보시면 좀 더 확실하게 이해가 되실 것 같습니다.

위에서 사용된 typeid는 template의 인자로 넘어오는 타입의 정보를 유추해 낼 수 있는 키워드 입니다.

main에서 double과 int라는 키워드는 전혀 사용하지 않았지만 3.14와 3이라는 데이터를 통해서

타입을 유추해 내기 위한 키워드가 auto 이고 이렇게 확정되어있는 타입을 

다시한번 선언에 사용할 수 있도록 해주는 키워드가 decltype 입니다.

이 두가지 키워드를 통해서 앞에서 나온 문제점을 해결할 수 있습니다.

위의 소스에서 보시면 add라는 템플릿 함수의 리턴 타입을 auto라고 지정해 두고

뒤쪽에서 a + b의 결과의 타입을 사용해 달라 라는 의미로 선언되어 있습니다.

리턴 타입에 auto만 적게되면 auto는 우변의 결과를 통해서 타입을 결정하는데 우변이 없기 때문에

에러가 발생하게 되고 declytype(a + b) 를 리턴 타입을 명시하는 부분에 적게 되면 a와 b가 코드상으로

리턴 타입을 적는 부분보다 뒤쪽에 있기 때문에 컴파일러가 찾지 못하는 오류가 발생합니다.

따라서 리턴 타입을 auto로 해 두고 이 auto의 타입을 유추할 수 있도록 해주는 

decltype 부분을 뒤쪽에 적어주게 되는 것 입니다.

혹시 Visual Studio 2010에서 -> 뒷부분을 제외하고 컴파일을 시도해 보시면 "후행 반환 형식이 필요합니다."

라는 에러 메시지를 보실 수 있습니다.


4. Meta Programming Binary

C++에서는 정수를 3가지 방법으로 사용할 수 있습니다.

출력 결과는 셋 모두 15라는 결과가 나옵니다.

정수형 상수 앞에 0이 붙어있으면 해당 정수는 8진수로 인식되고

정수형 상수 앞에 0x가 붙어있으면 해당 정수는 16진수로 인식됩니다.

하지만 정수형 상수를 2진수로 인식하도록 하는 방법은 현재 C++에서는 제공되고 있지 않죠.

2번에서 언급한 template specialization을 통해서 정수형 상수를 2진수로 인식하도록 해 보겠습니다.

먼저 정수형 상수가 들어오면 해당 수를 2진수로 변환하기 위해서 다음과 같은 template을 정의합니다.

위 template에 101이라는 정수가 전달된다고 하면 위 template은 컴파일 타임에 다음과 같이 해석이 됩니다.

즉, 일종의 재귀함수처럼 해석이 되는데 종료 조건이 없으므로 무한루프에 빠진 재귀함수와 비슷한 형태입니다.

따라서 이 template이 컴파일 타임에 정상적으로 해석될 수 있도록 종료조건을 명시해 주기 위해

template specialization을 사용하게 됩니다.

위와 같이 N이 0일때 VALUE를 0으로 지정해주면 위의 무한 재귀적인 형태에서

위와 같은 형태로 바뀌게 됩니다.

실제 사용시에는 다음과 같이 사용하면 됩니다.

위와 같이 컴파일 타임에 컴파일러가 코드를 읽어서 원하는 기능으로 코드를 변화시키는 기법을

메타 프로그래밍이라고 합니다.

메타 프로그래밍의 장점은 모든 번역이 컴파일 타임에 이루어 지기 때문에 런타임에 한줄씩 명령어를 해석해서

처리하는 방법보다 실행시간에 있어서 월등히 빨라요 모든 계산이 컴파일시점에 끝나 있기 때문에... 

마찬가지로 컴파일 시점에 모든 일을 하기 때문에 실행중에 추가적인 메모리가 필요하지 않다는 점도 

장점이라고 볼 수 있겠네요!

그 반대로 단점은 컴파일 시간이 길어진다는 점과 생성된 목적코드의 크기가 굉장히 커진다는 점 

분명히 C++임에도 왠지 다른언어처럼 보이는 복잡함과 변수에는 사용할 수 없다는 점 

이러한 부분들이 단점이라고 볼 수 있습니다.

메타 프로그래밍에서 이용할 수 있는 자료를 메타데이터라고 하는데 현재 C++에서 사용할 수 있는

메타데이터는 타입과 상수 2가지가 있습니다.

기본적인 메타 프로그래밍에 대한 소개는 여기서 마무리 하도록 하고 

위에서 구현한 binary의 단점을 보완해 보도록 하겠습니다.


5. 컴파일 타임 조건문

위에서 구현한 binary는 한가지 문제점이 있는데요, 여기서 사용자가 1과 0만을 입력하리라는 보장이 없습니다.

606 이런식으로 입력하면 뭐 물론 결과는 나오지만 

그렇게 되면 저희가 고민에 고민을 거듭해서 이름을 binary라고 지은 의미가 없잖아요?!

따라서 1과 0이 아닌 다른 숫자가 입력되었을때는 

컴파일이 되지 않도록 에러가 출력되면 좋을 것 같지 않으세요?

이럴 때 사용할 수 있는 방법은 몇가지가 있는데요.

그 중 2가지를 소개해 드리겠습니다.

먼저 비슷하게 template specialization을 사용하는 방법이 있습니다.

위와 같은 template struct를 추가하고

binary에서 다음과 같이 사용하면 됩니다.

위와 같이 하시면 N % 10이 1 또는 0 인 경우에만 ZeroOrOne의 VALUE라는 정적 상수 멤버가 존재하기 때문에

정상적으로 컴파일이 되어 값이 계산되는 반면 그 외의 정수에는 ZeroOrOne의 VALUE라는 정적 상수 멤버가

존재하기 않기 때문에 컴파일시에 에러가 발생하게 됩니다.

하지만 이 경우에는 어떤 이유때문에 에러가 발생했는지 명확하게 나오지 않고 멤버가 존재하지 않는다는

에러 메시지가 나오기 때문에 사용자 입장에서는 어떤 부분을 실수했는지 알기가 어렵습니다.

그래서 C++11에서는 컴파일 타임에 해당 조건을 만족하는지 검사할 수 있는 

static_assert라는 키워드가 추가되었습니다.

현재 Visual Studio 2010 버전에서도 사용해 볼 수 있습니다.

binary를 다음과 같이 정의 하시면 됩니다.

binary를 위와 같이 정의해 두고 실제 사용할 때

왼쪽과 같이 사용하면 오른쪽과 같은 에러 메시지가 나오게 됩니다.

현재 Visual Studio 2010과 2012RC 버전에서는 에러 메시지에 한글을 적을 경우 정상적으로 출력되지 않습니다.

9월에 출시되는 2012 정식버전에서는 한글도 정상적으로 지원되기를 기대해 보면서 

이번 포스팅을 마치도록 하겠습니다. 

감사합니다.


6. 참고자료

C++ Template MetaProgramming / DAVID ABRAHAMS 저 / 류광 역

Modern C++ Design / Andrei Alexandreescu 저 / 이기형 역

http://cafe.naver.com/cppmaster 강석민 강사님 카페