休息几周后,我尝试通过 David Vandevoorde 和 Nicolai M. Josuttis 所著的《模板 – 完整指南》一书来扩展和扩展我对模板的了解,以及我想要了解的内容这一刻是模板的显式实例化。
我实际上对该机制本身没有问题,但我无法想象我想要或想要使用此功能的情况。如果有人能向我解释这一点,我将不胜感激。
After few weeks break, I'm trying to expand and extend my knowlege of templates with the book Templates – The Complete Guide by David Vandevoorde and Nicolai M. Josuttis, and what I'm trying to understand at this moment is explicit instantiation of templates.
I don't actually have a problem with the mechanism as such, but I can't imagine a situation in which I would like or want to use this feature. If anyone can explain that to me I will be more than grateful.
发布评论
评论(4)
如果您定义了一个模板类,您只想为几个显式类型工作。
像普通类一样将模板声明放在头文件中。
就像普通类一样,将模板定义放在源文件中。
然后,在源文件的末尾,仅显式实例化您希望可用的版本。
愚蠢的例子:
来源:
Main
If you define a template class that you only want to work for a couple of explicit types.
Put the template declaration in the header file just like a normal class.
Put the template definition in a source file just like a normal class.
Then, at the end of the source file, explicitly instantiate only the version you want to be available.
Silly example:
Source:
Main
显式实例化可以减少编译时间和输出大小
这些是它可以提供的主要好处。它们来自以下部分中详细描述的以下两种效果:
删除来自标头的定义
显式实例化允许您将定义保留在 .cpp 文件中。
当定义位于标头并且您修改它时,智能构建系统将重新编译所有包含程序,这可能是数十个文件,可能会使单个文件更改后的增量重新编译速度慢得难以忍受。
将定义放入 .cpp 文件确实有一个缺点,即外部库无法将模板与自己的新类重用,但下面的“从包含的标头中删除定义,但也将模板公开为外部 API”显示了一种解决方法。
请参阅下面的具体示例。
检测包含和重建的构建系统示例:
对象重定义收益:了解问题
如果您只是在头文件上完全定义模板,那么包含该头文件的每个编译单元最终都会为每个不同的模板参数用法编译自己的模板隐式副本。
这意味着大量无用的磁盘使用和编译时间。
下面是一个具体示例,其中
main.cpp
和notmain.cpp
都隐式定义了MyTemplate
由于其在这些文件中的使用。main.cpp
notmain.cpp
mytemplate.hpp
notmain.hpp
GitHub 上游。
使用
nm
编译和查看符号:输出:
因此,我们看到为每个方法实例化生成了一个单独的部分,并且每个方法实例化都占用了目标文件中的空间。
从
man nm
中,我们看到W
表示弱符号,GCC选择它是因为这是一个模板函数。它在具有多个定义的链接时不会爆炸的原因是链接器接受多个弱定义,只选择其中之一放入最终的可执行文件中,在我们的例子中所有这些都是相同的,所以一切都很好。
输出中的数字含义:
0000000000000000
:节内的地址。这个零是因为模板会自动放入其自己的部分0000000000000017
:为它们生成的代码的大小我们可以更清楚地看到这一点:以
结尾
:和
_ZN10MyTemplateIiE1fEi
> 是MyTemplate::f(int)>
的损坏名称c++filt
决定不进行 unmangle。对象重定义问题的解决方案
通过使用显式实例化和以下任一方法可以避免此问题:
在 hpp 上保留定义并在 hpp 上为要运行的类型添加
extern template
被显式实例化。如以下所述:使用外部模板 (C++11)
extern template
阻止编译单元实例化完全定义的模板,除非我们显式实例化。这样,只有我们的显式实例化才会在最终对象中定义:mytemplate.hpp
<前><代码>#ifndef MYTEMPLATE_HPP
#定义MYTEMPLATE_HPP
模板<类T>
结构我的模板{
T f(T t) { 返回 t + 1; }
};
外部模板类 MyTemplate;
#endif
mytemplate.cpp
<前><代码>#include“mytemplate.hpp”
// 仅 int 需要显式实例化。;
模板类 MyTemplate
main.cpp
notmain.cpp
<前><代码>#include“mytemplate.hpp”
#include“notmain.hpp”
int notmain() { return MyTemplate().f(1); } }
缺点:
int
这样的内置类型,那么您似乎被迫在标头中添加它的包含,前向声明不是足够: 外部模板 &不完整的类型这会稍微增加标头依赖性。移动cpp文件上的定义,只保留hpp文件上的声明,即修改原来的例子为:
mytemplate.hpp
<前><代码>#ifndef MYTEMPLATE_HPP
#定义MYTEMPLATE_HPP
模板<类T>
结构我的模板{
Tf(Tt);
};
#endif
mytemplate.cpp
<前><代码>#include“mytemplate.hpp”
模板<类T>::f(T t) { return t + 1; }
T MyTemplate
// 显式实例化。;
模板类 MyTemplate
缺点:外部项目无法将您的模板与它们自己的类型一起使用。此外,您还被迫显式实例化所有类型。但也许这是一个好处,因为程序员不会忘记。
保留 hpp 上的定义并在每个包含器上添加
extern template
:mytemplate.cpp
<前><代码>#include“mytemplate.hpp”
// 显式实例化。;
模板类 MyTemplate
main.cpp
notmain.cpp
<前><代码>#include“mytemplate.hpp”
#include“notmain.hpp”
// 外部模板声明;
外部模板类 MyTemplate
int notmain() { return MyTemplate().f(1); } }
缺点:所有包含程序都必须将
extern
添加到其 CPP 文件中,而程序员可能会忘记这样做。对于任何这些解决方案,
nm
现在包含:所以我们看到只有
mytemplate.o
具有所需的MyTemplate
编译,而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
mytemplate_interface.hpp
mytemplate.cpp
main.cpp
编译并运行:
输出:
在 Ubuntu 18.04 中测试。
C++20 模块
https://en.cppreference.com /w/cpp/language/modules
我认为此功能将在可用时提供最佳设置,但我尚未检查它,因为它在我的 GCC 9.2.1 上尚不可用。
您仍然需要进行显式实例化才能获得加速/节省磁盘空间,但至少我们将拥有一个合理的解决方案“从包含的标头中删除定义,但也将模板公开为外部 API”,这不需要复制大约 100 次。
预期用法(没有显式实例化,不确定确切的语法是什么样的,请参阅:如何使用 C++20 模块的模板显式实例化?)是:
helloworld.cpp
main.cpp
,然后是 https://quuxplusone.github.io/blog/2019/11/ 07/modular-hello-world/
所以从这里我们看到clang可以将模板接口+实现提取到神奇的
helloworld.pcm
中,其中必须包含一些LLVM中间表示来源:C++ 模块系统中如何处理模板? 其中仍然允许进行模板规范。如何快速分析您的构建,看看它是否会从模板实例化中获益
因此,您有一个复杂的项目,并且您想要确定模板实例化是否会带来显着的收益,而无需实际执行完整的操作重构?
下面的分析可能会帮助您决定,或者至少在您进行实验时首先选择最有希望重构的对象,通过借鉴以下内容: 我的 C++ 对象文件太大
梦想:模板编译器缓存
我认为最终的解决方案是如果我们可以使用以下命令进行构建
,那么 myfile.o 将自动跨文件重用之前编译的模板。
这意味着除了将额外的 CLI 选项传递给构建系统之外,程序员无需付出额外的努力。
显式模板实例化的第二个好处:帮助 IDE 列出模板实例化
我发现某些 IDE(例如 Eclipse)无法解析“使用的所有模板实例化的列表”。
因此,例如,如果您在模板化代码中,并且想要找到模板的可能值,则必须一一找到构造函数的用法并一一推导可能的类型。
但在 Eclipse 2020-03 上,我可以通过对类名执行“查找所有用法”(Ctrl + Alt + G) 搜索来轻松列出显式实例化的模板,这将我指向例如 from:
to:
这是一个演示: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
另一种可以在 IDE 之外使用的游击技术是在最终的可执行文件上运行 nm -C 并 grep模板名称:
它直接指出
Dog
是实例化之一:Explicit instantiation allows reducing compile times and output sizes
These are the major gains it can provide. They come from the following two effects described in detail in the sections below:
Remove definitions from headers
Explicit instantiation allows you to leave definitions in the .cpp file.
When the definition is on the header and you modify it, an intelligent build system would recompile all includers, which could be dozens of files, possibly making incremental re-compilation after a single file change unbearably slow.
Putting definitions in .cpp files does have the downside that external libraries can't reuse the template with their own new classes, but "Remove definitions from included headers but also expose templates an external API" below shows a workaround.
See concrete examples below.
Examples of build systems that detect includes and rebuild:
Object redefinition gains: understanding the problem
If you just completely define a template on a header file, every single compilation unit that includes that header ends up compiling its own implicit copy of the template for every different template argument usage made.
This means a lot of useless disk usage and compilation time.
Here is a concrete example, in which both
main.cpp
andnotmain.cpp
implicitly defineMyTemplate<int>
due to its usage in those files.main.cpp
notmain.cpp
mytemplate.hpp
notmain.hpp
GitHub upstream.
Compile and view symbols with
nm
:Output:
So we see that a separate section is generated for every single method instantiation, and that each of of them takes of course space in the object files.
From
man nm
, we see thatW
means weak symbol, which GCC chose because this is a template function.The reason it doesn't blow up at link time with multiple definitions is that the linker accepts multiple weak definitions, and just picks one of them to put in the final executable, and all of them are the same in our case, so all is fine.
The numbers in the output mean:
0000000000000000
: address within section. This zero is because templates are automatically put into their own section0000000000000017
: size of the code generated for themWe can see this a bit more clearly with:
which ends in:
and
_ZN10MyTemplateIiE1fEi
is the mangled name ofMyTemplate<int>::f(int)>
whichc++filt
decided not to unmangle.Solutions to the object redefinition problem
This problem can be avoided by using explicit instantiation and either:
keep definition on hpp and add
extern template
on hpp for types which are going to be explicitly instantiated.As explained at: using extern template (C++11)
extern template
prevents a completely defined template from being instantiated by compilation units, except for our explicit instantiation. This way, only our explicit instantiation will be defined in the final objects:mytemplate.hpp
mytemplate.cpp
main.cpp
notmain.cpp
Downsides:
int
, it seems that you are forced to add the include for it on the header, a forward declaration is not enough: extern template & incomplete types This increases header dependencies a bit.moving the definition on the cpp file, leave only declaration on hpp, i.e. modify the original example to be:
mytemplate.hpp
mytemplate.cpp
Downside: external projects can't use your template with their own types. Also you are forced to explicitly instantiate all types. But maybe this is an upside since then programmers won't forget.
keep definition on hpp and add
extern template
on every includer:mytemplate.cpp
main.cpp
notmain.cpp
Downside: all includers have to add the
extern
to their CPP files, which programmers will likely forget to do.With any of those solutions,
nm
now contains:so we see have only
mytemplate.o
has a compilation ofMyTemplate<int>
as desired, whilenotmain.o
andmain.o
don't becauseU
means undefined.Remove definitions from included headers but also expose templates an external API in a header-only library
If your library is not header only, the
extern template
method will work, since using projects will just link to your object file, which will contain the object of the explicit template instantiation.However, for header only libraries, if you want to both:
then you can try one of the following:
mytemplate.hpp
: template definitionmytemplate_interface.hpp
: template declaration only matching the definitions frommytemplate_interface.hpp
, no definitionsmytemplate.cpp
: includemytemplate.hpp
and make explicit instantitationsmain.cpp
and everywhere else in the code base: includemytemplate_interface.hpp
, notmytemplate.hpp
mytemplate.hpp
: template definitionmytemplate_implementation.hpp
: includesmytemplate.hpp
and addsextern
to every class that will be instantiatedmytemplate.cpp
: includemytemplate.hpp
and make explicit instantitationsmain.cpp
and everywhere else in the code base: includemytemplate_implementation.hpp
, notmytemplate.hpp
Or even better perhaps for multiple headers: create an
intf
/impl
folder inside yourincludes/
folder and usemytemplate.hpp
as the name always.The
mytemplate_interface.hpp
approach looks like this:mytemplate.hpp
mytemplate_interface.hpp
mytemplate.cpp
main.cpp
Compile and run:
Output:
Tested in Ubuntu 18.04.
C++20 modules
https://en.cppreference.com/w/cpp/language/modules
I think this feature will provide the best setup going forward as it becomes available, but I haven't checked it yet because it is not yet available on my GCC 9.2.1.
You will still have to do explicit instantiation to get the speedup/disk saving, but at least we will have a sane solution for "Remove definitions from included headers but also expose templates an external API" which does not require copying things around 100 times.
Expected usage (without the explicit insantiation, not sure what the exact syntax will be like, see: How to use template explicit instantiation with C++20 modules?) be something along:
helloworld.cpp
main.cpp
and then compilation mentioned at https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/
So from this we see that clang can extract the template interface + implementation into the magic
helloworld.pcm
, which must contain some LLVM intermediate representation of the source: How are templates handled in C++ module system? which still allows for template specification to happen.How to quickly analyze your build to see if it would gain a lot from template instantiation
So, you've got a complex project and you want to decide if template instantiation will bring significant gains without actually doing the full refactor?
The analysis below might help you decide, or at least select the most promising objects to refactor first while you experiment, by borrowing some ideas from: My C++ object file is too big
The dream: a template compiler cache
I think the ultimate solution would be if we could build with:
and then
myfile.o
would automatically reuse previously compiled templates across files.This would mean 0 extra effort on the programmers besides passing that extra CLI option to your build system.
A secondary bonus of explicit template instantiation: help IDEs list template instantiations
I've found that some IDEs such as Eclipse cannot resolve "a list of all template instantiations used".
So e.g., if you are inside a templated code, and you want to find possible values of the template, you would have to find the constructor usages one by one and deduce the possible types one by one.
But on Eclipse 2020-03 I can easily list explicitly instantiated templates by doing a Find all usages (Ctrl + Alt + G) search on the class name, which points me e.g. from:
to:
Here's a demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Another guerrila technique you could use outside of the IDE however would be to run
nm -C
on the final executable and grep the template name:which directly points to the fact that
Dog
was one of the instantiations:直接从 https://learn.microsoft.com/en-us/ 复制cpp/cpp/显式实例化:
(例如,libstdc++ 包含
std::basic_string,allocator >
的显式实例化(即std::string
)因此,每次使用std::string
的函数时,不需要将相同的函数代码复制到对象中,编译器只需引用(链接)这些函数即可。到 libstdc++。)Directly copied from https://learn.microsoft.com/en-us/cpp/cpp/explicit-instantiation:
(For instance, libstdc++ contains the explicit instantiation of
std::basic_string<char,char_traits<char>,allocator<char> >
(which isstd::string
) so every time you use functions ofstd::string
, the same function code doesn't need to be copied to objects. The compiler only need to refer (link) those to libstdc++.)这取决于编译器模型 - 显然有 Borland 模型和 CFront 模型。然后,这还取决于您的意图 - 如果您正在编写一个库,您可能(如上所述)显式实例化您想要的专业化。
GNU c++ 页面在这里讨论这些模型 https:// gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html。
It depends on the compiler model - apparently there is the Borland model and the CFront model. And then it depends also on your intention - if your are writing a library, you might (as alluded above) explicitly instantiate the specializations you want.
The GNU c++ page discusses the models here https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html.