显式模板实例化 - 何时使用?

发布于 2025-01-11 01:35:37 字数 177 浏览 0 评论 0 原文

休息几周后,我尝试通过 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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

高速公鹿 2025-01-18 01:35:38

如果您定义了一个模板类,您只想为几个显式类型工作。

像普通类一样将模板声明放在头文件中。

就像普通类一样,将模板定义放在源文件中。

然后,在源文件的末尾,仅显式实例化您希望可用的版本。

愚蠢的例子:

// 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>;

Main

#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();
}

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:

// 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;

Source:

// 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>;

Main

#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();
}
山川志 2025-01-18 01:35:38

显式实例化可以减少编译时间和输出大小

这些是它可以提供的主要好处。它们来自以下部分中详细描述的以下两种效果:

  • 从标头中删除定义以防止智能构建系统在对这些模板的每次更改时重建包含程序(节省时间)
  • 防止对象重新定义(节省时间和大小)

删除来自标头的定义

显式实例化允许您将定义保留在 .cpp 文件中。

当定义位于标头并且您修改它时,智能构建系统将重新编译所有包含程序,这可能是数十个文件,可能会使单个文件更改后的增量重新编译速度慢得难以忍受。

将定义放入 .cpp 文件确实有一个缺点,即外部库无法将模板与自己的新类重用,但下面的“从包含的标头中删除定义,但也将模板公开为外部 API”显示了一种解决方法。

请参阅下面的具体示例。

检测包含和重建的构建系统示例:

对象重定义收益:了解问题

如果您只是在头文件上完全定义模板,那么包含该头文件的每个编译单元最终都会为每个不同的模板参数用法编译自己的模板隐式副本。

这意味着大量无用的磁盘使用和编译时间。

下面是一个具体示例,其中 main.cppnotmain.cpp 都隐式定义了 MyTemplate 由于其在这些文件中的使用。

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 nm中,我们看到W表示弱符号,GCC选择它是因为这是一个模板函数。

它在具有多个定义的链接时不会爆炸的原因是链接器接受多个弱定义,只选择其中之一放入最终的可执行文件中,在我们的例子中所有这些都是相同的,所以一切都很好。

输出中的数字含义:

  • 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 > 是 MyTemplate::f(int)> 的损坏名称c++filt 决定不进行 unmangle。

对象重定义问题的解决方案

通过使用显式实例化和以下任一方法可以避免此问题:

  1. 在 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

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

    notmain.cpp

    <前><代码>#include“mytemplate.hpp”
    #include“notmain.hpp”

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

    缺点:

    • 定义保留在标头中,使得单个文件更改重新编译到该标头可能会很慢
    • 如果您是纯头文件库,则强制外部项目进行自己的显式实例化。如果您不是仅使用标头的库,那么此解决方案可能是最好的。
    • 如果模板类型是在您自己的项目中定义的,而不是像int这样的内置类型,那么您似乎被迫在标头中添加它的包含,前向声明不是足够: 外部模板 &不完整的类型这会稍微增加标头依赖性。

  2. 移动cpp文件上的定义,只保留hpp文件上的声明,即修改原来的例子为:

    mytemplate.hpp

    <前><代码>#ifndef MYTEMPLATE_HPP
    #定义MYTEMPLATE_HPP

    模板<类T>
    结构我的模板{
    Tf(Tt);
    };

    #endif

    mytemplate.cpp

    <前><代码>#include“mytemplate.hpp”

    模板<类T>
    T MyTemplate::f(T t) { return t + 1; }

    // 显式实例化。
    模板类 MyTemplate;

    缺点:外部项目无法将您的模板与它们自己的类型一起使用。此外,您还被迫显式实例化所有类型。但也许这是一个好处,因为程序员不会忘记。

  3. 保留 hpp 上的定义并在每个包含器上添加 extern template

    mytemplate.cpp

    <前><代码>#include“mytemplate.hpp”

    // 显式实例化。
    模板类 MyTemplate;

    main.cpp

    #include ;
    
    #include“mytemplate.hpp”
    #include“notmain.hpp”
    
    // 外部模板声明
    外部模板类 MyTemplate;
    
    int main() {
        std::cout << notmain() + MyTemplate().f(1) << std::endl;
    }
    

    notmain.cpp

    <前><代码>#include“mytemplate.hpp”
    #include“notmain.hpp”

    // 外部模板声明
    外部模板类 MyTemplate;

    int notmain() { return MyTemplate().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 编译,而 notmain.omain.o 则不然,因为 U 表示未定义。

从包含的标头中删除定义,但也会在仅标头库中公开模板和外部 API

如果您的库不仅仅是标头,则 extern template 方法将起作用,因为使用项目将仅链接到您的对象文件,该文件将包含显式模板实例化的对象。

但是,对于仅包含标头的库,如果您想要以下两者:

  • 加快项目的编译速度
  • 将标头公开为外部库 API 供其他人使用

,那么您可以尝试以下方法之一:

    • 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

在 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;
}

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 选项传递给构建系统之外,程序员无需付出额外的努力。

