テンプレート化されたC ++クラスを.hpp / .cppファイルに分割することは可能ですか?


96

.hpp.cppファイルに分割されているC ++テンプレートクラスをコンパイルしようとするとエラーが発生します。

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

これが私のコードです:

stack.hpp

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

main.cpp

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ldもちろん正しいですstack.o。記号はにありません。

答えこの質問はそれが言うように、私はすでにやっているように、助けにはなりません。
これは役立つかもしれませんが、すべてのメソッドを.hppファイルに移動したくありません。そうする必要はありませんか?

.cppファイル内のすべてをファイルに移動.hppし、スタンドアロンオブジェクトファイルとしてリンクするのではなく、単にすべてを含めるための唯一の合理的な解決策はありますか?それはひどく醜いようです!その場合、以前の状態に戻り、名前stack.cppを変更してstack.hppそれで終了することもできます。


(バイナリファイルで)コードを本当に非表示にしたい場合、またはコードをクリーンにしたい場合には、2つの優れた回避策があります。最初の状況ではありますが、一般性を減らす必要があります。それは、ここで説明されていますstackoverflow.com/questions/495021/...
Sheric

回答:


151

テンプレートクラスの実装を別のcppファイルに記述してコンパイルすることはできません。そうするためのすべての方法は、誰かが主張する場合、個別のcppファイルの使用を模倣する回避策ですが、実際には、テンプレートクラスライブラリを作成し、それをヘッダーファイルとlibファイルと一緒に配布して実装を非表示にする場合は、単純に不可能です。

その理由を知るために、コンパイルプロセスを見てみましょう。ヘッダーファイルはコンパイルされません。それらは前処理されるだけです。次に、前処理されたコードは、実際にコンパイルされるcppファイルと結合されます。コンパイラがオブジェクトに適切なメモリレイアウトを生成する必要がある場合、テンプレートクラスのデータ型を知る必要があります。

実際には、テンプレートクラスはクラスではなく、引数からデータ型の情報を取得した後、コンパイル時にコンパイラによって宣言と定義が生成されるクラスのテンプレートであることを理解する必要があります。メモリレイアウトを作成できない限り、メソッド定義の命令は生成できません。クラスメソッドの最初の引数は「this」演算子であることを忘れないでください。すべてのクラスメソッドは、名前をマングリングし、最初のパラメーターを操作対象のオブジェクトとして個別のメソッドに変換されます。'this'引数は、ユーザーが有効な型引数を使用してオブジェクトをインスタンス化しない限り、コンパイラーがテンプレートクラスの場合に使用できないオブジェクトのサイズを実際に通知します。この場合、メソッド定義を別のcppファイルに入れてコンパイルしようとすると、オブジェクトファイル自体がクラス情報とともに生成されません。コンパイルは失敗せず、オブジェクトファイルを生成しますが、オブジェクトファイルにテンプレートクラスのコードを生成しません。これが、リンカーがオブジェクトファイル内のシンボルを見つけられず、ビルドが失敗する理由です。

重要な実装の詳細を隠す代替手段は何ですか?インターフェースを実装から分離することの背後にある主な目的は、バイナリ形式で実装の詳細を隠すことです。ここで、データ構造とアルゴリズムを分離する必要があります。テンプレートクラスは、アルゴリズムではなくデータ構造のみを表す必要があります。これにより、テンプレート化されていない別のクラスライブラリ(テンプレートクラスで機能するクラス、またはデータを保持するためにそれらを使用するクラス)に、より価値のある実装の詳細を隠すことができます。テンプレートクラスには、データを割り当て、取得、設定するためのコードが実際には含まれていません。残りの作業は、アルゴリズムクラスによって行われます。

この議論がお役に立てば幸いです。


2
「テンプレートクラスはクラスではないことを理解しなければならない」-それは逆ではないですか?クラステンプレートはテンプレートです。「テンプレートクラス」は「テンプレートのインスタンス化」の代わりに使用されることがあり、実際のクラスになります。
Xupicor 2016年

参考までに、回避策がないと言うのは正しくありません!データ構造をメソッドから分離することも、カプセル化によって反対されるため、悪い考えです。いくつかの状況で使用できる優れた回避策があります(私は最も信じています)。ここでは、stackoverflow.com
questions / 495021 /…

