显式实例化 - 何时使用?

The*_* do 73 c++ templates

几个星期休息之后,我正在尝试使用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)

  • 上面的大多数评论都不再是真实的,因为c ++ 11:一个显式的实例化声明(一个extern模板)阻止了隐式实例化:否则会导致隐式实例化的代码必须使用在其他地方提供的显式实例化定义.程序(通常在另一个文件中:这可用于减少编译时间)http://en.cppreference.com/w/cpp/language/class_template (7认同)
  • 说如果编译器在给定的翻译单元中有整个模板定义(包括函数定义),它会在需要时*将*实例化模板的特化(无论该特化是否已*显式*实例化)另一个恩)?即,为了获得显式实例化的编译/链接时好处,必须只包含模板*声明*,以便编译器*不能*实例化它? (2认同)
  • 什么是一个很好的检查/测试,以确保实际使用显式实例?即它工作正常,但我并不完全相信它不仅仅是按需实例化所有模板. (2认同)

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 ++.)

  • 是的,MSVC CRT库对所有流,语言环境和字符串类都有显式实例化,专门用于char和wchar_t.生成的.lib超过5兆字节. (7认同)
  • @Kenny:我知道防止隐式实例化的GCC选项,但这不是标准.据我所知VC++没有这样的选择.明确的.总是被吹捧为改进编译/链接时间(即使是Bjarne),但是为了使它能够用于此目的,编译器必须知道不隐式实例化模板(例如,通过GCC标志),或者不得给出模板定义,只是一个声明.这听起来不错吗?我只是想了解为什么会使用显式实例化(除了限制具体类型). (4认同)
  • 编译器如何知道模板已在其他地方显式实例化?是不是只生成类定义,因为它可用? (3认同)
  • @kennytm 我知道最初询问时它不可用,但现在使用 C++11,我们可以编写“extern template”,它表示模板在其他地方(可能在库中)实例化。如果模板声明是 extern 的,您甚至可以使用没有定义的模板声明。如果没有链接的定义,您将收到链接器错误。 (3认同)

Cir*_*四事件 47

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

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

  • 从头文件中删除定义以防止构建工具重建包含器(节省时间)
  • 对象重新定义(节省时间和大小)

从标题中删除定义

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

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

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

请参阅下面的具体示例。

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

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

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

这是一个具体示例,其中由于在这些文件中的使用main.cppnotmain.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)

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

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

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

  1. 保留 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)

    缺点:

    • 定义保留在标头中,使单个文件更改重新编译到该标头可能会很慢
    • 如果您是仅标头库,则强制外部项目进行自己的显式实例化。如果您不是仅限头文件的库,则此解决方案可能是最好的。
    • 如果模板类型是在您自己的项目中定义的,而不是内置的 like int,则似乎您被迫在标头上为其添加包含,前向声明是不够的:extern 模板和不完整类型这增加了标头依赖性一点点。
  2. 移动 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)

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

  3. 保持对 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.omain.o不会因为U手段不确定的。

从包含的头文件中删除定义,但也将模板公开为仅头文件库中的外部 API

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

但是,对于只有头文件的库,如果您想同时使用:

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

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

    • mytemplate.hpp: 模板定义
    • mytemplate_interface.hpp: 模板声明只匹配来自 的定义mytemplate_interface.hpp,没有定义
    • mytemplate.cpp: 包含mytemplate.hpp并进行显式实例化
    • main.cpp以及代码库中的其他任何地方: include mytemplate_interface.hpp, notmytemplate.hpp
    • mytemplate.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)

  • 这可能是我见过的最全面的答案。感谢您投入如此详细的工作来解释所有这些! (14认同)