部分的なテンプレート実装
Bobby Schmidt
Microsoft Corporation
July 9, 2002
この資料では、 Microsoft® Visual C++® .NET にまだ不足している C++ 標準に準拠するために必要な多種多様なテンプレート機能について説明します。
不足している標準準拠機能の部分的な一覧は、 Visual C++ ドキュメントで参照できます。 その一覧は、 「Visual C++ 実装が C++ 標準に準拠していない既知の点の一部」を示しています。 ここで重要な言葉は "一部" です。 そう、"すべてではない" のです。 そういうわけで、公開された一覧には掲載されていない、 いくつかの標準に準拠していない問題点について説明します。
クラス テンプレートの部分的な特殊化
クラス テンプレートには、まさに次のような 1 つの主要な宣言があります。
template<typename T>
class X
{
};
x<int> x1;
x<char> x2;
x<char *> x3;
このプライマリ テンプレートは、 おそらく無限のテンプレート引数のセットに対して汎用なテンプレートです。 上記のプライマリ テンプレートは、概念的には記述されたとおりに機能します。
template
class X<T> // 概念のみ; C++ の文法では許可されません
{
};
テンプレートが、多くの明示的な特殊化を持つこともあります。 各特殊化は、次のようにすべてのテンプレート パラメータを、 テンプレート引数の一意な順列に明示的に解決します。
template<typename T>
class X//<T>
{
};
template<
>class X<char>
{
};
x<int> x1;
x<char> x2; // 明示的な特殊化 X<char> を使用します
x<char *> x3;
極端な汎用化と極端な特殊化の間には中間的な存在があり、 C++ 標準では部分的な特殊化と呼んでいます。 その名前が暗示しているとおり、 このような特殊化は、次のようにテンプレート パラメータを部分的に解決します。
template<typename T>
class X
{
};
template<typename T>
class X<T *>
{
};
template<>
class X<char>
{
};
X<int> x1;
X<char> x2;
X<char *> x3; // 部分的な特殊化 X<T *> を使用します
Visual C++ .NET では、上記の例では、次のように自己崩壊します。
'T' : 宣言されていない識別子
構文エラー : '>'
構文エラー : ';' が '{' の前にありません
構文エラー : ';' が '}' の前にありません
テンプレート クラスの入れ子の UDT を列以外で定義できません。
直前のエラーを修復できません。コンパイルを中止します。
明示的な特殊化と同様に、 部分的な特殊化は限定された引数のセットと一致します (この意味では、明示的な特殊化は、最大限に限定された部分的な特殊化です)。 また、主要な宣言と同様に、 部分的な特殊化は無限の引数のセットと潜在的に一致します。 これらの無限のセットはオーバーラップすることがあります。 そのため、同じテンプレート引数が、次のように複数の部分的な特殊化と一致する可能性があります。
template<typename T>
class X<T *>
{
};
template<typename T>
class X<T const *>
{
};
X<char const *> x4; // どちらが一致しますか?
このようなあいまいさを防ぐために、 C++ 標準が定義する一致基準とランキング基準を使用して、 コンパイラが部分的な特殊化の候補を、いわゆる "部分的な順序付け" にランク付けを行います。 その順序付けに従うと、特殊化 X<T const *> が採用されます。
C++ 標準ライブラリを含めて、 現在の多くの C++ ライブラリは、 テンプレートの部分的な特殊化を必要とします。 同梱されているコンパイラにはこの点が不足しているので、 これらのライブラリは Visual C++ ではまったく機能しないか、 意味もなく機能するかのいずれかです。 この問題は、Visual C++ の次のリリースで、最初に解決を期待することの 1 つです。
関数テンプレートの部分的な順序付け
標準は、 次のようにオーバーロードされた関数テンプレート間での、 部分的な順序付けも定義します。
template<typename T>
void f(T)
{
}
template<typename T>
void f(T *)
{
}
template<>
void f(char)
{
}
int main()
{
f(6); // f(T) を使用します
f("6"); // f(T *) を使用します
f('6'); // f(char) を使用します
}
**注 ** f(T *) の宣言は、オーバーロードであり、 部分的な特殊化ではありません。 反対語の知識に関係なく、 「関数テンプレートの部分的な特殊化というようなものはありません」。 関数テンプレートの "明示的な" 特殊化は存在します。 f(char) がその例です。 いずれにせよ、 クラス テンプレートと関数テンプレートで、用語に一貫性のない理由がわかりません。
Visual C++ は、次のように関数テンプレートの部分的な順序付けを間違って処理します。
int main()
{
f(6); // OK, f<T> を適切に使用します
f("6"); // エラー, どちらの f を使用するのか判断できません
f('6'); // OK, f<char> を適切に使用します
}
標準がクラス テンプレートの順序付け定義することを、 等価な関数テンプレートの順序付けに置き換えて考えてみると、 この欠陥は驚くことではありません。 Visual C++ が関数テンプレートの順序付けをサポートするようになると、 おそらく、結果的にクラス テンプレートの順序付けが欠落することになるでしょう。
次のことを、読者への課題として残しておきます。
template<typename T>
void f(T const *)
{
}
template<>
void f(char *)
{
}
int main()
{
f("どちらのオーバーロードに一致すべきでしょうか?");
}
クラス テンプレートのパラメータ名
クラス テンプレートの主要な定義は、通常次のように、 名前でテンプレートのパラメータを指定します。
template<typename T> // T はテンプレート パラメータです
class X
{
void f(T); // T をここで指定します
};
ただし、次の明示的な特殊化を考えてみます。
template<> // テンプレート パラメータがありません
class X<int>
{
void f(T); // これは依然として機能しますか?
};
T というパラメータ名は、この特殊化で実際に宣言されていませんが、 依然としてこのスコープに存在して、名前解決に利用できますか? 私の C++ 標準の解釈によれば、 答えは「いいえ」です。 Visual C++ .NET によれば、答えは「はい」です。 上記の例は、特殊化を宣言した場合と同様にコンパイルされ、実行されます。
template<>
class X<int>
{
typedef int T;
void f(T);
};
メンバ - テンプレート定義
一部のクラスおよびクラス テンプレートは、 それ自体がテンプレートであるメンバを含むことができます。 C++ 標準は、他の種類のメンバと同様に、 以下のいずれかのように表されるメンバ テンプレート定義を可能にします。
class X
{
template<typename T>
T f(T)
{
return T();
}
};
または、次のように宣言から切り離すこともできます。
class X
{
template<typename T>
T f(T);
};
template<typename T>
T X::f(T)
{
return T();
}
Visual C++ .NET ドキュメントには、 2 つ目の例をコンパイルしてはいけないと記載されていますが、 このドキュメントは間違っています。 私の例では実際にコンパイルが行われます。 ただし、X 自体が通常のクラスではなく、テンプレートの場合、 次のようになります。
template<typename T2>
class X
{
template<typename T>
T f(T);
};
template<typename T2>
template<typename T>
T X<T2>::f(T)
{
return T();
}
また、メンバが演算子の場合は次のようになります。
class X
{
template<typename T>
operator T();
};
template<typename T>
X::operator T()
{
return T();
}
その結果、予想どおりに Visual C++ .NET がコンパイルに失敗します。
回避策 : C# または Java でメンバを定義する場合と同様に、 問題のメンバの宣言位置でそのメンバを定義する必要があります。
class X
{
template<typename T>
operator T()
{
return T();
}
};
メンバ テンプレートの明示的な特殊化
他のテンプレートと同様に、メンバ テンプレートも明示的な特殊化を持つことがあります。 上記の例を続けて見ていきます。
class X
{
template<typename T>
T f(T)
{
return T();
}
};
template<>
int X::f(int)
{
return int();
}
このコードをコンパイルすると、 Visual C++ は次のエラーを生成します。
'int X::f(int)' : オーバーロードされたメンバ関数が 'X' にありません
このエラーを除去しようとすると、 次のように X で特殊化を宣言ないことを選択するかもしれません。
class X
{
template<typename T>
T f(T)
{
return T();
}
template<>
int f(int);
};
これでもまだエラーになります。 C++ 標準によれば、 宣言は X を含む名前空間スコープ内で示される必要があります。 この場合名前空間スコープは、グローバル スコープです。
この問題に対する回避策がわかりません。 現状では、テンプレートのメンバを特殊化できないようです。
クラス テンプレート内の入れ子のクラス
C++ 標準によれば、 クラス テンプレートは次のように入れ子になったクラス宣言を含むことができます。
template<typename T>
class X
{
class Y;
};
Visual C++ は入れ子になった定義以外にも、次の定義も受け入れます。
template<typename T>
class X
{
class Y
{
}
};
ただし、次のように Y 定義を X 定義の外部に移動する場合を考えてみます。
template<typename T>
class X
{
class Y;
};
template<typename T>
class X<T>::Y // ここでエラーが発生しますが、OK のはずです
{
};
この結果、コンパイラは間違って、 以下のエラー メッセージを表示します。
'T' : 定義されていない識別子です
テンプレート クラスの入れ子の UDT を列以外で定義できません。
直前のエラーを修復できません。コンパイルを中止します。
このエラー メッセージは、 前述の部分的な特殊化の例で生成されたエラー メッセージと同じです。
この問題を以前の項目の特殊化の問題と結びつけて考えると、 板ばさみ状態になります。 テンプレートの準拠問題を回避するために、 メンバの宣言を使用してメンバを定義する必要があったり、 メンバの宣言から切り離してメンバを定義する必要があったりします。 これらの制限をすべての回避できる、あらゆる場合に通用する簡単な規則はありません。
Visual C++ .NET が完全に準拠するようになったとしても、 あらゆる状況で同じスタイルの規則を適用する必要はないでしょう。 私が初めて C++ を学習したときは、 ほとんどの場合、次のようにメンバ定義をメンバの宣言から切り離していました。
class X
{
public:
X(int = 10, char = '\0');
X &operator=(X const &);
long const *f() const;
// ...
};
X::X(int i, char c)
{
// ...
}
X &X::operator=(X const &that)
{
// ...
}
long const *X::f() const
{
// ...
}
C プログラマとしては、この方法はより自然で、 C に似ていると思えました。 クラス定義は簡潔で、クイック リファレンスとして役立ちました。 それに対して関数定義は、展開されていて、読みやすいものになりました。 また、C コードをこのスタイルの C++ コードに移植すると、迅速に行えることもわかりました。 余分な空白行や関数宣言の繰り返しをコードに埋め込むことは、 コードを明確にしたり、コードを移植するために、 それなりに価値のあることのように思えました。
テンプレートをきっかけに、自分のスタイルを再検討するようになりました。 つまり、同様の価値を得るために、次のような非常につまらない代償を払うことになりました。
template<class T>
class X
{
public:
X(int = 10, char = '\0');
X &operator=(X const &);
long const *f() const;
// ...
};
template<class T>
X<T>::X(int i, char c)
{
// ...
}
template<class T>
X<T> &X<T>::operator=(X const &that)
{
// ...
}
template<class T>
long const *X<T>::f() const
{
// ...
}
STL テンプレートや STLesque テンプレートに直面にしたとき、 ついに次のようにすることを決めました。
template<typename T = int, typename P = T *>
class X
{
public:
template <unsigned N = 10>
class X2
{
public:
template<typename T2>
static P f(T2 const &)
{
return new T[x][N];
}
};
};
事態は悪化しないとしても、ダメージは大きなものです。
template<typename T = int, typename P = T *>
class X
{
public:
template <unsigned N = 10>
class X2;
};
template<typename T, typename P>
template<unsigned N>
class X<T, P>::X2
{
public:
template<typename T2>
static P f(T2 const &);
};
template<typename T, typename P>
template<unsigned N>
template<typename T2>
P X<T, P>::X2<N>::f(T2 const &x)
{
return new T[x][N];
}
依存 - 名前解決
テンプレート内では、 一部の型と式がテンプレート パラメータによって異なる意味を持ちます。
template<typename T, unsigned N>
void f()
{
T a[N];
sizeof a;
}
int main()
{
f<int, 7>();
}
f の本体内では、 型 T と定数式 N は、 名付け親のテンプレート パラメータにわずかに依存します。 さらに、次のようになります。
- a の型は「N T の配列」です。 そのため、T と N の両方に依存します。
- 定数式 sizeof a は直接 a の型に依存します。 そのため、T と N に間接的に依存します。
コンパイラは、f の定義のコンテキストでは、 T と N の意味を単独で評価できません。 ただし代わりに、f のインスタンス生成位置で T と N を解決する必要があります。 このことは、a の型もサイズもインスタンスを生成するまで知ることができないことを意味します。 この例では、次の呼び出しが、
f<int, 7>();
次のインスタンスを作成します。
template<>
void f<int, 7>()
{
int a[7];
sizeof(a);
}
その結果 T と N を解決し、 a の型とサイズも解決します。
同じ 2 段階のメカニズムが、異なるエラー メッセージを生成する可能性があることに注意してください。 呼び出しを次のように変更します。
f<int, 0>();
その結果、 コンパイラはテンプレート引数 0 を、 テンプレート パラメータ unsigned N にバインドできます。 ただし、f<int, 0> の定義では、 コンパイラは次の式を形成できません。
int a[0];
配列のサイズを 0 で定義できないためです。 Visual C++ .NET は、呼び出しを適切にコンパイルすることに失敗し、 定義の位置とインスタンス生成の位置の両方にエラー メッセージを生成します。
より複雑な例
次に、Visual C++ .NET が最終的に悪い結果になる例を示します。
template<typename T>
struct X
{
X(T *p = 0)
{
f(p);
}
};
class Z;
X<Z> x;
f は何を意味するでしょうか? 一見しただけでは、 f は X の定義内で示される何かに名前を付ける必要があるだけで、 依存性のない名前だと思えるかもしれません。 では、"何か" を宣言してみましょう。
void f(void *);
template<typename T>
class X
// ...
その結果、x 定義は事実上次のインスタンスを作成します。
template<>
struct X<Z>
{
X(Z *p = 0)
{
::f(p);
}
};
次に Z を名前空間内に移動します。
// ...
namespace N
{
class Z;
}
X<N::Z> x;
x は、依然として次のインスタンスを作成します。
template<>
struct X<N::Z>
{
X(N::Z *t = 0)
{
::f(t);
}
};
最後に 1 つ以上のメンバを名前空間に追加します。
// ...
namespace N
{
class Z;
void f(Z *);
}
X<N::Z> x;
Visual C++ .NET は、依然として (および間違って) f を ::f として解決します。 しかし、f は N::f として解決するべきです。 そのため、作成されるインスタンスは次のようになります。
template<>
struct X<N::Z>
{
X(N::Z *t = 0)
{
N::f(t);
}
};
標準に準拠するコンパイラは、 次のような場合でも、修飾されていない f を N::f に魔法のように解釈します。
- 名前 N は、 直接テンプレート パラメータにバインドされません (名前空間をテンプレート引数として使用できません)。
- 名前 N は、X の定義のコンテキストでは可視になりません。
- 修飾されていない名前 f は、X のインスタンス生成の時点では可視になりません。
この魔法は、C++ 標準が「引数に依存した名前照合」と呼び、 また別の環境では「Koenig 照合」と呼ばれるものに関係しています。 このような照合により、 関数の引数に依存するスコープ内で関数の名前を見つけることができます。 Koenig 照合は、名前解決の複雑性を非常に高めます。 同時に、次のような式を許可します。
cout << x
この式は、任意の x では不可能です。
Visual C++ .NET には、テンプレート外部での Koenig 照合の問題があります。 次のコラムのために、このトピックを残しておきます。
export とエクスポートされたテンプレート
怖い、怖い。
export は "切り離しモデル" のテンプレートを許可します。 それは理解することになった、いえ、理解している "包含モデル" のテンプレートとは異なります。
- 包含モデルでは、インスタンスの生成時点以前に、 テンプレートのインスタンスを作成する翻訳単位でテンプレートを定義する (または含める) 必要があります。
- 切り離しモデルでは、1 つの翻訳単位でテンプレートを定義し、 他の (独立した) 翻訳単位でテンプレートのインスタンスを作成できます。
次の例は、必要だけれども機能しない例です。
export template<typename T>
class X;
Visual C++ .NET は、上記の例を処理できません。 確かに、この資料を執筆していたとき (6 月末)、 上記の例を処理できる市販のコンパイラは、発売されていませんでした。 C++ 標準で必要な言語機能のすべてである export およびエクスポートされたテンプレートは、 最も実装しそうにない未研究分野です。
その未研究分野は、 今まさに実装されようとしています。 Edison Design Group のウィザードは、 C++ front end の Version 3.0 をリリースし、 そのトランスレータは上記の例をコンパイルできます。 以前説明したように、EDG はコンパイラを完全には実装していませんが、 代わりに、その技術を取り入れるユーザーに対して、 コンパイラのフロント エンド ライセンスを提供します。 EDG のトランスレータを使用している製造会社である Greg Comeau は、 export を利用できる市販用のコンパイラを、 近々発売するようです。
今、まさに実装されようとしていますが、 export は 5 年も前から C++ 標準の機能であり、 機能の複雑性、コスト、および関連する利点を裏切っています。
export は実証され、理解され、受け入れられる事例の成文化というよりも、 C++ 委員会の理論上の発明品として世に出ました。 委員会が発明した他の重要な言語機能はすべて (名前空間、RTTI、テンプレートは除く) 言語の使用者と言語の実装者にとって、 予想外の結果になりました。 私は export が、この不幸な結果に続くだろうと確信しています。 それには多くの理由があります。 export は、問題を解決するためにデザインされたと言われていますが、 問題を解決せず、 実際はさらに問題を悪くし、 翻訳単位と名前解決の意味を脅かし、 将来の言語革新を妨げ、 完全に明確にすることはできないように見え、実装するには一見、高価です。 なんて格安なのでしょう!
Visual C++ .NET が直面している標準に準拠しない問題である export の不足は、 少なくともユーザーに関係します。 コンパイラが export をサポートしていたとしても、 あまり使用したくないでしょう。 export が失敗した試みだと考える理由に納得できない、 またはその理由を理解したい場合、 C/C++ Users Journal の 9 月号と 10 月号の Herb Sutter 氏の次回のコラムを読むことをお勧めします。
最後に
このコラムでは、重要なテンプレート関連の問題について取り上げました。 それは、C++ 標準の 14 節に関係する問題点です。 次のコラムまたは次の 2 回のコラムで、具体的にはテンプレートとあまり関係のない問題について、細かく分析します。
訂正
Visual C++ .NET で新たに機能する準拠機能を説明している以前のコラムで、 次のような非型テンプレート パラメータの例を示しています。
template<int N>
class X;
X<10> x;
また、Visual C++ 6.0 で機能する必要がある (ただし、実際には機能しない) 例を示しています。
おっと。
最初に記述したように、次の例は標準に準拠していません。 型 X<int> は、x の宣言位置では不完全です。 そのため、Visual C++ 6.0 では十分に機能しません。 記述するつもりだった例は、次のとおりです。
template<int N>
class X
{
};
X<10> x;
この訂正済みの例は、実際に Visual C++ 6.0 で十分機能します。 ただし、この例はコンパイラには適切ですが、 本来の例の目的をなくすので、このコラムにはあまり適切ではありません。
Deep C++ .NET
Bobby Schmidt は MSDN のテクニカル ライターです。 このほかに彼が寄稿している雑誌に、C/C++ Users Journal があります。 そこで彼は編集助手およびコラムニストとして活躍しています。 これまでのキャリアで、ラジオ DJ、野生生物飼育係、天文学者、ビリヤード場管理者、探偵、新聞配達夫、 そして大学講師を経験しています。