@Xupicor、あなたは正しい。技術的には「クラステンプレート」は、「テンプレートクラス」とそれに対応するオブジェクトをインスタンス化できるように記述するものです。ただし、一般的な用語では、両方の用語を交換可能に使用することはそれほど間違っていないと思います。「クラステンプレート」自体を定義する構文は、「クラス」ではなく「テンプレート」という単語で始まります。
Sharjith N. 2017

@シェリック、私は回避策がないとは言いませんでした。実際、利用できるのは、テンプレートクラスの場合にインターフェースと実装の分離を模倣するための回避策だけです。これらの回避策は、特定の型付きテンプレートクラスをインスタンス化しないと機能しません。とにかく、クラステンプレートを使用するという一般性の要点はすべて解消されます。データ構造をアルゴリズムから分離することは、データ構造をメソッドから分離することと同じではありません。データ構造クラスには、コンストラクター、ゲッター、セッターなどのメソッドを含めることができます。
Sharjith N. 2017

この作業を行うために私が見つけた最も近いものは、.h / .hppファイルのペアを使用し、テンプレートクラスを定義する.hファイルの最後に#include "filename.hpp"を使用することです。(セミコロンのあるクラス定義の閉じ括弧の下)。これは、少なくとも構造的にそれらをファイルごとに分離し、最終的にコンパイラーが.hppコードを#include "filename.hpp"の上にコピー/貼り付けするので許可されます。
Artorias2718

90

必要なインスタンス化がわかっている限り、それ可能です。

stack.cppの最後に次のコードを追加すると、機能します。

template class stack<int>;

テンプレート以外のスタックのメソッドはすべてインスタンス化され、リンク手順は正常に機能します。


7
実際には、ほとんどの人はこのために個別のcppファイルを使用します-stackinstantiations.cppのようなもの。
Nemanja Trifunovic

@NemanjaTrifunovicは、stackinstantiations.cppがどのように見えるかの例を示すことができますか?
qwerty9967 2013年

3
実際には他のソリューションがあります。codeproject.com/Articles/48575/...
sleepsort

@Benoîtエラーが発生しました: ';'の前にunqualified-idが必要です トークンテンプレートスタック<整数>; なぜなのかご存知ですか?ありがとう!
camino 2013年

3
実際、正しい構文はtemplate class stack<int>;です。
Paul Baltescu 2014年

8

このようにできます

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

これは、Daniwebで議論されています

また、中によくある質問が、C ++ exportキーワードを使用して。


5
includecppファイルを保存することは、一般的にひどい考えです。正当な理由がある場合でも、ファイル(実際には栄光のヘッダーにすぎません)には、何が起こっているのかを非常に明確にするために、hppまたは実際のファイルを対象とするsのtpp混乱を取り除くために、異なる拡張子(たとえば)を付ける必要があります。makefile cpp
underscore_d

@underscore_d .cppファイルを含めることがひどい考えである理由を説明していただけますか?
Abbas

1
@Abbas延長ためcpp(又はcc、あるいはc、または何でも)、得られた翻訳単位(プリプロセッサの出力)が別々にコンパイル可能であること、およびファイルの内容を一度のみコンパイルされていること、ファイルは、実装の一部であることを示しています。ファイルがインターフェースの再利用可能な部分であり、任意の場所に含まれることを示すものではありません。#includeINGの実際の cppファイルはすぐに当然のように複数の定義のエラーで画面を埋め、そしてでしょう。この場合には、そこにようであるとの理由#include、それは、cpp拡張のちょうど間違った選択でした。
underscore_d 2016

@underscore_dしたがって、基本的にその.cppような用途に拡張機能を使用することは間違っています。しかし、別の発言を使用すること.tppは完全に大丈夫です。これは同じ目的を果たしますが、理解を容易にするために別の拡張子を使用しますか?
Abbas

