언젠가 개발자 3명이 모인 술자리에서 C와 C++의 이야기가 나온적이 있다... 물론 나는 그러한 생산없는 토론을 별로 좋아하지 않지만 은연중에 이야기가 그리로 흘러간 것이다. 나또한 C를 C++보다 많은 기간 다루었고, C의 문법이 간결하고 단순해서 좋아한다. C++은 자체적으로 아주 덩치가 크고 복잡하고 모호한 것들을 많이 포함한 언어라고 생각한다.
각설하고, 그 때 난 템플릿이 C++의 장점이 될 수 있음을 말했고, 같이 이야기하던 분은 템플릿을 PVOID의 변형 정도로 생각하고 있었다... 그리하여... 난 pvoid.com을 접수하기 위해서 이 글을 쓴다.... ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ~~~
1. PVOID
PVOID 란 void 포인터, 즉 void *을 말한다. 이 포인터는 C언어에서는 굉장히 막강한 능력을 가지고 있다. 어떠한 능력인고 하니 모든 다른 포인터를 가리킬 수 있으며, 다른 모든 포인터로 자동적으로 캐스팅이 이루어진다. 실로 대단하다 하지 않을 수 없다. 따라서 C언어에서 일반화 코드 내지는 자료구조의 코드등에 약방감초처럼 등장하는 놈이 PVOID라 할 수 있겠다.
그럼 간단히 PVOID의 능력을 살펴보고 넘어가도록 하자.
int a = 3;
PVOID pv = &a;
C언어에서는 매우 합법적인 코드다. pv는 모든 포인터를 가리킬 수 있으므로, 당연히 int의 포인터도 가리킬 수 있는 것이다.
int \*pi = pv;
이또한 아주 합법적인 코드다. C언어에서 PVOID는 다른 포인터로 자동적으로 승격되기 때문이다.
여기서 중요한 코드를 한번 살펴 보도록 하자.
int a = 3;
int \*pi = &a;
PVOID pv = &a;
자 위와 같은 코드가 있다고 했을때 우리가 pi의 값을 읽기 위해서는 역참조 연산자 *를 사용해서 *pi로 읽게된다. 그러면 a에 저장되어 있는 값인 3이 나오게 되는 것이다. 그렇다면 PVOID를 역참조하기 위해서 *pv라고 하면 어떻게 될까? 그러면 컴파일에러가 발생하게 된다. 왜일까? 왜 역참조를 하지 못할까?
+---+---+---+---+
| | | | |
+---+---+---+---+
일반적인 32비트 컴퓨터에서 int는 위와 같은 4바이트로 이루어지게 된다. 여기서 int의 포인터라는 것이 가지고 있는 정보는 두가지가 되게 된다. 위의 메모리가 시작되는 주소와 함께, 그곳부터 4바이트를 읽으면 된다는 것이다.
A B C
+---+---+---+---+ +---+---+---+---+ +---+---+---+---+
|FA |0C |2B |34 | |FA |0C |2B |34 | |FA |0C |2B |34 |
+---+---+---+---+ +---+---+---+---+ +---+---+---+---+
편 의상 메모리에 데이터가 위와 같이 저장되어 있다고 가정해 보자. A,B,C는 각각 주소를 나타낸다. int의 포인터가 A라는 위치를 가리킬때, 해당 데이터를 읽게되면 그 위치부터의 4바이트를 읽어서 FA 0C 2B 34라는 값이 나오게 된다. 그렇다면 해당 포인터에 1을 더하면 어떻게 될까... ?? 그러면 해당 포인터는 B지점으로 이동하게 된다. 왜냐하면 int가 가리키는 놈은 4바이트라는 것을 알고 있으므로 다음 데이터를 읽기 위해서 4바이트 뒤로 이동하는 것이다.
여기서 우리는 PVOID가 역참조를 하지 못하는 이유를 알 수 있다. PVOID는 모든 것을 가리킬 수 있다는 사실에서... 그 놈이 가리키는 놈의 크기를 알지 못하는 것이다. 즉, 다른 포인터는 모두 번지와 함께 대상체의 크기 정보를 가지고 있는 반면에 PVOID는 대상체의 크기 정보는 없이 단순히 번지만 가지고 있는 것이다. 따라서 PVOID는 포인터의 역참조 연산을 비롯해 +,-연산도 이루어질 수 없는 것이다.
그러므로 또한 우리가 코드상에서 PVOID를 통해서 일반화를 시킬 경우는 항상 크기를 같이 저장하거나, 아니면 포인터를 읽고/쓰는 부분을 콜백으로 처리하는 등의 형태를 가지게 된다. 즉, 다음과 같은 swap함수의 원형이 나올 수 없다는 이야기가된다.
void swap(PVOID pa, PVOID pb);
PVOID만으로는 절대로 swap을 할 수가 없다... 왜냐하면 크기 정보가 없기 때문이다. 물론 프로그래머가 PVOID가 가지고 오는 포인터가 무조건 int의 포인터라고 가정하면 저런 원형도 나올 수 있다.
void swap(PVOID pa, PVOID pb)
{
int \*pc = pa;
int \*pd = pb;
...
}
하 지만 위의 코드의 swap함수는 일반화가 안된 그저 int값만 교환하는 swap함수와 다를 바가 없게 되는 것이다. 따라서 C에서 일반화된 코드를 작성하기 위해서 PVOID를 선택한 경우는 그 사이즈또한 같이 인수로 받아야 한다.
void swap(PVOID pa, PVOID pb, size\_t size);
이제야 비로서 사이즈에 맞는 swap을 할 수 있는 함수가 된 것이다.
이번 장에서 가장 중요하게 기억해야 할 점은 PVOID는 모든 포인터를 가리킬 수 있다는 점, 다른 포인터로 자동 승격된다는 점. 그리고 PVOID내에는 크기 정보가 없다는 점이다.
2. 템플릿
그 럼 이번에는 템플릿을 한번 짚고 넘어가 보도록 하자... 템플릿 또한 일반화 프로그램의 대명사와 같은 놈이므로 앞장에 나온 PVOID와 비슷하다고 생각할 수 있다. 하지만 일반화 코드를 만들때 사용한다는 점 왜에는 전혀 똑같이 않다. 실제로 비교 대상도 아니고, 템플릿의 내부 구현이 PVOID와 상관이 있지도 않다... 그럼 이제 템플릿을 한번 살펴보자...
템플릿의 기본 아이디어는 코드를 상세화 하지 않고 그 형틀만 만든다는 것이다. 즉, 형틀만 만들어 두고 컴파일하는 시점에 작은것이 필요하면 형틀에서 작은 놈을 찍어내고, 큰것이 필요하면 형틀에서 큰것을 찍어내는 구조와 같다.
그럼 앞장에서 보였던 swap함수를 이번에는 템플릿으로 한번 작성해 보자...
template<class T>
void swap(T &a, T &b)
{
T c;
c = a;
a = b;
b = c;
}
위가 템플릿의 swap함수이다... 여기서 변화 가능한 부분이 무엇인고 하면... 바로 T가 된다... 형틀에서 나머지는 모두 고정되어 있되... T만 바뀔 수 있는 것이다...
int a, b;
swap(a,b);
위와 같이 호출했다고 해보자. 그럼 컴파일러는 생각을 하게 된다... 음 요놈이 int형으로 호출했네... 그럼 내가 int형 함수를 하나 만들어야 겠군... 하고는
void swap(int &a, int &b)
{
int c;
c = a;
a = b;
b = c;
}
위 함수를 만들게 된다.
이번에는 아래와 같이 호출했다고 해보자...
double a,b;
swap(a,b);
그럼 컴파일러는 ... 어? double 버전도 필요한가 보네... 하고는 함수를 하나 더 만들게 된다.
void swap(double &a, double &b)
{
double c;
c = a;
a = b;
b = c;
}
자... 그럼 결국 함수는 두개가 된 셈이다. 프로그래머는 함수를 하나 만들었지만 컴파일타임에 컴파일러가 보고 함수를 필요한 만큼 찍어내는 것이 템플릿의 기본 전략인 셈이다...
이번 장에서 기억해야 할 점은 세가지다.
-
C++에서 일반화 프로그래밍에 템플릿을 사용할 수 있다.
-
템플릿은 PVOID와 관련이 없다.
-
템플릿의 구현의 핵심은 컴파일 타임에 필요한 만큼 함수를 생성하는데 있다.
3. PVOID와 템플릿...
이번 장에서는 실제 어셈블된 코드를 통해서 어떠한 방식의 접근 법이 어떠한 장점과 단점을 가지는지 알아 보도록 하자.
먼저 아래코드는 C언어 버전이다. 편리한대로 PVOID를 void*로 작성하였다. 또한 어셈블해서 코드를 살펴 볼 것이므로 최대한 간단하게 작성했다. 모든 에러와 예외는 없다는 가정하에서 작성되었다는 점을 기억해야 한다.
#include <malloc.h>
#include <memory.h>
void swap(void \*pa, void \*pb, size\_t size)
{
void \*pm = malloc(size);
memcpy(pm, pa, size);
memcpy(pa, pb, size);
memcpy(pb, pm, size);
free(pm);
}
int main()
{
int a=1, b=2;
double c=3, d=4;
swap(&a, &b, sizeof a);
swap(&c, &d, sizeof c);
return 0;
}
위 코드를 어셈블한 리스팅은 아래와 같다.
PUBLIC \_swap
EXTRN \_free:NEAR
EXTRN \_malloc:NEAR
EXTRN \_memcpy:NEAR
\_TEXT SEGMENT
\_pa$ = 8
\_pb$ = 12
\_size$ = 16
\_pm$ = -4
\_swap PROC NEAR
; 5 : {
00000 55 push ebp
00001 8b ec mov ebp, esp
00003 51 push ecx
; 6 : void \*pm = malloc(size);
00004 8b 45 10 mov eax, DWORD PTR \_size$[ebp]
00007 50 push eax
00008 e8 00 00 00 00 call \_malloc
0000d 83 c4 04 add esp, 4
00010 89 45 fc mov DWORD PTR \_pm$[ebp], eax
; 7 :
; 8 : memcpy(pm, pa, size);
00013 8b 4d 10 mov ecx, DWORD PTR \_size$[ebp]
00016 51 push ecx
00017 8b 55 08 mov edx, DWORD PTR \_pa$[ebp]
0001a 52 push edx
0001b 8b 45 fc mov eax, DWORD PTR \_pm$[ebp]
0001e 50 push eax
0001f e8 00 00 00 00 call \_memcpy
00024 83 c4 0c add esp, 12 ; 0000000cH
; 9 : memcpy(pa, pb, size);
00027 8b 4d 10 mov ecx, DWORD PTR \_size$[ebp]
0002a 51 push ecx
0002b 8b 55 0c mov edx, DWORD PTR \_pb$[ebp]
0002e 52 push edx
0002f 8b 45 08 mov eax, DWORD PTR \_pa$[ebp]
00032 50 push eax
00033 e8 00 00 00 00 call \_memcpy
00038 83 c4 0c add esp, 12 ; 0000000cH
; 10 : memcpy(pb, pm, size);
0003b 8b 4d 10 mov ecx, DWORD PTR \_size$[ebp]
0003e 51 push ecx
0003f 8b 55 fc mov edx, DWORD PTR \_pm$[ebp]
00042 52 push edx
00043 8b 45 0c mov eax, DWORD PTR \_pb$[ebp]
00046 50 push eax
00047 e8 00 00 00 00 call \_memcpy
0004c 83 c4 0c add esp, 12 ; 0000000cH
; 11 :
; 12 : free(pm);
0004f 8b 4d fc mov ecx, DWORD PTR \_pm$[ebp]
00052 51 push ecx
00053 e8 00 00 00 00 call \_free
00058 83 c4 04 add esp, 4
; 13 : }
0005b 8b e5 mov esp, ebp
0005d 5d pop ebp
0005e c3 ret 0
\_swap ENDP
\_TEXT ENDS
PUBLIC \_main
EXTRN \_\_fltused:NEAR
\_TEXT SEGMENT
\_a$ = -4
\_b$ = -8
\_c$ = -16
\_d$ = -24
\_main PROC NEAR
; 16 : {
0005f 55 push ebp
00060 8b ec mov ebp, esp
00062 83 ec 18 sub esp, 24 ; 00000018H
; 17 : int a=1, b=2;
00065 c7 45 fc 01 00
00 00 mov DWORD PTR \_a$[ebp], 1
0006c c7 45 f8 02 00
00 00 mov DWORD PTR \_b$[ebp], 2
; 18 : double c=3, d=4;
00073 c7 45 f0 00 00
00 00 mov DWORD PTR \_c$[ebp], 0
0007a c7 45 f4 00 00
08 40 mov DWORD PTR \_c$[ebp+4], 1074266112 ; 40080000H
00081 c7 45 e8 00 00
00 00 mov DWORD PTR \_d$[ebp], 0
00088 c7 45 ec 00 00
10 40 mov DWORD PTR \_d$[ebp+4], 1074790400 ; 40100000H
; 19 :
; 20 : swap(&a, &b, sizeof a);
0008f 6a 04 push 4
00091 8d 45 f8 lea eax, DWORD PTR \_b$[ebp]
00094 50 push eax
00095 8d 4d fc lea ecx, DWORD PTR \_a$[ebp]
00098 51 push ecx
00099 e8 00 00 00 00 call \_swap
0009e 83 c4 0c add esp, 12 ; 0000000cH
; 21 : swap(&c, &d, sizeof c);
000a1 6a 08 push 8
000a3 8d 55 e8 lea edx, DWORD PTR \_d$[ebp]
000a6 52 push edx
000a7 8d 45 f0 lea eax, DWORD PTR \_c$[ebp]
000aa 50 push eax
000ab e8 00 00 00 00 call \_swap
000b0 83 c4 0c add esp, 12 ; 0000000cH
; 22 : return 0;
000b3 33 c0 xor eax, eax
; 23 : }
000b5 8b e5 mov esp, ebp
000b7 5d pop ebp
000b8 c3 ret 0
\_main ENDP
\_TEXT ENDS
END
너무나 사실 그대로의 코드이기에 별로 살펴 볼 필요가 없다.
이번에는 템플릿 버전이다.
template<class T>
void swap(T &a, T &b)
{
T c;
c = a;
a = b;
b = c;
}
int main()
{
int a=1, b=2;
double c=3, d=4;
swap(a, b);
swap(c, d);
return 0;
}
마찬가지의 어셈블 리스팅이다.
\_TEXT SEGMENT
\_a$ = -4
\_b$ = -8
\_c$ = -16
\_d$ = -24
\_main PROC NEAR
; 19 : {
00000 55 push ebp
00001 8b ec mov ebp, esp
00003 83 ec 18 sub esp, 24 ; 00000018H
; 20 : int a=1, b=2;
00006 c7 45 fc 01 00
00 00 mov DWORD PTR \_a$[ebp], 1
0000d c7 45 f8 02 00
00 00 mov DWORD PTR \_b$[ebp], 2
; 21 : double c=3, d=4;
00014 c7 45 f0 00 00
00 00 mov DWORD PTR \_c$[ebp], 0
0001b c7 45 f4 00 00
08 40 mov DWORD PTR \_c$[ebp+4], 1074266112 ; 40080000H
00022 c7 45 e8 00 00
00 00 mov DWORD PTR \_d$[ebp], 0
00029 c7 45 ec 00 00
10 40 mov DWORD PTR \_d$[ebp+4], 1074790400 ; 40100000H
; 22 :
; 23 : swap(a, b);
00030 8d 45 f8 lea eax, DWORD PTR \_b$[ebp]
00033 50 push eax
00034 8d 4d fc lea ecx, DWORD PTR \_a$[ebp]
00037 51 push ecx
00038 e8 00 00 00 00 call ?swap@@YAXAAH0@Z ; swap
0003d 83 c4 08 add esp, 8
; 24 : swap(c, d);
00040 8d 55 e8 lea edx, DWORD PTR \_d$[ebp]
00043 52 push edx
00044 8d 45 f0 lea eax, DWORD PTR \_c$[ebp]
00047 50 push eax
00048 e8 00 00 00 00 call ?swap@@YAXAAN0@Z ; swap
0004d 83 c4 08 add esp, 8
; 25 :
; 26 : return 0;
00050 33 c0 xor eax, eax
; 27 : }
00052 8b e5 mov esp, ebp
00054 5d pop ebp
00055 c3 ret 0
\_main ENDP
\_TEXT ENDS
; COMDAT ?swap@@YAXAAH0@Z
\_TEXT SEGMENT
\_a$ = 8
\_b$ = 12
\_c$ = -4
?swap@@YAXAAH0@Z PROC NEAR ; swap, COMDAT
; 10 : {
00000 55 push ebp
00001 8b ec mov ebp, esp
00003 51 push ecx
; 11 : T c;
; 12 :
; 13 : c = a;
00004 8b 45 08 mov eax, DWORD PTR \_a$[ebp]
00007 8b 08 mov ecx, DWORD PTR [eax]
00009 89 4d fc mov DWORD PTR \_c$[ebp], ecx
; 14 : a = b;
0000c 8b 55 08 mov edx, DWORD PTR \_a$[ebp]
0000f 8b 45 0c mov eax, DWORD PTR \_b$[ebp]
00012 8b 08 mov ecx, DWORD PTR [eax]
00014 89 0a mov DWORD PTR [edx], ecx
; 15 : b = c;
00016 8b 55 0c mov edx, DWORD PTR \_b$[ebp]
00019 8b 45 fc mov eax, DWORD PTR \_c$[ebp]
0001c 89 02 mov DWORD PTR [edx], eax
; 16 : }
0001e 8b e5 mov esp, ebp
00020 5d pop ebp
00021 c3 ret 0
?swap@@YAXAAH0@Z ENDP ; swap
\_TEXT ENDS
; COMDAT ?swap@@YAXAAN0@Z
\_TEXT SEGMENT
\_a$ = 8
\_b$ = 12
\_c$ = -8
?swap@@YAXAAN0@Z PROC NEAR ; swap, COMDAT
; 10 : {
00000 55 push ebp
00001 8b ec mov ebp, esp
00003 83 ec 08 sub esp, 8
; 11 : T c;
; 12 :
; 13 : c = a;
00006 8b 45 08 mov eax, DWORD PTR \_a$[ebp]
00009 8b 08 mov ecx, DWORD PTR [eax]
0000b 89 4d f8 mov DWORD PTR \_c$[ebp], ecx
0000e 8b 50 04 mov edx, DWORD PTR [eax+4]
00011 89 55 fc mov DWORD PTR \_c$[ebp+4], edx
; 14 : a = b;
00014 8b 45 08 mov eax, DWORD PTR \_a$[ebp]
00017 8b 4d 0c mov ecx, DWORD PTR \_b$[ebp]
0001a 8b 11 mov edx, DWORD PTR [ecx]
0001c 89 10 mov DWORD PTR [eax], edx
0001e 8b 49 04 mov ecx, DWORD PTR [ecx+4]
00021 89 48 04 mov DWORD PTR [eax+4], ecx
; 15 : b = c;
00024 8b 55 0c mov edx, DWORD PTR \_b$[ebp]
00027 8b 45 f8 mov eax, DWORD PTR \_c$[ebp]
0002a 89 02 mov DWORD PTR [edx], eax
0002c 8b 4d fc mov ecx, DWORD PTR \_c$[ebp+4]
0002f 89 4a 04 mov DWORD PTR [edx+4], ecx
; 16 : }
00032 8b e5 mov esp, ebp
00034 5d pop ebp
00035 c3 ret 0
?swap@@YAXAAN0@Z ENDP ; swap
\_TEXT ENDS
END
보면 컴파일러는 ?swap@@YAXAAHO@Z라는 함수와 함께 ?swap@@YAXAANO@Z라 는 함수를 호출 하는 것을 볼 수 있다. 실제로 컴파일 타임에 필요에 의해서 두가지 버전의 함수를 만든 것이다. 전체 리스팅에서 아래로 가면 두가지 함수의 실제 코드가 따라 나오게 되는데, 스택 프레임 생성과 복사하는 크기 외에는 그렇게 큰 차이가 나지 않는다.
자... 여기서 우리는 결론을 내릴 수 있다. 템플릿의 구현은 PVOID와 전혀 상관이 없다. 단지 컴파일러가 컴파일 타임에 필요한 코드를 생성해 주는 것 뿐이다.
그럼 템플릿과 PVOID의 각기 장/단점을 알아보자.
PVOID로 만든 swap함수는 불행하게도 size가 실행시간에 결정되기 때문에, 힙을 사용할 수 밖에 없다. 하지만 함수 하나가 모든 자료형에 대해서 적용되므로 코드 크기는 작다고 할 수 있다.
템플릿 버전은 각각에 다른 버전의 함수가 만들어지기 때문에 크기가 커진다는 단점이 있다. 반면에 힙같은 외부 의존성이 낮기 때문에 빠르다고 할 수 있다.
결론을 정리하면 PVOID는 작고 느리다. 반면 템플릿은 크고 빠르다가 되겠다.
4. 무엇이 비교 대상인가?
그 러나 사실 위의 3장은 전혀 관계없는 두가지의 것을 비교하고 있다. PVOID와 템플릿. 이건 템플릿의 구현 과정에 PVOID가 전혀 관여하지 않는다는 사실을 보여주기 위해서 설명한 장들이었다. 3번 장을 읽고는 의문이 들었을 것이다. 템플릿이 크다고? 그렇다면 ATL/STL등에서 주장하는 작다는 말은 무엇인가?... 어떨때는 크다... 어떨때는 작다... 뭐야?????????????.... 같이 술마시던 분이 했던 말이다...
맞는 말이다. 비교 대상에 따라 틀린 것이다. 주어와 비교 대상이 빠졌기 때문에 오해가 생긴 것이다.
템플릿은 PVOID에 비해서는 큰 코드가 만들어진다.
템플릿은 정적 라이브러리에 비해서는 작은 코드가 만들어진다.
이 것이 결론이다. 실제로도 템플릿과 PVOID를 통상적으로 비교하는 일은 없다... 정적 라이브러리가 가지는 단점과 비교할때 많이 사용된다. 정적 라이브러리는 불필요한 필요없는 코드들도 모두 링크되기 때문에 그만큼 오버헤드를 가지게 된다. 하지만 템플릿은 컴파일타임에 사용자가 호출한 함수들만 연결하기 때문에 필요없는 것들은 링크단계까지 아예 갈 일이 없는 것이다. 실제로 그것이 ATL이 MFC에 비해서 가지는 장점이기도 하다.
끝으로 C언어의 창시자인 커닝헌의 인터뷰 중 일부를 발췌해서 올린다...
그는 C와 C++의 장,단점을 날카롭게 한가지로 일축해서 표현한다.
결국 도구는 다 그 용도에 맞게 써야된다는 것이다...
Sometimes I do write C++ instead of C. C++ I think is basically too big a language, although there's a reason for almost everything that's in it. When I write a C program of any size, I probably will wind-up using 75, 80, 90% of the language features. In other words, most of the language is useful in almost any kind of program. By contrast, if I write in C++ I probably don't use even 10% of the language, and in fact the other 90% I don't think I understand. In that sense I would argue that C++ is too big, but C++ does give you may of the things that you need to write big programs: it does really make it possible for you to create objects, to protect the internal representation of information so that it presents a nice facade that you can't look behind. C++ has an enormous amount of mechanism that I think is very useful, and that C doesn't give you.때때로 나는 C대신 C++을 사용해서 프로그램을 작성합니다. 제가 생각하기에 C++은 안에 모든 기능을 내장하기 때문이라고 하더라도 매우 큰 언어입니다. 제가 어떤 크기의 C프로그램이든 작성할때, 저는 75, 80, 90% 정도의 언어 기능을 사용합니다. 다시 말하면 어떤 종류의 프로그램이든 간에 대부분의 C언어 기능이 유용하다는 점 입니다. 이와는 대조적으로 제가 C++을 사용한다면 언어의 10% 정도도 사용하지 못합니다. 그리고 나머지 90%는 제가 이해하고 있다고 생각하지 않습니다. 이러한 점들에서 전 C++은 너무 크다고 말합니다. 그러나 C++은 큰 프로그램을 작성하는데 필요한 많은 기능들을 제공합니다. 객체를 만드는 것이 가능하며, 내부적인 정보의 표현을 보호할 수 있습니다. 그래서 결국 내부를 보지 못하게 만드는 훌륭한 외관을 표현할 수 있습니다. C++은 제가 생각하기에 아주 방대한 양의 유용한 메카니즘을 가지고 있습니다. 그리고 그것은 C언어가 당신에게 주지 못하는 점 입니다.