明示的なテンプレートのインスタンス化-いつ使用されますか?


95

数週間の休憩の後、私は本「テンプレート-デビッドヴァンデヴォーデとニコライM.ジョスティスによる完全ガイド」でテンプレートの知識を拡大および拡張しようとしています。現時点で理解しようとしているのは、テンプレートの明示的なインスタンス化です。 。

メカニズム自体には問題はありませんが、この機能を使いたい、あるいは使いたいという状況は想像できません。誰かが私にそれを説明することができれば、私はもっと感謝するでしょう。

回答:


67

https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiationから直接コピー:

明示的なインスタンス化を使用して、コードで実際に使用せずに、テンプレート化されたクラスまたは関数のインスタンス化を作成できます。これは、配布にテンプレート使用するライブラリ(.lib)ファイルを作成するときに役立つため、インスタンス化されていないテンプレート定義はオブジェクト(.obj)ファイルに入れられません。

(たとえば、libstdc ++にはstd::basic_string<char,char_traits<char>,allocator<char> >(のstd::string)の明示的なインスタンス化が含まれているためstd::string、の関数を使用するたびに、同じ関数コードをオブジェクトにコピーする必要はありません。コンパイラは、それらをlibstdc ++に参照(リンク)するだけで済みます。)


7
はい、MSVC CRTライブラリには、charおよびwchar_tに特化した、すべてのストリーム、ロケール、および文字列クラスの明示的なインスタンス化があります。結果の.libは5メガバイトを超えます。
ハンスパッサント2010

4
テンプレートが他の場所で明示的にインスタンス化されていることをコンパイラはどのようにして知るのですか?利用可能であるため、クラス定義を生成するだけではありませんか?

@STing:テンプレートがインスタンス化されると、シンボルテーブルにそれらの関数のエントリがあります。
kennytm 2010年

@ケニー:同じTUですでにインスタンス化されているかどうかということですか?どのコンパイラも、同じTUで同じ特殊化を複数回インスタンス化しないほど賢いと思います。(ビルド/リンク時間に関して)明示的なインスタンス化の利点は、特殊化が1つのTUで(明示的に)インスタンス化された場合、それが使用される他のTUではインスタンス化されないことだと思いました。番号?

4
@Kenny:暗黙のインスタンス化を防ぐためのGCCオプションについては知っていますが、これは標準ではありません。私の知る限り、VC ++にはそのようなオプションはありません。明示的なinst。は常にコンパイル/リンク時間を改善すると宣伝されていますが(Bjarneによっても)、その目的を果たすために、コンパイラはテンプレートを暗黙的にインスタンス化しないことを何らかの方法で知っている必要があります(たとえば、GCCフラグを介して)。テンプレート定義、宣言のみ。この音は正しいですか?(具体的なタイプを制限する以外に)明示的なインスタンス化を使用する理由を理解しようとしています。

84

いくつかの明示的な型に対してのみ機能するテンプレートクラスを定義する場合。

通常のクラスと同じように、テンプレート宣言をヘッダーファイルに配置します。

通常のクラスと同じように、テンプレート定義をソースファイルに配置します。

次に、ソースファイルの最後で、使用可能にしたいバージョンのみを明示的にインスタンス化します。

愚かな例:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

ソース:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

メイン

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}

1
それはコンパイラが与えられた翻訳単位で(関数の定義を含む)全体のテンプレート定義を持っている場合、それはと言うことは正しいですます(関係なく、その専門分野がされているかどうかの必要なときに、テンプレートの特殊化をインスタンス化し、明示的に別のTUでインスタンス)?つまり、明示的なインスタンス化のコンパイル/リンク時の利点を享受するには、コンパイラインスタンス化できないように、テンプレート宣言のみを含める必要がありますか?

1
@ user123456:おそらくコンパイラに依存します。しかし、ほとんどの状況でおそらく真実です。
マーティンヨーク

1
コンパイラに、事前に指定した型に対してこの明示的にインスタンス化されたバージョンを使用させる方法はありますが、「奇妙な/予期しない」型でテンプレートをインスタンス化しようとする場合は、「通常どおり」動作させます。必要に応じてテンプレートをインスタンス化しますか?
デビッドドリア