1
はい、@Abbas cpp/ ccの/ etc避け、それは以外の使用のものには良いアイデアだしなければならないhpp-例えばtpptccファイル名の残りの部分を再利用していることを示すことができるように-などtpp、それはヘッダのように働くものの、ファイルを対応するのテンプレート宣言のアウトオブライン実装を保持しhppます。したがって、この投稿は良い前提で始まります-宣言と定義を2つの異なるファイルに分離します。これにより、grok / grepが容易になるか、循環依存IMEのために必要になる場合がありますが、2番目のファイルの拡張子が間違っていることを示唆することで、ひどく終わります。
underscore_d

6

いいえ、それは不可能です。ずなどexport、すべての意図や目的のために実際に存在しないキーワード。

できる最善の方法は、関数の実装を ".tcc"または ".tpp"ファイルに入れ、.hppファイルの末尾に.tccファイルを#includeすることです。ただし、これは単なる表面的なものです。それでも、ヘッダーファイルにすべてを実装するのと同じです。これは単に、テンプレートを使用するために支払う価格です。


3
あなたの答えは正しくありません。使用するテンプレート引数がわかっていれば、cppファイルのテンプレートクラスからコードを生成できます。詳細については、私の回答を参照してください。
ブノワ

2
確かに、しかしこれには、テンプレートを使用する新しいタイプが導入されるたびに.cppファイルを更新して再コンパイルする必要があるという深刻な制限が伴います。
Charles Salvia、

3

テンプレート化されたコードをヘッダーとcppに分離しようとする主な理由は2つあると思います。

1つは単なる優雅さのためです。私たちは皆、読み取り、管理が面倒で、後で再利用できるコードを書くのが好きです。

その他は、コンパイル時間の短縮です。

私は現在(いつものように)OpenCLと組み合わせてシミュレーションソフトウェアをコーディングしており、HW機能に応じて必要に応じてfloat(cl_float)またはdouble(cl_double)型を使用して実行できるようにコードを保持したいと考えています。現在、これはコードの先頭で#define REALを使用して行われていますが、あまりエレガントではありません。必要な精度を変更するには、アプリケーションを再コンパイルする必要があります。実際の実行時の型はないので、当分の間、これに対応する必要があります。幸いなことに、OpenCLカーネルはコンパイルされたランタイムであり、単純なsizeof(REAL)によってカーネルコードランタイムを適宜変更できます。

さらに大きな問題は、アプリケーションがモジュール式であっても、補助クラス(シミュレーション定数を事前に計算するクラスなど)を開発するときにテンプレート化する必要があることです。これらのクラスはすべて、クラスの依存関係ツリーの最上位に少なくとも1回表示されます。最終的なテンプレートクラスSimulationには、これらのファクトリクラスのいずれかのインスタンスが含まれます。つまり、ファクトリクラスにマイナーな変更を加えるたびに、ソフトウェアを再構築する必要があります。これは非常に迷惑ですが、もっと良い解決策を見つけることができないようです。


2

#include "stack.cppの終わりにのみstack.hpp。このアプローチは、実装が比較的大きく、.cppファイルの名前を通常のコードと区別するために別の拡張子に変更する場合にのみお勧めします。


4
これを行っている場合は、#ifndef STACK_CPP(およびフレンド)をstack.cppファイルに追加する必要があります。
スティーブンニューウェル

この提案に私を負かしてください。私もスタイル上の理由でこのアプローチを好まない。
ルーク

2
はい、そのような場合、2番目のファイルには拡張子cpp(またはccその他)を絶対に付けないでください。これは、実際の役割とはまったく対照的だからです。代わりに、(A)ヘッダーおよび(B)別のヘッダーの下部に含まれるヘッダーであることを示す別の拡張子を指定する必要があります。私が使用tpp手近にもために立つことができ、このためにtEM p後半イムplementation(アウトオブラインの定義)。:私はもっとここにこの程度rambled stackoverflow.com/questions/1724036/...
underscore_d

2

場合によっては、cppファイルでほとんどの実装を非表示にすることが可能です。すべてのテンプレートパラメーターで共通の機能を非テンプレートクラス(おそらくタイプアンセーフ)に抽出できる場合。その後、ヘッダーにはそのクラスへのリダイレクト呼び出しが含まれます。「テンプレート膨張」問題と戦う場合も、同様のアプローチが使用されます。


+
1-

2

