TypeがTになる時

あるものが別のものに変化するとき大抵なんらかの前触れがあり、 本人が気が付いていようがいまいがお構いなしにそれはやってくる。 時には気味が悪いと感じていたものにさえそれを境に小気味よいと感じるようになることだってある。 このような考え方の変化は今までの行動の結果であるだろうし、 新たな世界に踏み込んでいるというそれこそ更なる大きな変化の前兆の前触れかもしれない。

2000年の夏に友人からテンプレートに関する相談を受けたのであるが、 いかんせん私はテンプレートに関する知識があまりなくまあ言ってみれば こちらも勉強させてもらうつもりでその問題にとりくむことにした。 そのコードは次のような感じである。
// input.cpp
#include "improved.h"

void main()
{
  int cmd = examcin(1, 4);
  cout << cmd << endl;

  return;
}
// improved.cpp
#include "improved.h"

template <typename Type> Type examcin(Type min, Type max, char* inprompt, char* errprompt)
{
  ...

  Type exam=0;

  ...

  return exam;
}
examcin*1はinpromptで入力を促す文を表示し、 min以上max以下の整数を入力させるテンプレート関数である。 範囲外の整数が入力されるとerrpromptを表示し再入力を促す。 アルゴリズムに問題はなく完全にC++の仕様がらみの問題であったので詳細なコードは省略してある。
// improved.h
#include <iostream.h>

using namespace std;

template <typename Type> Type examcin(Type min, Type max, char* inprompt="input ", char* errprompt="input again ");
このようなファイル構成である。 コンパイルもうまくいく。ちゃんとオブジェクト(Obj)ファイル*2ができる。 しかしそれらをリンクさせようとすると
input.obj : error LNK2001:
外部シンボル "int __cdecl examcin(int,int,char *,char *)" は未解決です
とエラーになってしまうという問題なのである。 私はよくわからない問題を考える際にはもっと単純な例から出発して少しずつ問題が起こっている状態にまで近づけて なにが悪いのかということをよくやっている。 普段はそうでもないのだがコードを見てもわからないときにはこの手法をよく使う。 この方法のデメリットは手間が大変かかることでかなり非生産的といわれてもしかたがないしあまりにも帰納的でもある。 私自身コードを書くのに帰納的なことをするのは気分がよくないのであるが、 現在問題が発生しているコードをいじくり回して泥沼にはまるよりかは幾分かはましな方法ではないかと考えている。 ま、もちろんこの世界では何よりも先駆けて重要なことは問題に関する情報を収集することであり それが解決への鍵を握っているということも忘れてはならないことであるが。

閑話休題。 単純な例としてテンプレート関数ではなくただの関数ではどうなるのだろうかと考えた。 これなら完全に、リンカもエラーを吐く*3ことなく終了するはずである。 そこでMSDNライブラリのテンプレートに関する情報i)を見てみると...。
関数テンプレートのインスタンス化
各型に対して関数 テンプレートを初めて呼び出すと、"インスタンス" が 作成されます。 これは、その型用に特化したテンプレート関数です。 その型に対して関数を呼び出すたびに、このインスタンスが呼び出されます。 同一のインスタンスが複数存在しても(モジュールが異なっている場合でも)、 実行可能ファイル内のこのインスタンスの数は 1 つだけです。
ということである。 つまり、どんなにテンプレートを書きまくっても使ってくれる所が無ければただの型(タイヤキの型)に成り下がり、 あげくの果てにObjファイルには入れてもらえないということである(通常関数は参照されていてもいなくてもObjファイルに入れられ、 最終的にリンカがその関数を実行ファイル内に入れるかどうか決める)。 事実、テンプレート関数だけで成り立っているコードをコンパイルすると、ものの見事に格好だけのObjファイルが出来てしまう。 テンプレートの記述は宣言の性格を持っていることがよくわかる。 しかし、今回の問題が私たちのテンプレートに関する知識が大幅に不足していることに起因していることだとわかったのである。 というわけでここで今回の外部シンボル未解決問題の経緯を考えてみると以下の様になるだろう。
1.プリプロセッサはhファイルとcppファイルを結合、処理を行った。
2.コンパイラはinputグループ*4のコンパイルに着手。
examcinが使われているので、関数テンプレートプロトタイプから
int __cdecl examcin(int,int,char *,char *)
というexamcinのインスタンスを作成する。 この際、inputグループにはtempleteの内容に関する情報はないのでコンパイラは外部シンボル(参照)としてリンカに解決を任せて処理を済ましたことにする。
3.コンパイラはimprovedグループのコンパイルに着手。
テンプレートでexamcinが宣言されているがどこからも参照されていないと判断し、その部分は全面的に削除。
4.コンパイラが作成したObjファイルを整形して実行ファイルにするリンカに処理の中心が移る。
リンカはinput.objを走査、解決されていない外部参照があるのを発見する。リンカは他のObjファイルを走査して、
int __cdecl examcin(int,int,char *,char *)
というシンボルが無いかどうか照合する。 しかし見つからない。 なぜならもうすでにそのシンボルの型(タイヤキの型)さえもObjファイルには含まれていないからである。
5.外部シンボルをついに解決できなかったリンカはとうとう切れる...。
input.obj : error LNK2001:
外部シンボル "int __cdecl examcin(int,int,char *,char *)" は未解決です
以上の記述は私がいろいろな情報から勝手に推測したものであり絶対的なものではない。 つまり想像であるのだが大筋はあっているはずである。これにしたがってみると元のコードは以下の様に修正される。
// input.cpp
#include "improved.h"