2
明示的なインスタンス化が実際に使用されていることを確認するための適切なチェック/テストは何でしょうか?つまり、機能していますが、すべてのテンプレートをオンデマンドでインスタンス化するだけではないことを完全には確信していません。
デビッドドリア

7
c ++ 11以降、上記のコメントのおしゃべりのほとんどは当てはまりません。明示的なインスタンス化宣言(externテンプレート)は暗黙的なインスタンス化を防止します。暗黙的なインスタンス化を引き起こすコードは、他の場所で提供される明示的なインスタンス化定義を使用する必要があります。プログラム(通常、別のファイル内:これを使用してコンパイル時間を短縮できます) en.cppreference.com/w/cpp/language/class_template
xaxxon 2016

21

明示的なインスタンス化により、コンパイル時間とオブジェクトサイズを削減できます

これらはそれが提供できる主な利益です。これらは、以下のセクションで詳細に説明されている次の2つの効果に由来します。

  • ヘッダーから定義を削除して、ビルドツールがインクルーダーを再構築しないようにします
  • オブジェクトの再定義

ヘッダーから定義を削除する

明示的なインスタンス化により、.cppファイルに定義を残すことができます。

定義がヘッダーにあり、それを変更すると、インテリジェントビルドシステムがすべてのインクルーダーを再コンパイルします。これは数十のファイルになる可能性があり、コンパイルが耐えられないほど遅くなります。

.cppファイルに定義を配置することには、外部ライブラリが独自の新しいクラスでテンプレートを再利用できないという欠点がありますが、以下の「含まれるヘッダーから定義を削除するだけでなく、テンプレートを外部APIに公開する」は回避策を示しています。

以下の具体例を参照してください。

オブジェクトの再定義のメリット:問題の理解

ヘッダーファイルでテンプレートを完全に定義した場合、そのヘッダーを含むすべてのコンパイルユニットは、異なるテンプレート引数の使用ごとに、テンプレートの独自の暗黙的なコピーをコンパイルすることになります。

これは、多くの無駄なディスク使用量とコンパイル時間を意味します。

これは具体的な例であり、これらのファイルでの使用により、main.cppとがnotmain.cpp暗黙的に定義MyTemplate<int>されています。

main.cpp

#include <iostream>

#include "mytemplate.hpp"
#include "notmain.hpp"

int main() {
    std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}

notmain.cpp

#include "mytemplate.hpp"
#include "notmain.hpp"

int notmain() { return MyTemplate<int>().f(1); }

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

template<class T>
struct MyTemplate {
    T f(T t) { return t + 1; }
};

#endif

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

int notmain();

#endif

GitHubアップストリーム

nm:を使用してシンボルをコンパイルおよび表示します。

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate

出力:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

からman nmWこれはテンプレート関数であるためGCCが選択した弱い記号を意味することがわかります。弱い記号は、コンパイルされた暗黙的に生成されたコードMyTemplate<int>が両方のファイルでコンパイルされたことを意味します。

複数の定義でリンク時に爆発しない理由は、リンカーが複数の弱い定義を受け入れ、そのうちの1つを選択して最終的な実行可能ファイルに入れるためです。

出力の数値は次のことを意味します。

  • 0000000000000000:セクション内のアドレス。このゼロは、テンプレートが自動的に独自のセクションに配置されるためです
  • 0000000000000017:それらのために生成されたコードのサイズ

これは、次のようにしてもう少し明確に確認できます。

objdump -S main.o | c++filt

で終わる:

Disassembly of section .text._ZN10MyTemplateIiE1fEi:

0000000000000000 <MyTemplate<int>::f(int)>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   89 75 f4                mov    %esi,-0xc(%rbp)
   f:   8b 45 f4                mov    -0xc(%rbp),%eax
  12:   83 c0 01                add    $0x1,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

そして、_ZN10MyTemplateIiE1fEiのマングルされた名前であるunmangleしないことを決めました。MyTemplate<int>::f(int)>c++filt

