几个星期休息之后,我正在尝试使用David Vandevoorde和Nicolai M. Josuttis 所着的模板 - 完整指南来扩展和扩展我的模板知识,我现在想要了解的是模板的显式实例化.
我实际上并没有这样的机制问题,但我无法想象我想要或想要使用此功能的情况.如果有人能向我解释,我将不仅仅是感激.
Mar*_*ork 71
如果您定义了一个模板类,您只想使用几种显式类型.
将模板声明放在头文件中,就像普通类一样.
将模板定义放在源文件中,就像普通类一样.
然后,在源文件的末尾,显式地仅实例化您想要可用的版本.
愚蠢的例子:
// 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;
Run Code Online (Sandbox Code Playgroud)
资源:
// 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>;
Run Code Online (Sandbox Code Playgroud)
主要
#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();
}
Run Code Online (Sandbox Code Playgroud)
ken*_*ytm 49
直接从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 ++.)
Cir*_*四事件 47
显式实例化允许减少编译时间和输出大小
这些是它可以提供的主要收益。它们来自以下部分中详细描述的以下两种效果:
从标题中删除定义
显式实例化允许您在 .cpp 文件中保留定义。
当定义在头文件中并且您修改它时,智能构建系统将重新编译所有包含程序,这可能是几十个文件,可能会在单个文件更改后进行增量重新编译,速度慢得难以忍受。
将定义放在 .cpp 文件中确实有一个缺点,即外部库无法将模板与它们自己的新类重用,但下面的“从包含的头文件中删除定义,但也将模板公开为外部 API”显示了一种解决方法。
请参阅下面的具体示例。
对象重定义收益:理解问题
如果您只是在头文件上完全定义了一个模板,那么包含该头文件的每个编译单元最终都会为每个不同的模板参数用法编译自己的模板的隐式副本。
这意味着大量无用的磁盘使用和编译时间。
这是一个具体示例,其中由于在这些文件中的使用main.cpp而notmain.cpp隐式定义MyTemplate<int>。
主程序
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
Run Code Online (Sandbox Code Playgroud)
我的模板.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t) { return t + 1; }
};
#endif
Run Code Online (Sandbox Code Playgroud)
不是main.hpp
#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP
int notmain();
#endif
Run Code Online (Sandbox Code Playgroud)
编译和查看符号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
Run Code Online (Sandbox Code Playgroud)
输出:
notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
Run Code Online (Sandbox Code Playgroud)
因此,我们看到为每个方法实例化生成了一个单独的部分,并且它们中的每一个都在目标文件中占据了当然的空间。
从man nm,我们看到这W意味着弱符号,GCC 选择它是因为这是一个模板函数。
它在链接时不会因为多个定义而爆炸的原因是链接器接受多个弱定义,并且只选择其中一个放入最终的可执行文件中,并且在我们的例子中它们都是相同的,所以都是美好的。
输出中的数字意味着:
0000000000000000: 部分内的地址。这个零是因为模板会自动放入它们自己的部分0000000000000017:为他们生成的代码的大小我们可以更清楚地看到这一点:
objdump -S main.o | c++filt
Run Code Online (Sandbox Code Playgroud)
结束于:
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
Run Code Online (Sandbox Code Playgroud)
并且_ZN10MyTemplateIiE1fEi是错位的名字MyTemplate<int>::f(int)>,其c++filt决定不unmangle。
对象重定义问题的解决方案
可以通过使用显式实例化和以下任一方式来避免此问题:
保留 hppextern template上的定义,并为将要显式实例化的类型添加hpp。
如上所述:使用 extern 模板 (C++11) extern template可防止编译单元实例化完全定义的模板,我们的显式实例化除外。这样,只有我们的显式实例化才会在最终对象中定义:
我的模板.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
Run Code Online (Sandbox Code Playgroud)
我的模板.cpp
#include "mytemplate.hpp"
// Explicit instantiation required just for int.
template class MyTemplate<int>;
Run Code Online (Sandbox Code Playgroud)
主程序
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
int main() {
std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
int notmain() { return MyTemplate<int>().f(1); }
Run Code Online (Sandbox Code Playgroud)
缺点:
int,则似乎您被迫在标头上为其添加包含,前向声明是不够的:extern 模板和不完整类型这增加了标头依赖性一点点。移动 cpp 文件上的定义,只保留 hpp 上的声明,即修改原始示例为:
我的模板.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
Run Code Online (Sandbox Code Playgroud)
我的模板.cpp
#include "mytemplate.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
// Explicit instantiation.
template class MyTemplate<int>;
Run Code Online (Sandbox Code Playgroud)
缺点:外部项目不能将您的模板与它们自己的类型一起使用。您还被迫显式实例化所有类型。但也许这是一个好处,因为程序员不会忘记。
保持对 hpp 的定义并添加extern template每个包含器:
我的模板.cpp
#include "mytemplate.hpp"
// Explicit instantiation.
template class MyTemplate<int>;
Run Code Online (Sandbox Code Playgroud)
主程序
#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;
}
Run Code Online (Sandbox Code Playgroud)
notmain.cpp
#include "mytemplate.hpp"
#include "notmain.hpp"
// extern template declaration
extern template class MyTemplate<int>;
int notmain() { return MyTemplate<int>().f(1); }
Run Code Online (Sandbox Code Playgroud)
缺点:所有包含者都必须将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)
Run Code Online (Sandbox Code Playgroud)
所以我们看到只有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以及代码库中的其他任何地方: include mytemplate_interface.hpp, notmytemplate.hppmytemplate.hpp: 模板定义mytemplate_implementation.hpp: 包含mytemplate.hpp并添加extern到每个将被实例化的类mytemplate.cpp: 包含mytemplate.hpp并进行显式实例化main.cpp以及代码库中的其他任何地方: include mytemplate_implementation.hpp, notmytemplate.hpp或者对于多个标题甚至更好:在您的文件夹中创建一个intf/impl文件includes/夹并mytemplate.hpp始终使用该名称。
该mytemplate_interface.hpp方法如下所示:
我的模板.hpp
#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP
#include "mytemplate_interface.hpp"
template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }
#endif
Run Code Online (Sandbox Code Playgroud)
mytemplate_interface.hpp
#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP
template<class T>
struct MyTemplate {
T f(T t);
};
#endif
Run Code Online (Sandbox Code Playgroud)
我的模板.cpp
#include "mytemplate.hpp"
// Explicit instantiation.
template class MyTemplate<int>;
Run Code Online (Sandbox Code Playgroud)
主程序
#include <iostream>
#include "mytemplate_interface.hpp"
int main() {
std::cout << MyTemplate<int>().f(1) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)
编译并运行:
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
Run Code Online (Sandbox Code Playgroud)
输出:
2
Run Code Online (Sandbox Code Playgroud)
在 Ubuntu 18.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;
}
Run Code Online (Sandbox Code Playgroud)
主程序
import helloworld; // import declaration
int main() {
hello(1);
hello("world");
}
Run Code Online (Sandbox Code Playgroud)
然后在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
Run Code Online (Sandbox Code Playgroud)
所以由此我们看到clang可以将模板接口+实现提取到magic中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
Run Code Online (Sandbox Code Playgroud)
梦想:模板编译器缓存
我认为最终的解决方案是,如果我们可以构建:
g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp
Run Code Online (Sandbox Code Playgroud)
然后myfile.o会自动跨文件重用以前编译的模板。
这意味着除了将额外的 CLI 选项传递给您的构建系统之外,程序员需要付出 0 额外的努力。
显式模板实例化的第二个好处:帮助 IDE 列出模板实例
我发现某些 IDE(例如 Eclipse)无法解析“使用的所有模板实例的列表”。
因此,例如,如果您在模板化代码中,并且想要找到模板的可能值,则必须一一找到构造函数的用法并一一推导出可能的类型。
但是在 Eclipse 2020-03 上,我可以通过对类名执行 Find all usages (Ctrl + Alt + G) 搜索来轻松列出显式实例化的模板,例如:
template <class T>
struct AnimalTemplate {
T animal;
AnimalTemplate(T animal) : animal(animal) {}
std::string noise() {
return animal.noise();
}
};
Run Code Online (Sandbox Code Playgroud)
到:
template class AnimalTemplate<Dog>;
Run Code Online (Sandbox Code Playgroud)
这是一个演示:https : //github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
您可以在 IDE 之外使用的另一种游击技术是nm -C在最终可执行文件上运行并 grep 模板名称:
nm -C main.out | grep AnimalTemplate
Run Code Online (Sandbox Code Playgroud)
这直接指出Dog了作为实例化之一的事实:
0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
46369 次 |
| 最近记录: |