テンプレートをヘッダーファイルにのみ実装できるのはなぜですか?
C ++標準ライブラリからの引用:チュートリアルとハンドブック:
現時点でテンプレートを使用する唯一の移植可能な方法は、インライン関数を使用してヘッダーファイルにテンプレートを実装することです。
どうしてこれなの?
(明確化:ヘッダーファイルだけがポータブルソリューションではありませんが、最も便利なポータブルソリューションです。)
回答
警告:実装をヘッダーファイルに入れる必要はありません。この回答の最後にある代替ソリューションを参照してください。
とにかく、コードが失敗する理由は、テンプレートをインスタンス化するときに、コンパイラが指定されたテンプレート引数を使用して新しいクラスを作成するためです。例えば:
template<typename T>
struct Foo
{
T bar;
void doSomething(T param) {/* do stuff using T */}
};
// somewhere in a .cpp
Foo<int> f;
この行を読み取ると、コンパイラーは新しいクラスを作成します(FooInt
これを呼び出しましょう)。これは次のクラスと同等です。
struct FooInt
{
int bar;
void doSomething(int param) {/* do stuff using int */}
}
したがって、コンパイラは、テンプレート引数(この場合int
)を使用してメソッドをインスタンス化するために、メソッドの実装にアクセスできる必要があります。これらの実装がヘッダーにない場合、それらにアクセスできないため、コンパイラーはテンプレートをインスタンス化できません。
これに対する一般的な解決策は、テンプレート宣言をヘッダーファイルに記述してから、クラスを実装ファイル(.tppなど)に実装し、この実装ファイルをヘッダーの最後にインクルードすることです。
Foo.h
template <typename T>
struct Foo
{
void doSomething(T param);
};
#include "Foo.tpp"
Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
このように、実装は宣言から分離されていますが、コンパイラーはアクセスできます。
代替ソリューション
もう1つの解決策は、実装を分離したままにして、必要なすべてのテンプレートインスタンスを明示的にインスタンス化することです。
Foo.h
// no implementation
template <typename T> struct Foo { ... };
Foo.cpp
// implementation of Foo's methods
// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float
私の説明が十分に明確でない場合は、このテーマに関するC ++ Super-FAQをご覧ください。
これは、個別のコンパイルが必要であり、テンプレートがインスタンス化スタイルのポリモーフィズムであるためです。
説明のために、もう少し具体的に見てみましょう。次のファイルがあるとします。
- foo.h
- のインターフェースを宣言します
class MyClass<T>
- のインターフェースを宣言します
- foo.cpp
- の実装を定義します
class MyClass<T>
- の実装を定義します
- bar.cpp
- 使用
MyClass<int>
- 使用
個別のコンパイルは、bar.cppとは独立してfoo.cppをコンパイルできるはずであることを意味します。コンパイラーは、各コンパイル単位での分析、最適化、およびコード生成のすべてのハードワークを完全に独立して実行します。プログラム全体の分析を行う必要はありません。プログラム全体を一度に処理する必要があるのはリンカだけであり、リンカの仕事は大幅に簡単です。
bar.cppは、foo.cppをコンパイルするときに存在する必要はありませんが、すでに持っていたfoo.oをbar.oとリンクできるはずです。作成したばかりで、fooを再コンパイルする必要はありません。.cpp。foo.cppはさえ、動的ライブラリにコンパイルすることなく、どこか別の場所に分散できfoo.cpp、と彼らは私が書いた後何年記述したコードとリンクfoo.cppを。
「インスタンス化スタイルのポリモーフィズム」とは、テンプレートMyClass<T>
が実際には、任意の値で機能するコードにコンパイルできる汎用クラスではないことを意味しますT
。これにより、ボクシングなどのオーバーヘッドが追加され、アロケータやコンストラクタに関数ポインタを渡す必要があります。C++テンプレートの目的はclass MyClass_int
、ほぼ同一のclass MyClass_float
などを記述しなくても、最終的にコンパイルされたコードを作成できるようにすることです。私たちは、主にいるかのようにしていた個別の各バージョンが書かれて。したがって、テンプレートは文字通りテンプレートです。クラステンプレートはクラスではなく、T
遭遇するたびに新しいクラスを作成するためのレシピです。テンプレートをコードにコンパイルすることはできません。コンパイルできるのは、テンプレートをインスタンス化した結果のみです。
したがって、foo.cppがコンパイルされると、コンパイラーはbar.cppを参照してそれMyClass<int>
が必要であることを認識できません。テンプレートを表示することはできますがMyClass<T>
、そのためのコードを出力することはできません(これはテンプレートであり、クラスではありません)。また、bar.cppがコンパイルされると、コンパイラはを作成する必要があることを認識できますがMyClass<int>
、テンプレートMyClass<T>
(foo.h内のインターフェイスのみ)を認識できないため、作成できません。
foo.cpp自体がを使用する場合MyClass<int>
、そのコードはfoo.cppのコンパイル中に生成されるため、bar.oがfoo.oにリンクされている場合は、それらを接続して機能します。その事実を利用して、単一のテンプレートを作成することにより、テンプレートのインスタンス化の有限セットを.cppファイルに実装できます。ただし、bar.cppがテンプレートをテンプレートとして使用し、好きなタイプでインスタンス化する方法はありません。foo.cppの作成者が提供すると考えたテンプレートクラスの既存のバージョンのみを使用できます。
テンプレートをコンパイルするとき、コンパイラは「すべてのバージョンを生成」する必要があると思うかもしれません。使用されたことのないバージョンは、リンク中に除外されます。ポインタや配列などの「型修飾子」機能により、組み込み型だけでも無限の数の型を生成できるため、このようなアプローチが直面する大きなオーバーヘッドと極端な困難は別として、プログラムを拡張するとどうなりますか。追加することによって:
- baz.cpp
- 宣言および実装し
class BazPrivate
、使用しますMyClass<BazPrivate>
- 宣言および実装し
私たちもそうでない限り、これが機能する可能性のある方法はありません
- プログラム内の他のファイルを変更するたびにfoo.cppを再コンパイルする必要があります。これにより、
MyClass<T>
- その必要baz.cppは、(ヘッダを含む可能性を介して)含有の完全テンプレートを
MyClass<T>
コンパイラが生成することができるように、MyClass<BazPrivate>
のコンパイル時baz.cpp。
プログラム全体の分析コンパイルシステムはコンパイルに永遠に時間がかかり、ソースコードなしでコンパイルされたライブラリを配布することは不可能であるため、誰も(1)を好きではありません。したがって、代わりに(2)があります。
ここにはたくさんの正解がありますが、これを追加したいと思います(完全を期すため):
実装cppファイルの下部で、テンプレートが使用されるすべてのタイプを明示的にインスタンス化すると、リンカは通常どおりそれらを見つけることができます。
編集:明示的なテンプレートのインスタンス化の例を追加します。テンプレートが定義され、すべてのメンバー関数が定義された後に使用されます。
template class vector<int>;
これにより、クラスとそのすべてのメンバー関数がインスタンス化されます(したがって、リンカーが使用できるようになります)。同様の構文がテンプレート関数に対して機能するため、非メンバー演算子のオーバーロードがある場合は、それらに対して同じことを行う必要がある場合があります。
上記の例は、ベクターがヘッダーで完全に定義されているため、かなり役に立ちません。ただし、一般的なインクルードファイル(プリコンパイル済みヘッダー?)がextern template class vector<int>
、ベクターを使用する他のすべての(1000?)ファイルでインスタンス化されないようにする場合を除きます。
テンプレートは、実際にオブジェクトコードにコンパイルする前に、コンパイラによってインスタンス化される必要があります。このインスタンス化は、テンプレート引数がわかっている場合にのみ実現できます。ここで、テンプレート関数がで宣言されa.h
、で定義されa.cpp
、で使用されるシナリオを想像してみてくださいb.cpp
。a.cpp
がコンパイルされるとき、次のコンパイルでb.cpp
テンプレートのインスタンスが必要になることは必ずしもわかっていません。ましてや、どの特定のインスタンスであるかは言うまでもありません。より多くのヘッダーファイルとソースファイルの場合、状況はすぐに複雑になる可能性があります。
コンパイラーをよりスマートに作成して、テンプレートのすべての使用を「先読み」できると主張することもできますが、再帰的またはその他の複雑なシナリオを作成することは難しくないと確信しています。AFAIK、コンパイラはそのような先読みをしません。アントンが指摘したように、一部のコンパイラはテンプレートインスタンス化の明示的なエクスポート宣言をサポートしていますが、すべてのコンパイラがそれをサポートしているわけではありません(まだ?)。
実際には、標準C ++ 11の前には、定義されたexport
キーワードしまうことが可能とヘッダファイルにテンプレートを宣言し、他の場所でそれらを実装するために行うことを。
人気のあるコンパイラはどれもこのキーワードを実装していません。私が知っているのは、Comeau C ++コンパイラで使用されるEdisonDesignGroupによって作成されたフロントエンドだけです。コンパイラは適切なインスタンス化のためにテンプレート定義を必要とするため、他のすべての場合はヘッダーファイルにテンプレートを書き込む必要がありました(他の人がすでに指摘しているように)。
その結果、ISO C ++標準委員会はexport
、C ++ 11でテンプレートの機能を削除することを決定しました。
標準のC ++にはそのような要件はありませんが、一部のコンパイラでは、使用するすべての変換ユニットですべての関数およびクラステンプレートを使用できるようにする必要があります。実際、これらのコンパイラーの場合、テンプレート関数の本体をヘッダーファイルで使用できるようにする必要があります。繰り返す:つまり、これらのコンパイラは、.cppファイルなどの非ヘッダーファイルでの定義を許可しません。
この問題を軽減するはずのexportキーワードがありますが、移植性にはほど遠いです。
コンパイラーは、テンプレートパラメーターに指定/推定されたパラメーターに応じて、異なるバージョンのコードをインスタンス化する必要があるため、テンプレートをヘッダーで使用する必要があります。テンプレートはコードを直接表すのではなく、そのコードのいくつかのバージョンのテンプレートであることに注意してください。非テンプレート関数を.cpp
ファイルにコンパイルすると、具体的な関数/クラスがコンパイルされます。これは、さまざまなタイプでインスタンス化できるテンプレートには当てはまりません。つまり、テンプレートパラメータを具象型に置き換えるときに具象コードを発行する必要があります。
export
個別のコンパイルに使用することを目的としたキーワードを持つ機能がありました。このexport
機能は非推奨でC++11
あり、AFAIKでは1つのコンパイラのみが実装しました。を利用しないでくださいexport
。分割コンパイルは可能ではないC++
か、C++11
多分にC++17
概念は、それを作る場合、私たちは別のコンパイルのいくつかの方法を持つことができ、。
個別のコンパイルを実現するには、個別のテンプレート本体チェックが可能である必要があります。コンセプトで解決できるようです。規格委員会で最近発表されたこの論文をご覧ください。テンプレートコードのコードをユーザーコードでインスタンス化する必要があるため、これが唯一の要件ではないと思います。
テンプレートの個別のコンパイルの問題これは、現在作業中のモジュールへの移行で発生している問題でもあると思います。
編集:2020年8月の時点で、モジュールはすでにC ++で現実のものとなっています。 https://en.cppreference.com/w/cpp/language/modules
上記には多くの良い説明がありますが、テンプレートをヘッダーと本文に分ける実用的な方法がありません。
私の主な関心事は、定義を変更するときに、すべてのテンプレートユーザーの再コンパイルを回避することです。
テンプレート本体にすべてのテンプレートインスタンス化を含めることは、私にとって実行可能な解決策ではありません。テンプレートの作成者は、その使用法とテンプレートユーザーがそれを変更する権利を持っていないかどうかをすべて知っているとは限らないからです。
私は次のアプローチを採用しました。これは古いコンパイラ(gcc 4.3.4、aCC A.03.13)でも機能します。
テンプレートの使用ごとに、独自のヘッダーファイル(UMLモデルから生成)にtypedefがあります。その本体にはインスタンス化が含まれています(これは、最後にリンクされているライブラリになります)。
テンプレートの各ユーザーはそのヘッダーファイルをインクルードし、typedefを使用します。
概略例:
MyTemplate.h:
#ifndef MyTemplate_h
#define MyTemplate_h 1
template <class T>
class MyTemplate
{
public:
MyTemplate(const T& rt);
void dump();
T t;
};
#endif
MyTemplate.cpp:
#include "MyTemplate.h"
#include <iostream>
template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}
template <class T>
void MyTemplate<T>::dump()
{
cerr << t << endl;
}
MyInstantiatedTemplate.h:
#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"
typedef MyTemplate< int > MyInstantiatedTemplate;
#endif
MyInstantiatedTemplate.cpp:
#include "MyTemplate.cpp"
template class MyTemplate< int >;
main.cpp:
#include "MyInstantiatedTemplate.h"
int main()
{
MyInstantiatedTemplate m(100);
m.dump();
return 0;
}
このようにして、すべてのテンプレートユーザー(および依存関係)ではなく、テンプレートのインスタンス化のみを再コンパイルする必要があります。
これは、テンプレートクラスのメソッド実装を定義する最も移植性の高い方法は、テンプレートクラス定義内でそれらを定義することであることを意味します。
template < typename ... >
class MyClass
{
int myMethod()
{
// Not just declaration. Add method implementation here
}
};
ここに注目すべきものを追加するだけです。テンプレート化されたクラスのメソッドは、関数テンプレートでない場合は、実装ファイルで問題なく定義できます。
myQueue.hpp:
template <class T>
class QueueA {
int size;
...
public:
template <class T> T dequeue() {
// implementation here
}
bool isEmpty();
...
}
myQueue.cpp:
// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
return this->size == 0;
}
main()
{
QueueA<char> Q;
...
}
懸念事項が、それを使用するすべての.cppモジュールの一部として.hをコンパイルすることによって生成される余分なコンパイル時間とバイナリサイズの肥大化である場合、多くの場合、テンプレートクラスをテンプレート化されていない基本クラスから派生させることができます。インターフェイスの型に依存しない部分であり、その基本クラスは.cppファイルに実装できます。
コンパイラは割り当てのタイプを認識している必要があるため、これは正確に正しいです。したがって、テンプレートクラス、関数、列挙型などは、ヘッダーファイルを公開する場合、またはライブラリの一部(静的または動的)にする場合は、ヘッダーファイルにも実装する必要があります。ヘッダーファイルは、c / cppファイルとは異なりコンパイルされないためです。です。コンパイラがタイプを知らない場合、それをコンパイルすることはできません。.Netでは、すべてのオブジェクトがObjectクラスから派生しているために可能です。これは.Netではありません。
コンパイルステップでテンプレートを使用すると、コンパイラはテンプレートのインスタンス化ごとにコードを生成します。コンパイルおよびリンクプロセスでは、main.cppに含まれる.hファイルにはまだ実装がないため、.cppファイルは参照または未定義のシンボルを含む純粋なオブジェクトまたはマシンコードに変換されます。これらは、テンプレートの実装を定義する別のオブジェクトファイルとリンクする準備ができているため、完全なa.out実行可能ファイルがあります。
ただし、定義したテンプレートのインスタンス化ごとにコードを生成するには、コンパイルステップでテンプレートを処理する必要があるため、ヘッダーファイルとは別にテンプレートをコンパイルするだけでは、テンプレートは常に手作業で行われるため、機能しません。各テンプレートのインスタンス化は、文字通りまったく新しいクラスです。通常のクラスでは、.hはそのクラスの青写真であり、.cppは生の実装であるため、.hと.cppを分離できます。これにより、実装ファイルを定期的にコンパイルおよびリンクできますが、テンプレートを使用すると、.hはその方法の青写真になります。クラスはオブジェクトがどのように見えるかではなく、テンプレート.cppファイルはクラスの生の通常の実装ではなく、単にクラスの青写真であるため、.hテンプレートファイルの実装はコンパイルできません。コンパイルするには具体的なものが必要です。テンプレートはその意味で抽象的です。
したがって、テンプレートが個別にコンパイルされることはなく、他のソースファイルで具体的なインスタンス化が行われている場合にのみコンパイルされます。ただし、具体的なインスタンス化では、テンプレートファイルの実装を知る必要があります。これtypename T
は、.hファイルで具体的な型を使用して変更するだけでは、リンクする.cppが何であるかがわからないため、機能しないためです。後で、テンプレートが抽象的でコンパイルできないことを覚えておいてください。そのため、今すぐ実装を提供する必要があります。これにより、何をコンパイルしてリンクするかがわかり、実装が含まれるソースファイルにリンクされます。基本的に、テンプレートをインスタンス化した瞬間に、まったく新しいクラスを作成する必要があります。提供した型を使用するときにそのクラスがどのように表示されるかわからない場合は、コンパイラに通知しない限り、それを行うことはできません。テンプレートの実装。これで、コンパイラーはT
私の型に置き換えて、コンパイルしてリンクする準備ができている具象クラスを作成できます。
要約すると、テンプレートはクラスの外観の青写真であり、クラスはオブジェクトの外観の青写真です。コンパイラは具体的な型のみをコンパイルするため、具体的なインスタンス化とは別にテンプレートをコンパイルすることはできません。つまり、少なくともC ++のテンプレートは、純粋な言語の抽象化です。テンプレートをいわば抽象化解除する必要があります。これは、テンプレートの抽象化が通常のクラスファイルに変換され、通常どおりにコンパイルできるように、処理する具体的な型をテンプレートに与えることによって行われます。テンプレート.hファイルとテンプレート.cppファイルを分離しても意味がありません。.cppと.hの分離は、.cppを個別にコンパイルして個別にリンクできる場合にのみ、テンプレートを個別にコンパイルできないため、テンプレートは抽象化されているため、常に強制されるため、無意味です。抽象化は常に具体的なインスタンス化と一緒に配置します。具体的なインスタンス化は、使用されているタイプについて常に知る必要があります。
意味typename T
getは、リンクステップではなくコンパイルステップT
で置き換えられるため、コンパイラにとってまったく意味のない具体的な値型として置き換えずにテンプレートをコンパイルしようとすると、オブジェクトコードが作成されないため、オブジェクトコードを作成できません。何であるかを知っていT
ます。
template.cppファイルを保存し、他のソースで見つかったときにタイプを切り替える何らかの機能を作成することは技術的に可能です。標準には、export
テンプレートを個別に配置できるキーワードがあると思います。 cppファイルですが、実際にこれを実装しているコンパイラはそれほど多くありません。
ちなみに、テンプレートクラスの特殊化を行う場合、ヘッダーを実装から分離できます。これは、定義による特殊化とは、個別にコンパイルおよびリンクできる具象型に特化していることを意味するためです。
個別に実装する方法は次のとおりです。
//inner_foo.h
template <typename T>
struct Foo
{
void doSomething(T param);
};
//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
//foo.h
#include <foo.tpp>
//main.cpp
#include <foo.h>
inner_fooには前方宣言があります。foo.tppには実装があり、inner_foo.hが含まれています。foo.hには、foo.tppを含めるための1行しかありません。
コンパイル時に、foo.hの内容がfoo.tppにコピーされ、ファイル全体がfoo.hにコピーされた後、コンパイルされます。このように、制限はなく、1つの追加ファイルと引き換えに、名前は一貫しています。
これを行うのは、コードの静的アナライザーが* .tppのクラスの前方宣言を認識しないと壊れてしまうためです。これは、IDEでコードを記述したり、YouCompleteMeなどを使用したりするときに煩わしいものです。
テンプレートのインスタンス化のための「cfront」モデルと「borland」モデルの間のトレードオフについて説明しているこのgccページを参照することをお勧めします。
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html
「ボーランド」モデルは、完全なテンプレート定義を提供し、物事を複数回コンパイルすることで、作者が提案したものに対応しています。
これには、手動および自動のテンプレートインスタンス化の使用に関する明示的な推奨事項が含まれています。たとえば、「-repo」オプションを使用して、インスタンス化する必要のあるテンプレートを収集できます。または、「-fno-implicit-templates」を使用して自動テンプレートインスタンス化を無効にして、手動テンプレートインスタンス化を強制することもできます。
私の経験では、(テンプレートライブラリを使用して)コンパイルユニットごとにインスタンス化されるC ++標準ライブラリとBoostテンプレートに依存しています。大規模なテンプレートクラスの場合、必要なタイプのテンプレートを手動でインスタンス化します。
他のプログラムで使用するためのテンプレートライブラリではなく、動作するプログラムを提供しているため、これが私のアプローチです。この本の著者であるJosuttisは、テンプレートライブラリに多くのことを取り組んでいます。
速度が本当に気になる場合は、プリコンパイル済みヘッダーを使用して検討すると思います。 https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
これは多くのコンパイラでサポートを得ています。ただし、テンプレートヘッダーファイルでは、プリコンパイル済みヘッダーは難しいと思います。