C ++テンプレート関数定義を.CPPファイルに保存する


526

ヘッダーのインラインではなく、CPPファイルに保存したいテンプレートコードがあります。使用されるテンプレートの種類がわかっている限り、これを実行できます。例えば:

.hファイル

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

.cppファイル

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

最後の2行に注意してください。foo:: doテンプレート関数は、intとstd :: stringsでのみ使用されるため、これらの定義はアプリがリンクすることを意味します。

私の質問は-これは厄介なハックですか、それとも他のコンパイラ/リンカーで動作しますか?現在、このコードはVS2008でのみ使用していますが、他の環境に移植したいと考えています。


22
これが可能であるとは思いもしませんでした-興味深いトリックです!これは、これを知ることで相当な最近のいくつかのタスクに役立ちました-乾杯
08:16

69
私を踏みつけるのは、do識別子としての使用法です:p
Quentin

私はgccと同様のサマリングを行いましたが、まだ研究中です
Nick

16
これは「ハック」ではありません。前方非難です。これは言語の標準で場所があります。そう、それはすべての標準準拠コンパイラで許可されています。
Ahmet Ipkin

1
何十ものメソッドがある場合はどうなりますか?template class foo<int>;template class foo<std::string>;.cppファイルの最後で実行できますか?
無視

回答:


231

あなたが説明する問題は、ヘッダーでテンプレートを定義することによって、または上で説明したアプローチによって解決できます。

C ++ FAQ Liteから次の点を読むことをお勧めします。

彼らは、これら(およびその他)のテンプレートの問題について多くの詳細を説明しています。


39
答えを補足するために、参照されているリンクは質問に肯定的に答えます。つまり、Robが提案したことを実行して、コードを移植可能にすることができます。
ivotron、2011年

161
回答自体に関連する部分を投稿できますか?SOでそのような参照が許可されているのはなぜですか。このリンクは大幅に変更されているため、このリンクで何を探すべきかわかりません。
2015

124

このページの他の人は、明示的なテンプレートの特殊化(または少なくともVS2008の場合)の正しい構文は(Iと同じように)何なのかと疑問に思っている場合、次のように...

.hファイルで...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

そして、あなたの.cppファイル

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;

15
「明示的なCLASSテンプレートの特殊化のため」という意味ですか。その場合、それはテンプレート化されたクラスが持つすべての関数をカバーしますか?
アーサー

@Arthurはそうではありません、私はいくつかのテンプレートメソッドをヘッダーに残し、cppの他のほとんどのメソッドはうまくいきます。とても良い解決策です。
user1633272

質問者の場合、クラステンプレートではなく、関数テンプレートがあります。
user253751

23

このコードは整形式です。テンプレートの定義がインスタンス化の時点で表示されることに注意する必要があります。標準を引用すると、§14.7.2.4:

エクスポートされていない関数テンプレート、エクスポートされていないメンバー関数テンプレート、またはクラステンプレートのエクスポートされていないメンバー関数または静的データメンバーの定義は、明示的にインスタンス化されるすべての翻訳単位に存在する必要があります。


2
何をしない非エクスポート平均?
Dan Nissenbaum、2014年

1
@Danコンパイルユニットの内部でのみ表示され、外部では表示されません。複数のコンパイルユニットをリンクすると、エクスポートされたシンボルをそれら全体で使用できます(テンプレートの場合、単一の、または少なくとも、一貫した定義が必要です。それ以外の場合は、UBに実行します)。
Konrad Rudolph

ありがとう。すべての関数は(デフォルトでは)コンパイル単位の外に表示されると思いました。2つのコンパイル単位a.cpp(関数の定義a() {})とb.cpp(関数の定義b() { a() })がある場合、これは正常にリンクします。私が正しい場合、上記の引用は典型的なケースには当てはまらないようです...私はどこか間違っているのですか?
Dan Nissenbaum、2014年

@Dan Trivialの反例:inline関数
Konrad Rudolph

