커스텀 Queue 클래스를 작성하는데 queue의 item은 추후에 변경 될 수 있으므로 템플릿으로 작성하고자 한다. Queue class를 header에 정의하고 구현을 cpp 파일에 작성해 준다음 main 함수에서 다음과 같이 queue를 생성하고 컴파일을 시도한다.
다음은 해당 template을 사용하는 caller(main 함수)이다.
// // C++ template instantiation test. // // - litcoder #include <iostream> #include "queue.h" using namespace std; int main() { // Create an integer type queue. queue<int> q; // Push elements from the queue. q.push(0); q.push(1); q.push(2); // Pop and print all elements. while (! q.empty()) { cout << "Popped: " << q.pop() << endl; } return 0; }
하지만 별 문제 없을것이라는 생각과 달리 안타깝게도 빌드는 실패한다. 공들여서 만든 method가 링커에 의해 하나도 빠짐없이 “undefined reference” 오류를 뱉으면서 실패한다.
$ make g++ -g -Wall -c -o queue.o queue.cpp g++ -g -Wall -c -o main.o main.cpp g++ -o qtest queue.o main.o /usr/bin/ld: main.o: in function `main': /home/litcoder/Downloads/cpptemplate/main.cpp:12: undefined reference to `queue<int>::queue()' /usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:15: undefined reference to `queue<int>::push(int)' /usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:16: undefined reference to `queue<int>::push(int)' /usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:17: undefined reference to `queue<int>::push(int)' /usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:21: undefined reference to `queue<int>::pop()' /usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:20: undefined reference to `queue<int>::empty()' /usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:25: undefined reference to `queue<int>::~queue()' /usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:25: undefined reference to `queue<int>::~queue()' collect2: error: ld returned 1 exit status make: *** [Makefile:7: qtest] Error 1
이 문제에 대한 해결책은 생각보다 간단한데, cpp파일에 따로 분리했던 구현 부분을 선언들과 함께 header file에 두는 것이다. (해치웠나?)
$ make g++ -g -Wall -c -o queue.o queue.cpp g++ -g -Wall -c -o main.o main.cpp g++ -o qtest queue.o main.o $ ./qtest Popped: 0 Popped: 1 Popped: 2
왜 undefined reference 오류가 났을까?
Queue.o가 빌드됐으니 거기 보면 분명히 저 method들이 선언되어 있을텐데 말이다. 빌드된 symbol들을 살펴보자,
$ objdump -t ./queue.o ./queue.o: file format elf64-x86-64 SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 queue.cpp
O.O 없다. 아무것도. queue.o는 아무 symbol이 없는 빈 파일이었다.
앞서 해결됐다고 했던 header에서 template 선언과 구현을 모두 수행하는 경우에도 queue.o의 symbol table이 비어 있는것은 마찬가지 이지만, 이 경우, caller인 main.o에는 template이 실제 사용하는 type에 binding되어 embed되어 있다. main.o의 symbol들을 살펴보면 다음과 같이 mangle된 symbol들을 볼 수 있다.
$ objdump -t ./main.o ./main.o: file format elf64-x86-64 SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 main.cpp ... 0000000000000000 w F .text._ZN5queueIiEC2Ev 0000000000000048 _ZN5queueIiEC1Ev 0000000000000000 w F .text._ZN5queueIiE4pushEi 000000000000003f _ZN5queueIiE4pushEi ...
참고로, C++ symbol의 type은 demangler를 이용하면 볼 수 있는데, demangle한 결과는 다음과 같다.
_ZN5queueIiEC2Ev -> queue<int>::queue() _ZN5queueIiE4pushEi -> queue<int>::push(int)
결론
C++ template은 빌드하는 시점에서 어떤 data type으로 binding될 지 알 수 없기 때문에 구현 부분을 별도의 파일로 만들어 봤자 아무 것도 빌드 되지 않는다. Header file에 선언과 구현을 함께 두고 이 template을 사용하는 code가 이를 include하여 어떤 data type으로 binding될 지 결정해 주면 그때서야 실제 symbol이 caller module에 포함된다.
즉, template을 선언과 구현으로 나누어서 따로 빌드하는 것은 불가능할 뿐더러 의미도 없으니 include될 하나의 파일(주로 header)에 넣어서 선언과 구현을 합쳐두는게 가장 좋다.