テンプレートの実装をヘッダに書かなければならない理由

ロベールのC++入門講座 09-03

ロベール本ではテンプレートの実装はヘッダに書かなければならないとさらりと書かれていましたが、何故そうしないといけないのかが本を読むだけでは今一良くわからなかったので色々調べてみたところ、理由がわかりました。

まず宣言と定義をファイルを分けて実装した場合、どういった状態になるかを見てみましょう。

// -- csample.h --
#ifndef __CSAMPLE_H__
#define __CSAMPLE_H__

template<class T> void template_swap(T& a, T&b);

#endif
// -- csample.cpp --
#include "csample.h"

template<class T> void template_swap(T& a, T&b) {
    T t = a;
    a = b;
    b = t;
}
// -- main.cpp --
#include "csample.h"
#include <iostream>

int main () {
    int a = 10;
    int b = 20;
    
    template_swap(a,b);
    
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    
    return 0;
}

csampleヘッダにはテンプレート関数の宣言だけ書いておき、csample.cppファイルに実装を定義しています。

さてこれをコンパイルするとどうなるか?

$ cl /W4 /EHs main.cpp csample.cpp
Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

main.cpp
csample.cpp
コードを生成中...
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj
csample.obj
main.obj : error LNK2019: 未解決の外部シンボル "void __cdecl template_swap<int>(
int &,int &)" (??$template_swap@H@@YAXAAH0@Z) が関数 _main で参照されました。
main.exe : fatal error LNK1120: 外部参照 1 が未解決です。

コンパイル自体は通ったものの、リンクエラーが出ています。

どうやらmain.objで使用しているint型のtemplate_swapが見つからないようです。

template_swapはcsampleヘッダで宣言してるしcsample.cppには実装も書いてあるのに何故こんなエラーになるのでしょうか?

答えは分割コンパイルにあります。

分割コンパイルはあくまでもファイル単位でコンパイルします。

自分の脳内で順を追って考えてみます。

まずmain.cppのコンパイルから考えてみましょう。main.cppの中で

template_swap(a,b);

と呼び出している部分がありますね。これはcsampleヘッダに

template<class T> void template_swap(T& a, T&b);

と宣言してあるので、template_swapの実装自体はまだ見えてないものの、template_swapの宣言が存在しているのでコンパイルが通り、無事main.objファイルが生成されます。

次にcsample.cppのコンパイルではtemplate_swapの実装が存在しています。

template<class T> void template_swap(T& a, T&b) {
    T t = a;
    a = b;
    b = t;
}

ですが、このtemplate_swapを使っている処理がどこにも見当たらないのでこのtemplate_swap関数のテンプレートの実体化は行われません。

なので何一つ実体化が行われない状態でcsample.objファイルが生成されます。

さぁそれぞれのファイルの分割コンパイルが終わりました。

main.objではint型のtemplate_swapの実装を探しますが、csample.objは単体でコンパイルした時点ではどこからも参照されてなかったので実体化が行われていないのでint型のtemplate_swapは存在せず、結果未解決のシンボルが発生するということになります。

酷い話ですね。

こういう理由から、テンプレートを使う場合はその実装もヘッダで書かないといけないというわけなんですね。

一応、明示的実体化と呼ばれる方法で宣言と定義を分けて実装することもできるようです。

やり方は簡単。上記の例で言うと、csample.cppをコンパイルする時点でint型のtemplate_swapが実体化されるように予めint型のtemplate_swapを使いますよーと宣言しておけばいいのです。

// -- csample.cpp --
#include "csample.h"

// int型のtemplate_swapを明示的実体化
template void template_swap(int& a, int&b);

template<class T> void template_swap(T& a, T&b) {
    T t = a;
    a = b;
    b = t;
}

このようにしておけば、csample.cpp単体で見たときにもint型のtemplate_swapが使われることがわかるので実体化され、main.objから利用する際に見つけることができるようになります。

ただしこのやり方の欠点としては、そもそもどんな型でも処理させるためのテンプレートという機構なのに、どの型で使われるかを予め宣言しておかないといけないという本末転倒ぶりがやば過ぎてあまり使う気がしないです。

やはりヘッダに実装も全部書いてしまうのが一番良いのだと思います。

そういう意味ではテンプレートを利用するクラスや関数を実装する場合は、なるべく小さなものにすべきなんでしょうね。