1
@Dan関数テンプレートは暗黙的に使用されinlineます。その理由は、標準化されたC ++ ABIがなければ、これがなければ影響を与えることを定義することは困難/不可能だからです。
Konrad Rudolph

15

これは、テンプレートがサポートされているすべての場所で正常に動作するはずです。明示的なテンプレートのインスタンス化は、C ++標準の一部です。


13

あなたの例は正しいですが、あまり移植性がありません。(@ namespace-sidで指摘されているように)使用できる少しすっきりした構文もあります。

テンプレートクラスが、共有されるライブラリの一部であるとします。テンプレートクラスの他のバージョンをコンパイルする必要がありますか?ライブラリのメンテナは、クラスのテンプレート化されたすべての使用を予測することになっていますか?

別のアプローチは、あなたが持っているもののわずかなバリエーションです:テンプレート実装/インスタンス化ファイルである3番目のファイルを追加します。

foo.hファイル

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

foo.cppファイル

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

foo-impl.cppファイル

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

1つの警告は、あなたがコンパイルするコンパイラ指示する必要がありますということであるfoo-impl.cpp代わりにfoo.cpp、後者は何もしないコンパイルなどを。

もちろん、3番目のファイルに複数の実装を含めることも、使用するタイプごとに複数の実装ファイルを含めることもできます。

これにより、テンプレートクラスを他の用途に共有する際の柔軟性が大幅に向上します。

このセットアップでは、各翻訳単位で同じヘッダーファイルを再コンパイルしないため、再利用されたクラスのコンパイル時間も短縮されます。


これで何が買えるの?新しい特殊化を追加するために、foo-impl.cppを編集する必要があります。
MK。

foo.cppバージョンが実際にコンパイルされる元の実装詳細(別名の定義foo-impl.cpp)と宣言()の分離foo.h。ほとんどのC ++テンプレートが完全にヘッダーファイルで定義されているのが嫌いです。これは、c[pp]/h使用するすべてのクラス/名前空間/グループのペアのC / C ++標準に反しています。この代替策が広く使用されていない、または知られていないという理由だけで、人々は依然としてモノリシックヘッダーファイルを使用しているようです。
Cameron Tacklind 2017年

1
@MK。他の場所でさらにインスタンス化が必要になるまで(たとえば、テンプレート型としてモックを使用した単体テスト)、最初にソースファイルの定義の最後に明示的なテンプレートインスタンス化を配置していました。この分離により、外部にインスタンス化を追加できます。さらに、h/cpp元のインスタンス化のリストをインクルードガードで囲む必要がありましたが、元のペアをペアとして保持した場合でも機能しますがfoo.cpp、通常どおりコンパイルできます。私はまだC ++にかなり慣れていないので、この混合使用に追加の警告があるかどうかを知りたいと思います。
Thirdwater 2018

3
私はそれが分離することが望ましいと考えるfoo.cppfoo-impl.cpp。ファイルには入れない#include "foo.cpp"でくださいfoo-impl.cpp。代わりに、に宣言extern template class foo<int>;を追加してfoo.cpp、コンパイラーがコンパイル時にテンプレートをインスタンス化しないようにしfoo.cppます。ビルドシステムが両方の.cppファイルをビルドし、両方のオブジェクトファイルをリンカーに渡すことを確認し ます。これには複数の利点foo.cppがあります。a)インスタンス化がないことは明らかです。b)foo.cppへの変更は、foo-impl.cppの再コンパイルを必要としません。
Shmuel Levine

3
これは、ヘッダーの実装と頻繁に使用される型のインスタンス化の両方の利点を生かしたテンプレート定義の問題に対する非常に優れたアプローチです。私はこの設定になるだろう唯一の変更は、名前を変更することがあるfoo.cppfoo_impl.hしてfoo-impl.cppばかりにfoo.cpp。同様foo.cppfoo.h、to からインスタンス化するためのtypedefも追加しますusing foo_int = foo<int>;。トリックは、ユーザーに選択肢として2つのヘッダーインターフェイスを提供することです。ユーザーが事前定義済みのインスタンス化をfoo.h必要とする場合は、インクルードするfoo_impl.h
ウォーマー

