テンプレート引数の一致

Robert Schmidt
Microsoft Corporation

2000 年 5 月 4 日

1 年前、MSDN の Dr. GUI から、C++ テンプレート全般と標準テンプレート ライブラリ(STL)の詳細について書いてはどうかとの提案を受けました。当時はこのトピックを取り上げると、それだけで何年もかかりっきりになりそうだったため、気が進みませんでした。それに、Microsoft のコンパイラには重要なテンプレート機能の一部が欠けていて、バンドルされる標準ライブラリも STL としては貧弱なものだったため、連載が事実上 Microsoft への悪口に変質することを恐れたのです。

しかし、最近次のような理由から、私の考え方も変わってきました。

  • Dinkumware が Visual C++ に添付しているライブラリをアップデートした。現在アップデートされたライブラリのライセンスを取得すべく努力中で、このライセンスを取得できれば、自分のコラムで新旧両方のバージョンが使えるようになる。このアップデートによってコンパイラの弱点が克服されるわけではないが、(うまく行けば)標準ライブラリを問題の原因から除外して考えることができる。

  • Scott Meyers がこの 6 月にオレゴン州ポートランドで 3 日間の STL セミナーを行った。私は、彼の「ボケ」へのツッコミ役と実験演習の助手を兼ねて、セミナーへの参加を依頼された。このセミナーで、私は自分のスキルを磨き、理解を十分に深めるチャンスを得た(同時に彼や奥さんのナンシーと一緒にオレゴンのお宅で過ごすチャンスも得た)。

  • 自分の雑誌コラムを本のように章立てて書こうとする姿勢を改めた。これによって、テンプレート /STL の分野を扱うことに対するためらいが消え、(ニュースグループの投稿を基に)話題性のあるもの、自分が興味あるもの、より大きな目的に応えるものなどから、主題を選ぶことができるようになった。

その名前から汲み取れるように、標準テンプレート ライブラリは C++ テンプレートに大きく依存しています。コンパイラのテンプレート サポートが不十分だと、STL を適正にサポートする能力も弱くなります。Microsoft のコンパイラにはこの点でいくつかの制限があり、そのうちの 2 つをこのコラムで取り上げます。

スカラー型の引数

次のテンプレートを見てください。

  template<typename T>
