C++ template 선언과 구현을 서로 다른 파일에 나눌 수 없다

커스텀 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)에 넣어서 선언과 구현을 합쳐두는게 가장 좋다.