したがって、メソッドのインスタンス化ごとに個別のセクションが生成され、それぞれがオブジェクトファイルのスペースを使用することがわかります。

オブジェクト再定義問題の解決策

この問題は、明示的なインスタンス化と次のいずれかを使用することで回避できます。

  • hppの定義を保持し、extern template明示的にインスタンス化されるタイプのhppを追加します。

    で説明されているように、externテンプレート(C ++ 11) extern templateを使用すると、明示的なインスタンス化を除いて、完全に定義されたテンプレートがコンパイルユニットによってインスタンス化されなくなります。このようにして、明示的なインスタンス化のみが最終オブジェクトで定義されます。

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t) { return t + 1; }
    };
    
    extern template class MyTemplate<int>;
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    欠点:

    • ヘッダーのみのライブラリの場合は、外部プロジェクトに独自の明示的なインスタンス化を強制します。ヘッダーのみのライブラリでない場合は、このソリューションが最適です。
    • テンプレートタイプが独自のプロジェクトで定義されており、のような組み込みintではない場合、ヘッダーにそのインクルードを追加する必要があるようです。前方宣言では不十分です:externテンプレートと不完全なタイプこれにより、ヘッダーの依存関係が増加します少し。
  • cppファイルの定義を移動し、hppの宣言のみを残します。つまり、元の例を次のように変更します。

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    欠点:外部プロジェクトは、独自のタイプでテンプレートを使用できません。また、すべてのタイプを明示的にインスタンス化する必要があります。しかし、おそらくこれは、プログラマーが忘れないので、良い面です。

  • hppの定義を保持し、extern templateすべてのインクルーダーを追加します。

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    欠点:すべてのインクルーダーはexternCPPファイルにを追加する必要がありますが、プログラマーはこれを忘れる可能性があります。

これらのソリューションのいずれかを使用すると、次のものnmが含まれます。

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

我々が持って見るように、唯一mytemplate.oのコンパイル持ちMyTemplate<int>ながら、必要に応じてをnotmain.oしてmain.oいるためないU手段は未定義。

含まれているヘッダーから定義を削除しますが、ヘッダーのみのライブラリでテンプレートを外部APIに公開します

ライブラリがヘッダーのみではない場合、extern templateプロジェクトを使用すると、明示的なテンプレートのインスタンス化のオブジェクトが含まれるオブジェクトファイルにリンクするだけなので、メソッドは機能します。

ただし、ヘッダーのみのライブラリの場合、両方が必要な場合:

  • プロジェクトのコンパイルをスピードアップします
  • 他の人が使用できるように、ヘッダーを外部ライブラリAPIとして公開します

次に、次のいずれかを試すことができます。

    • mytemplate.hpp:テンプレート定義
    • mytemplate_interface.hpp:からの定義にのみ一致するテンプレート宣言、定義mytemplate_interface.hppなし
    • mytemplate.cppmytemplate.hpp明示的なインスタンス化を含めて作成する
    • main.cppそしてコードベースの他のすべての場所:インクルードmytemplate_interface.hpp、ではなくmytemplate.hpp
    • mytemplate.hpp:テンプレート定義
    • mytemplate_implementation.hpp:インスタンス化されるすべてのクラスに含まれmytemplate.hpp、追加externされます
    • mytemplate.cppmytemplate.hpp明示的なインスタンス化を含めて作成する
    • main.cppそしてコードベースの他のすべての場所:インクルードmytemplate_implementation.hpp、ではなくmytemplate.hpp

または、複数のヘッダーの場合はさらに良いでしょう。フォルダー内にintf/implフォルダーを作成し、常に名前としてincludes/使用mytemplate.hppします。

mytemplate_interface.hppアプローチは、次のようになります。

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

#include "mytemplate_interface.hpp"

template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }

#endif

mytemplate_interface.hpp

#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP

template<class T>
struct MyTemplate {
    T f(T t);
};

#endif

mytemplate.cpp

#include "mytemplate.hpp"

// Explicit instantiation.
template class MyTemplate<int>;