void tag(T x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

関数パラメータ x のサイズを報告するメッセージを出力するテンプレートです。これを次のように小さな Visual C++ プログラムで実行すると、

  #include <iostream>

template<typename T>
void tag(T x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

int main()
   {
   int i;
   tag(i);
   return 0;
   }

この関数は次のように出力します。

  object of size 4

コンパイラは int 型の引数 i から tag のパラメータの型(int)を推測します。次にコンパイラは tag テンプレートのパターンから、

  template<>
void tag(int x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

というようにテンプレートを特殊化し、インスタンス tag(int) を生成して、これを tag(i) としてコールします。最終的な結果は、次の場合と同じです。

  #include <iostream>

template<typename T>
void tag(T x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

int main()
   {
   int i;
   tag<int>(i);
   return 0;
   }

こちらは、テンプレート引数 int を明示的に指定しています。

配列引数

今度は、サンプルを次のように変更しました。

  #include <iostream>

template<typename T>
void tag(T x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

int main()
   {
   int i;
   int n[10];
   tag(i);
   tag(n);
   return 0;
   }

予想される出力結果は、

  object of size 4
object of size 40

ですが、実際には次のようになります。

  object of size 4
object of size 4

問題は、n が普通の配列からポインタへの変換を受けていることです。コンパイラは、n が「10 個の int 配列」型から「int へのポインタ」に変換された場合は、次のような形式で tag を特殊化したテンプレートがこのポインタに一致すると推測します。

  template<>
void tag(int *x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

この関数が測定しているのはポインタのサイズであって、元の配列のサイズではありません。

解決方法は、配列からポインタへの変換を回避することです。配列を値渡しにすると、常にポインタに変換されてしまいます。これを参照渡しにすれば、変換されません。この知恵を生かして、tag を次のように変更します。

  template<typename T>
void tag(T &x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

コンパイラは引数 n から tag のパラメータを推測します。今度は、推測されるパラメータの型が「10 個の int 配列への参照」になります。その結果、特殊化されたテンプレートは、

  template<>
void tag(int (&x)[10])
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

となり、テンプレートのパラメータ Tint (&)[10] に置換されています。前述のプログラムは次のように書かれたコードと同じように動作します。

  #include <iostream>

template<typename T>
void tag(T &x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

int main()
   {
   int i;
   int n[10];
   tag<int>(i);
   tag<int[10]>(n);
   return 0;
   }

実行によって得られる出力結果も予想どおりです。

  object of size 4
object of size 40

コンパイラは、パラメータの型 int (&)[10] に対して暗黙のうちに tag の特殊化テンプレートを作成します。代わりに、プログラマが同じ型に対して明示的に特殊化テンプレートを作成することもできます。そのようにして特殊化したテンプレートでは、パラメータに対してより詳しい説明書きを加えることもできます。たとえば、次のようにすれば、

  template<typename T>
void tag(T &t)
   {
   std::cout <<"object of size " <<sizeof(t) <<std::endl;
   }

template<>
void tag(int (&x)[10])
   {
   std::cout <<"array object of size " <<sizeof(x)
         <<" and length " <<10 <<std::endl;
   }

大部分のオブジェクトには汎用的なステートメントが、10 個の int 配列には専用のステートメントが、それぞれ出力されるようになります。

ほかの配列型

この考え方を発展させると、int をテンプレート パラメータ T に置き換えることで、上記の特殊化操作をあらゆる型の 10 個の配列に対して行うことができます。この変更を加えた後のテスト プログラムは次のようになります。

  #include <iostream>

template<typename T>
void tag(T &x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

template<typename T>
void tag(T (&x)[10])
   {
   std::cout <<"array object of size " <<sizeof(x)
         <<" and length " <<10 <<std::endl;
   }

int main()
   {
   int i;
   int n[10];
   tag(i);
   tag(n);
   return 0;
   }

このプログラムの出力結果も、やはり次のようになるはずです。

  object of size 4
array object of size 40 and length 10

ところがどっこい、Visual C++ は tag(n) の呼び出しがあいまいであるという警告を出します。コンパイラは、パラメータの型の候補である

  int &x

  int (&x)[10]

が、どちらも n の型 int[10] に対して同じ程度で一致すると見なしています。

このコンパイラの引数解決は標準規格に準拠していないと考えられます。コンパイラは明確に

  tag(int (&x)[10])

を選択すべきです。どちらの考え方が正しいかはともかく、あいまいさは解決しなければなりません。おそらく最も簡単な解決策は、最初のオーバーロードを元の形に戻すことでしょう。

  template<typename T> void tag(T /*&*/x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

これで、コンパイラは n の型 int[10]

  int *

  int (&)[10]

のどちらかに一致させなければなりません。C++ 規格 13.3.3.1.1 節の規定によれば、1 番目の型との一致には「左辺値変形変換」が必要なのに対し、2 番目の型との一致には「恒等変換」が必要になります。規定では 2 番目の型との一致の方が 1 番目より「良い」と見なされるので、コンパイラは明確に nint(&)[10] と一致させることになります。

(この方法は、ここでの問題自体の解決策にはなりますが、一般的な解決策としてはお勧めできません。将来的には tag(T) の特殊化によって T がオブジェクト型に置換される可能性があります。勤勉な読者の方々ならご存知のとおり、オブジェクトを値渡しすると神罰が下されます。)

ほかの配列長

tag に対して行った操作を、10 個の配列だけでなくあらゆる長さの配列に適用できるようにすれば、それが究極の解決策になります。これを成し遂げるには、tag に次の 2 つのテンプレート引数を持たせる必要があります。

  • 配列要素の型

  • 配列の長さ

1 番目はすでにクリアしました。2 番目をクリアするには、tag の 2 番目のオーバーロードを次のように書き換える必要があります。

  template<typename T, size_t N>
void tag(T (&x)[N])
   {
   std::cout <<"array object of size " <<sizeof(x)
         <<" and length " <<N <<std::endl;
   }

N はテンプレートの非型パラメータです。規格に準拠したコンパイラなら、このように変更した tag によって、すべての配列について望みの出力結果が得られます。たとえば、Visual C++ 上で EDG のフロント エンドを実行させると、次のプログラムによって、

  #include <iostream>
#include <stdlib.h>

template<typename T>
void tag(T &x)
   {
   std::cout <<"object of size " <<sizeof(x) <<std::endl;
   }

template<typename T, size_t N>
void tag(T (&x)[N])
   {
   std::cout <<"array object of size " <<sizeof(x)
         <<" and length " <<N <<std::endl;
   }

int main()
   {
   int i;
   int n[10];
   char s[20];
   tag(i);
   tag(n);
   tag(s);
   return 0;
   }

次の出力結果が得られます。

  object of size 4
array object of size 40 and length 10
array object of size 20 and length 20

残念ながら、このプログラムを Visual C++ でコンパイルしたところ、tag(T (&x)[N]) の宣言のところで、

  '<Unknown>' : reference to zero-sized array is illegal

というエラー メッセージが出てしまいました。いろいろと試しましたが、コンパイラのこの制限を確実に回避する方法は今のところわかっていません。次の方法がいちばん手っ取り早いものの、あまり確実な方法ではありません。

  template<typename T>
void tag_array(T &x)
      {
      size_t const N(sizeof(x) / sizeof(x[0]));
      std::cout <<"array object of size " <<sizeof(x)
            <<" and length " <<N <<std::endl;
      }

int main()
   {
   int i;
   int n[10];
   int *p;
   std::vector<int> v(10);
   tag_array(i); // compile-time error as desired
   tag_array(n); // OK as desired, but...
   tag_array(p); // OK even though 'p' is not an array
   tag_array(v); // OK even though 'v' is not an array
   return 0;
   }

気休めかもしれませんが、Visual C++ もある条件下でなら非型パラメータをサポートします。たとえば、次のプログラムによって、

  #include <iostream>

template<int N>
void tag(int x = N)
   {
   std::cout <<"template parameter = " <<N <<std::endl;
   std::cout <<"function parameter = " <<x <<std::endl;
   }

int main()
   {
   tag<5>();
   return 0;
   }

次のように、

  template parameter = 5
function parameter = 5

予想される出力結果が得られます。

終わりに

テンプレート引数の一致とその推測処理については、引き続き執筆する予定です。関連する規定は驚くほど緻密で、そのためコンパイラのベンダ(標準規格を解釈するのが仕事の人々)は、規定の適用方法について互いの異なる意見を持っていることもよくあります。規定は STL の重要な部分にも影響を与えていて、模範的なプログラマでさえ時としてその影響に驚かされることがあります。

Robert Schmidt は MSDN のテクニカル ライターです。このほかに彼が寄稿している雑誌に、C/C++ Users Journal があります。そこでは、彼は編集補助およびコラムニストとして活躍しています。これまでのキャリアで、ラジオ DJ、野生生物飼育係、天文学者、ビリヤード場管理者、探偵、新聞配達夫、そして大学講師を経験しています。

Deep C++用語集