数週間の休憩の後、私は本「テンプレート-デビッドヴァンデヴォーデとニコライM.ジョスティスによる完全ガイド」でテンプレートの知識を拡大および拡張しようとしています。現時点で理解しようとしているのは、テンプレートの明示的なインスタンス化です。 。
メカニズム自体には問題はありませんが、この機能を使いたい、あるいは使いたいという状況は想像できません。誰かが私にそれを説明することができれば、私はもっと感謝するでしょう。
数週間の休憩の後、私は本「テンプレート-デビッドヴァンデヴォーデとニコライM.ジョスティスによる完全ガイド」でテンプレートの知識を拡大および拡張しようとしています。現時点で理解しようとしているのは、テンプレートの明示的なインスタンス化です。 。
メカニズム自体には問題はありませんが、この機能を使いたい、あるいは使いたいという状況は想像できません。誰かが私にそれを説明することができれば、私はもっと感謝するでしょう。
回答:
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 ++に参照(リンク)するだけで済みます。)
いくつかの明示的な型に対してのみ機能するテンプレートクラスを定義する場合。
通常のクラスと同じように、テンプレート宣言をヘッダーファイルに配置します。
通常のクラスと同じように、テンプレート定義をソースファイルに配置します。
次に、ソースファイルの最後で、使用可能にしたいバージョンのみを明示的にインスタンス化します。
愚かな例:
// 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();
}
明示的なインスタンス化により、コンパイル時間とオブジェクトサイズを削減できます
これらはそれが提供できる主な利益です。これらは、以下のセクションで詳細に説明されている次の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
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 nm
、W
これはテンプレート関数であるため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); }
欠点:すべてのインクルーダーはextern
CPPファイルにを追加する必要がありますが、プログラマーはこれを忘れる可能性があります。
これらのソリューションのいずれかを使用すると、次のもの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
プロジェクトを使用すると、明示的なテンプレートのインスタンス化のオブジェクトが含まれるオブジェクトファイルにリンクするだけなので、メソッドは機能します。
ただし、ヘッダーのみのライブラリの場合、両方が必要な場合:
次に、次のいずれかを試すことができます。
mytemplate.hpp
:テンプレート定義mytemplate_interface.hpp
:からの定義にのみ一致するテンプレート宣言、定義mytemplate_interface.hpp
なしmytemplate.cpp
:mytemplate.hpp
明示的なインスタンス化を含めて作成するmain.cpp
そしてコードベースの他のすべての場所:インクルードmytemplate_interface.hpp
、ではなくmytemplate.hpp
mytemplate.hpp
:テンプレート定義mytemplate_implementation.hpp
:インスタンス化されるすべてのクラスに含まれmytemplate.hpp
、追加extern
されますmytemplate.cpp
:mytemplate.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)
コンパイラモデルによって異なります。明らかに、BorlandモデルとCFrontモデルがあります。そして、それはあなたの意図にも依存します-あなたがライブラリを書いているなら、あなたは(上で示唆したように)あなたが望む専門化を明示的にインスタンス化するかもしれません。
GNU c ++ページでは、モデルについてhttps://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.htmlで説明しています。