커스텀 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)에 넣어서 선언과 구현을 합쳐두는게 가장 좋다.
궁금했던 내용인데 속이 시원하네요