5

これは間違いなく厄介なハックではありませんが、特定のテンプレートで使用するすべてのクラス/タイプに対してこれを行う必要がある(明示的なテンプレートの特殊化)ことに注意してください。テンプレートのインスタンス化を要求する多くのタイプの場合、.cppファイルに多数の行が含まれる可能性があります。この問題を解決するには、使用するすべてのプロジェクトにTemplateClassInst.cppを設定して、インスタンス化されるタイプをより細かく制御できるようにします。明らかに、ODRを壊してしまう可能性があるため、このソリューションは完璧ではありません(別名:銀の弾丸)。


ODRを壊すことは確かですか?TemplateClassInst.cppのインスタンス化行が(テンプレート関数定義を含む)同一のソースファイルを参照している場合、すべての定義が同一であるため(たとえ繰り返されても)、ODRに違反しないことが保証されていませんか?
Dan Nissenbaum、2014年

ODRとは何ですか?
取り外し

4

最新の標準には、exportこの問題を緩和するのに役立つキーワード()がありますが、私が知っているコモー以外のコンパイラには実装されていません。

これについてはFAQ-liteを参照してください


2
申し訳ありませんが、最後の問題を解決するたびに新しい問題が発生し、ソリューション全体がますます複雑になるため、エクスポートは停止しています。また、「export」キーワードを使用しても、CPPから(とにかくH. Sutterから)「エクスポート」することはできません。だから私は言う:あなたの息を止めないでください...
paercebal '22

2
エクスポートを実装するには、コンパイラに完全なテンプレート定義が必要です。あなたが得るすべては、一種のコンパイルされた形でそれを持っていることです。しかし、実際にはそれに意味がありません。
Zan Lynx

2
...そして、最小限のゲインのための過度の複雑さのために、それは標準から消えました
DevSolar

4

これは、テンプレート関数を定義する標準的な方法です。テンプレートを定義するために私が読んだ方法は3つあると思います。またはおそらく4。それぞれ長所と短所。

  1. クラス定義で定義します。クラス定義はあくまでも参照用であり、読みやすいはずなので、これはまったく好きではありません。ただし、クラスを外部で定義するよりも、テンプレートを定義する方が簡単です。また、すべてのテンプレート宣言が同じレベルの複雑さにあるわけではありません。このメソッドは、テンプレートを真のテンプレートにします。

  2. 同じヘッダー内で、クラスの外でテンプレートを定義します。ほとんどの場合、これが私の好みの方法です。それはあなたのクラス定義をきちんと保ちます、テンプレートは本当のテンプレートのままです。ただし、テンプレートを完全に命名する必要があるため、注意が必要です。また、コードはすべての人が利用できます。ただし、コードをインラインにする必要がある場合は、これが唯一の方法です。これは、クラス定義の最後に.INLファイルを作成することでも実現できます。

  3. header.hとimplementation.CPPをmain.CPPに含めます。それがそのやり方だと思います。事前インスタンス化を準備する必要はありません。本当のテンプレートのように動作します。私が持っている問題はそれが自然ではないということです。通常、ソースファイルはインクルードせず、インクルードする予定です。ソースファイルをインクルードしたので、テンプレート関数をインライン化できると思います。

  4. 投稿された方法であるこの最後の方法は、3のように、ソースファイルでテンプレートを定義しています。ただし、ソースファイルを含める代わりに、必要なテンプレートにテンプレートを事前にインスタンス化します。私はこの方法で問題がなく、時々重宝します。大きなコードが1つあるので、インライン化してもメリットがないので、CPPファイルに入れてください。また、一般的なインスタンス化を知っていて、それらを事前定義できる場合。これにより、基本的に同じことを5、10回書く必要がなくなります。この方法には、コードを独占的に保つという利点があります。ただし、CPPファイルに定期的に使用される小さな関数を含めることはお勧めしません。これにより、ライブラリのパフォーマンスが低下します。