显式模板实例化的第二个好处:帮助 IDE 列出模板实例化

我发现某些 IDE(例如 Eclipse)无法解析“使用的所有模板实例化的列表”。

因此,例如,如果您在模板化代码中,并且想要找到模板的可能值,则必须一一找到构造函数的用法并一一推导可能的类型。

但在 Eclipse 2020-03 上,我可以通过对类名执行“查找所有用法”(Ctrl + Alt + G) 搜索来轻松列出显式实例化的模板,这将我指向例如 from:

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

to:

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 是实例化之一:

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(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 to prevent intelligent build systems from rebuilding includers on every change to those templates (saves time)
  • prevent object redefinition (saves time and size)

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 and notmain.cpp implicitly define MyTemplate<int> due to its usage in those files.

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 upstream.

Compile and view symbols with 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

Output:

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

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 that W 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 section
  • 0000000000000017: size of the code generated for them

We can see this a bit more clearly with:

objdump -S main.o | c++filt

which ends in:

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

and _ZN10MyTemplateIiE1fEi is the mangled name of MyTemplate<int>::f(int)> which c++filt decided not to unmangle.

Solutions to the object redefinition problem

This problem can be avoided by using explicit instantiation and either:

  1. 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

    #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); }
    

    Downsides:

    • the definition stays in the header, making single file change recompiles to that header possibly slow
    • if you are header only library, you force external projects to do their own explicit instantiation. If you are not a header-only library, this solution is likely the best.
    • if the template type is defined in your own project and not a built-in like 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.
  2. moving the definition on the cpp file, leave only declaration on hpp, i.e. modify the original example to be:

    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>;
    

    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.

  3. keep definition on hpp and add extern template on every includer:

    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); }
    

    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:

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

so we see have only mytemplate.o has a compilation of MyTemplate<int> as desired, while notmain.o and main.o don't because U 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:

  • speed up your project's compilation
  • expose headers as an external library API for others to use it

then you can try one of the following:

    • mytemplate.hpp: template definition
    • mytemplate_interface.hpp: template declaration only matching the definitions from mytemplate_interface.hpp, no definitions
    • mytemplate.cpp: include mytemplate.hpp and make explicit instantitations
    • main.cpp and everywhere else in the code base: include mytemplate_interface.hpp, not mytemplate.hpp
    • mytemplate.hpp: template definition
    • mytemplate_implementation.hpp: includes mytemplate.hpp and adds extern to every class that will be instantiated
    • mytemplate.cpp: include mytemplate.hpp and make explicit instantitations
    • main.cpp and everywhere else in the code base: include mytemplate_implementation.hpp, not mytemplate.hpp

Or even better perhaps for multiple headers: create an intf/impl folder inside your includes/ folder and use mytemplate.hpp as the name always.

The mytemplate_interface.hpp approach looks like this:

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;
}

Compile and run:

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

Output:

2

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

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");
}

and then compilation mentioned at 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

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

# 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

The dream: a template compiler cache

I think the ultimate solution would be if we could build with:

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

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:

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

to:

template class AnimalTemplate<Dog>;

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:

nm -C main.out | grep AnimalTemplate

which directly points to the fact that Dog was one of the instantiations:

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
不顾 2025-01-18 01:35:38

直接从 https://learn.microsoft.com/en-us/ 复制cpp/cpp/显式实例化

您可以使用显式实例化来创建模板化类或函数的实例化,而无需在代码中实际使用它。因为这在您创建使用模板进行分发的库 (.lib) 文件时非常有用,因此未实例化的模板定义不会放入对象 (.obj) 文件中。

(例如,libstdc++ 包含 std::basic_string,allocator > 的显式实例化(即 std::string)因此,每次使用 std::string 的函数时,不需要将相同的函数代码复制到对象中,编译器只需引用(链接)这些函数即可。到 libstdc++。)

Directly copied from https://learn.microsoft.com/en-us/cpp/cpp/explicit-instantiation:

You can use explicit instantiation to create an instantiation of a templated class or function without actually using it in your code. Because this is useful when you are creating library (.lib) files that use templates for distribution, uninstantiated template definitions are not put into object (.obj) files.

(For instance, libstdc++ contains the explicit instantiation of std::basic_string<char,char_traits<char>,allocator<char> > (which is std::string) so every time you use functions of std::string, the same function code doesn't need to be copied to objects. The compiler only need to refer (link) those to libstdc++.)

極樂鬼 2025-01-18 01:35:38

这取决于编译器模型 - 显然有 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文