スタックがどのタイプで使用されるかがわかっている場合は、それらをcppファイルで明示的にインスタンス化し、関連するすべてのコードをそこに保持できます。

DLL間でこれらをエクスポートすることも可能です(!)が、構文を正しくするのはかなりトリッキーです(__declspec(dllexport)とエクスポートキーワードのMS固有の組み合わせ)。

これをdouble / floatをテンプレート化した数学/ geomライブラリで使用しましたが、コードがかなり多くありました。(私は当時ググってみましたが、今日はそのコードはありません。)


2

問題は、テンプレートが実際のクラスを生成しないことです。これは、クラスを生成する方法をコンパイラーに伝える単なるテンプレートです。具象クラスを生成する必要があります。

簡単で自然な方法は、ヘッダーファイルにメソッドを配置することです。しかし、別の方法があります。

.cppファイルで、必要なすべてのテンプレートのインスタンス化とメソッドへの参照がある場合、コンパイラーはそれらを生成してプロジェクト全体で使用します。

新しいstack.cpp:

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}

8
dummey関数は必要ありません。「template stack <int>;」を使用してください。これにより、テンプレートが現在のコンパイル単位に強制的にインスタンス化されます。テンプレートを定義しているが、共有ライブラリーに特定の実装をいくつかだけ必要とする場合に非常に役立ちます。
マーティンヨーク

@Martin:すべてのメンバー関数を含みますか?それは素晴らしいです。この提案を「非表示のC ++機能」スレッドに追加する必要があります。
Mark Ransom

@LokiAstari誰かがもっと知りたい場合に備えて、この記事を見つけました:cplusplus.com/forum/articles/14272
Andrew Larsson

1

すべてをhppファイルに含める必要があります。問題は、クラスが実際に作成されないのは、コンパイラが他のcppファイルで必要であるとコンパイラが認識するまでです。そのため、テンプレート化されたクラスをコンパイルするには、すべてのコードを利用できる必要があります。

テンプレートを一般的な非テンプレートパーツ(cpp / hppに分割できる)と非テンプレートクラスを継承するタイプ固有のテンプレートパーツに分割しようとする傾向があります。


0

テンプレートは必要に応じてコンパイルされるため、マルチファイルプロジェクトには制限が課せられます。テンプレートクラスまたは関数の実装(定義)は、その宣言と同じファイルになければなりません。つまり、インターフェイスを別のヘッダーファイルに分離することはできず、テンプレートを使用するファイルにはインターフェイスと実装の両方を含める必要があります。


0

別の可能性は次のようなことです:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

私はこの提案がスタイルの問題として嫌いですが、あなたに合うかもしれません。


1
インクルードされる栄光の2番目のヘッダーには、実際のソースファイルcpp混同を避けるため以外に、少なくとも拡張子が必要です。一般的な提案には、tppおよびが含まれtccます。
underscore_d

0

'export'キーワードは、テンプレートの実装とテンプレートの宣言を区別する方法です。これは、既存の実装なしでC ++標準で導入されました。やがて、実際にそれを実装したのは2、3のコンパイラだけでした。詳細情報を読むInform IT article exportの


1
これはほぼリンクのみの回答であり、そのリンクは無効です。
underscore_d

0

1).hファイルと.cppファイルを分離する主な理由は、クラスの.hを含むユーザーのコードにリンクできる個別にコンパイルされたObjコードとしてクラス実装を隠すためです。

2)非テンプレートクラスには、すべての変数が.hおよび.cppファイルで具体的かつ具体的に定義されています。そのため、コンパイラーは、コンパイル/変換前にクラスで使用されるすべてのデータ型に関する情報を必要とします。タイプ:

        TClass<int> myObj;

3)このインスタンス化の後でのみ、コンパイラーは渡されたデータ型に一致するテンプレートクラスの特定のバージョンを生成します。

4)したがって、.cppは、ユーザー固有のデータ型を知らずに個別にコンパイルすることはできません。したがって、ユーザーが必要なデータ型を指定するまで「.h」内にソースコードとしてとどまる必要があり、特定のデータ型に生成してコンパイルできます。


-3

私はVisual Studio 2010で作業しています。ファイルを.hと.cppに分割したい場合は、.hファイルの最後にcppヘッダーを含めます

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.