注、私は肥大化したobjファイルの結果を認識していません。


3

はい、それは特殊化の明示的なインスタンス化を行う標準的な方法です。あなたが述べたように、このテンプレートを他のタイプでインスタンス化することはできません。

編集:コメントに基づいて修正。


用語にこだわるのは「明示的なインスタンス化」です。
Richard Corden

2

一例を挙げましょう。何らかの理由でテンプレートクラスが必要だとします。

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

このコードをVisual Studioでコンパイルする場合-そのまま使用できます。gccはリンカーエラーを生成します(複数の.cppファイルから同じヘッダーファイルが使用されている場合):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

実装を.cppファイルに移動することは可能ですが、その場合は次のようにクラスを宣言する必要があります-

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

そして、.cppは次のようになります。

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

ヘッダーファイルに最後の2行がない場合、gccは正常に動作しますが、Visual Studioはエラーを生成します。

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

.dllエクスポートを介して関数を公開する場合、テンプレートクラスの構文はオプションですが、これはWindowsプラットフォームにのみ適用できるため、test_template.hは次のようになります。

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

前の例の.cppファイルを使用します。

ただし、これによりリンカーの頭痛が増えるため、.dll関数をエクスポートしない場合は前の例を使用することをお勧めします。


1

更新の時間です!インライン(.inl、またはおそらく他の)ファイルを作成し、その中のすべての定義をコピーします。必ず各関数の上にテンプレートを追加してください(template <typename T, ...>)。ここで、ヘッダーファイルをインラインファイルに含める代わりに、逆の処理を行います。クラスの宣言の後にインラインファイルを含めます(#include "file.inl")。

なぜ誰もこれについて言及しなかったのか、本当にわかりません。私には差し迫った欠点はありません。


25
直接的な欠点は、テンプレート関数をヘッダーで直接定義するのと基本的に同じであることです。したら#include "file.inl"、プリプロセッサはの内容をfile.inl直接ヘッダーに貼り付けます。ヘッダーでの実装を避けたい理由が何であれ、このソリューションはその問題を解決しません。
コーディグレイ

5
- そして、技術的に不必要に、アウトオブラインのtemplate定義に必要な冗長で心を曲げるすべてのボイラープレートを作成するタスクで自分に負担をかけていること意味します。私は人々がそれをしたい理由を得ます-非テンプレート宣言/定義との最も同等を達成するために、インターフェース宣言を整然とした見た目にするためなど-しかし、それは常に面倒の価値があるわけではありません。それは双方のトレードオフを評価し、最も悪いものを選ぶケースです。... namespace classモノになるまで:O [ モノになってください ]
underscore_d

2
@Andrew委員会のパイプに行き詰まっているようですが、意図的ではないと言っている人を見たと思います。それがC ++ 17になったといいのですが。たぶん次の10年。
underscore_d

@CodyGray:技術的には、これは実際にはコンパイラーと同じであり、したがってコンパイル時間を短縮していません。それでも、これは言及する価値があり、私が見た多くのプロジェクトで実践されていると思います。この道を進むと、インターフェースを定義から分離するのに役立ちます。これは良い習慣です。この場合、ABIの互換性などには役立ちませんが、インターフェイスの読み取りと理解が容易になります。
キロアルファインディア

0

あなたが与えた例には何の問題もありません。しかし、関数定義をcppファイルに保存するのは効率的ではないと私は信じなければなりません。関数の宣言と定義を分離する必要性だけを理解しています。

Boost Concept Check Library(BCCL)を明示的なクラスのインスタンス化と一緒に使用すると、cppファイルでテンプレート関数コードを生成できます。


8
それについて何が非効率的ですか?
コーディグレイ

0

上記のどれも私にとってはうまくいかなかったので、ここでそれを解決する方法を示します。私のクラスにはテンプレート化されたメソッドが1つしかありません

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

これにより、リンカーエラーが回避され、TemporaryFunctionを呼び出す必要がまったくなくなります。

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