main.cpp

#include <iostream>

#include "mytemplate_interface.hpp"

int main() {
    std::cout << MyTemplate<int>().f(1) << std::endl;
}

コンパイルして実行します。

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o

出力:

2

Ubuntu18.04でテスト済み。

C ++ 20モジュール

https://en.cppreference.com/w/cpp/language/modules

この機能は、利用可能になった時点で最適なセットアップを提供すると思いますが、GCC 9.2.1ではまだ利用できないため、まだ確認していません。

高速化/ディスク節約を実現するには、明示的なインスタンス化を行う必要がありますが、少なくとも、「含まれているヘッダーから定義を削除するだけでなく、テンプレートを外部APIに公開する」ための適切なソリューションがあり、約100回コピーする必要はありません。

予想される使用法(明示的なインスタンス化がない場合、正確な構文がどのようになるかわからない。C++ 20モジュールでテンプレートの明示的なインスタンス化を使用する方法を参照)は次のようなものです

helloworld.cpp

export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
template<class T>
export void hello(T t) {      // export declaration
    std::cout << t << std::end;
}

main.cpp

import helloworld;  // import declaration
 
int main() {
    hello(1);
    hello("world");
}

次に、https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/で言及されているコンパイル

clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o

したがって、これから、clangがテンプレートインターフェイス+実装をマジックhelloworld.pcmに抽出できることがわかります。マジックには、ソースのLLVM中間表現が含まれている必要があります。C++モジュールシステムでテンプレートはどのように処理されますか?これでも、テンプレートの指定を行うことができます。

ビルドをすばやく分析して、テンプレートのインスタンス化から多くの利益が得られるかどうかを確認する方法

それで、あなたは複雑なプロジェクトを持っていて、実際に完全なリファクタリングを行うことなく、テンプレートのインスタンス化が大幅な利益をもたらすかどうかを決定したいですか?

以下の分析は、以下からいくつかのアイデアを借りることによって、最初にリファクタリングする最も有望なオブジェクトを決定するか、少なくとも選択するのに役立つ可能性があります。私のC ++オブジェクトファイルが大きすぎる

# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
  grep ' W ' > nm.log

# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log

# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log

# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log

# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list. 
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
  sort -k1 -n > nm.gains.log

# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log

# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log

夢:テンプレートコンパイラキャッシュ

究極の解決策は、次のもので構築できればだと思います。

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

その後、myfile.o以前にコンパイルされたテンプレートをファイル間で自動的に再利用します。

これは、その余分なCLIオプションをビルドシステムに渡す以外に、プログラマーに余分な労力をかけないことを意味します。

明示的なテンプレートインスタンス化の2番目のボーナス:IDEがテンプレートインスタンス化を一覧表示するのに役立ちます

Eclipseなどの一部のIDEは、「使用されているすべてのテンプレートインスタンス化のリスト」を解決できないことがわかりました。

したがって、たとえば、テンプレート化されたコード内にいて、テンプレートの可能な値を見つけたい場合は、コンストラクターの使用法を1つずつ見つけて、可能なタイプを1つずつ推測する必要があります。

しかし、Eclipse 2020-03では、クラス名で[すべての使用法を検索](Ctrl + Alt + G)検索を実行することで、明示的にインスタンス化されたテンプレートを簡単に一覧表示できます。

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

に:

template class AnimalTemplate<Dog>;

デモは次のとおりです:https//github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

ただし、IDEの外部で使用できる別のゲリラ手法nm -Cは、最終的な実行可能ファイルで実行し、テンプレート名をgrepすることです。

nm -C main.out | grep AnimalTemplate

これDogは、インスタンス化の1つであったという事実を直接示しています。

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)

1

コンパイラモデルによって異なります。明らかに、BorlandモデルとCFrontモデルがあります。そして、それはあなたの意図にも依存します-あなたがライブラリを書いているなら、あなたは(上で示唆したように)あなたが望む専門化を明示的にインスタンス化するかもしれません。

GNU c ++ページでは、モデルについてhttps://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.htmlで説明しています

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