void main()
{
  int cmd = examcin(1, 4);
  cout << cmd << endl;

  return;
}
// improved.h
#include <iostream.h>

using namespace std;

template <typename Type> Type examcin(Type min, Type max, char* inprompt="input ", char* errprompt="input again ");
{
  ...

  Type exam=0;

  ...

  return exam;
}
ということになり、結果としてimproved.cppは必要でなくなったわけである。

この問題についていろいろと考えているとき自分自身のなかのコーディングはどのようにあるべきかというような ある意味一種の思想のようなものが変化していることに気がついた。 STL系のコードを見たことがある方ならおわかりいただけると思うのであるが、 そこでは少し風変わりなコーディングスタイルが採用されていると私は思う。 たとえば次のようなコードii)である。
virtual _OI do_put(_OI _F, bool _Intl,
  ios_base& _X, _E _Fill, long double _V) const
  {bool _Neg = false;
  if (_V < 0)
   _Neg = true, _V = -_V;
  size_t _Exp;
  for (_Exp = 0; 1e35 <= _V && _Exp < 5000; _Exp += 10)
   _V /= 1e10;
このような変数をわずかな文字で表現するコーディングスタイルは正直、私は受け入れることができなかった。 一部の人がこのようなスタイルをとるのであればともかくSTL系のコードはこのスタイルなのである。 また、素直にいえば汚いコードであると認識してきたと思う。 しかし、今回の問題についていろいろと考えていているとコードに対してイライラしていたとき ふと「TypeをTに書き換えてみよう」という考えが浮かんだのである。 なにも考えないまま「T」に書き換え、なにも考えないまま「おお、いい感じ」と認識した。
template <typename T> T examcin(T min, T max, char* inprompt="input ", char* errprompt="input again ");
この経過は今でも不思議で仕方がない。 なんせこの時からSTL系のコードに対して嫌悪感を抱かなくなったのであるから。 いろいろなコードを読むことはいろいろな国を旅行することと似ていると思うが、 今回の問題は自分なりのコーディング思想に新たな考え方が入ってきたその瞬間であると言える。[終]

[注釈]
  1. その友人はRPGをC++でやろうとしている。 examcinもそのコードの一部である。 [戻る]
  2. これらの用語はMicrosoft Windows環境下でのものである。 Microsoft Windows環境下ではソースコードから最終的にアプリケーション(厳密には実行可能ファイル)を生成する作業を「ビルド」と言う。 ビルドという言葉自体はいろいろな作業をまとめて呼ぶ総称名である。 つまり、ソースコードをコンパイルしてバイナリつまりオブジェクトファイル群を生成しそれらをリンカによって結びつけて実行可能ファイルを生成するという工程を表す。 [戻る]
  3. これはとても個人的なことであるのだが「吐く」という言葉はとてもタフな感じを抱かせる。 うなってしまう文句iii)は『最強最速のコードを吐き出すコンパイラとして多くのデベロッパーに愛用されていた』である。 [戻る]
  4. グループというよりは「空間」と呼んでしまったほうが正確かもしれない。 私は根がBasic系であるのだがそのせいかCやC++のファイル間のつながり具合はサバサバしていると感じる。 呼ばなきゃ誰もこないし使えないという感じとでもいうか。 extern宣言子を頭につけてコンパイラに「私は外部シンボルの属性持ってますよ(つまりこのファイルでは定義されていない)」と通知しなければならないし。 [戻る]
[参考資料]
  1. Microsoft Developer Network Library 2000.7 >> テンプレート
    • 関数テンプレートの操作 > 関数テンプレートのインスタンス化
  2. 月刊 Cマガジン 1999年6月号(ソフトバンク)
    • p144.l2 Standard C/C++ > money_put面(P.J.Plauger)
  3. Direct3D プログラミングガイドブック(清水亮・ISBN:4-88135-543-0・翔泳社)
    • p15.n21 入門編 > DirectXへの長い道 > DirectXを使うための準備 > とりあえず選択の余地がない、コンパイラ選び
[キーワード]
テンプレート関数 関数テンプレート 外部シンボル インスタンス 動作 templete T Type