使用extern模板(C++ 11)

cod*_*ddy 107 c++ templates extern c++11

图1:功能模板

TemplHeader.h

template<typename T>
void f();
Run Code Online (Sandbox Code Playgroud)

TemplCpp.cpp

template<typename T>
void f(){
   //...
}    
//explicit instantation
template void f<T>();
Run Code Online (Sandbox Code Playgroud)

Main.cpp的

#include "TemplHeader.h"
extern template void f<T>(); //is this correct?
int main() {
    f<char>();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这是正确的使用方法extern template,还是仅将此关键字用于类模板,如图2所示?

图2:类模板

TemplHeader.h

template<typename T>
class foo {
    T f();
};
Run Code Online (Sandbox Code Playgroud)

TemplCpp.cpp

template<typename T>
void foo<T>::f() {
    //...
}
//explicit instantation
template class foo<int>;
Run Code Online (Sandbox Code Playgroud)

Main.cpp的

#include "TemplHeader.h"
extern template class foo<int>();
int main() {
    foo<int> test;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我知道将所有这些放在一个头文件中是好的,但如果我们在多个文件中实例化具有相同参数的模板,那么我们会得到多个相同的定义,编译器会将它们全部删除(除了一个)以避免错误.我该怎么用extern template?我们可以只将它用于类,还是可以将它用于函数?

此外,图1和图2可以扩展为模板位于单个头文件中的解决方案.在这种情况下,我们需要使用extern template关键字来避免多个相同的瞬间.这仅适用于课程或功能吗?

Dan*_*ani 164

您知道它将在其他地方实例化时,您应该只使用extern template强制编译器实例化模板.它用于减少编译时间和目标文件大小.

例如:

// header.h

template<typename T>
void ReallyBigFunction()
{
    // Body
}

// source1.cpp

#include "header.h"
void something1()
{
    ReallyBigFunction<int>();
}

// source2.cpp

#include "header.h"
void something2()
{
    ReallyBigFunction<int>();
}
Run Code Online (Sandbox Code Playgroud)

这将导致以下目标文件:

source1.o
    void something1()
    void ReallyBigFunction<int>()    // Compiled first time

source2.o
    void something2()
    void ReallyBigFunction<int>()    // Compiled second time
Run Code Online (Sandbox Code Playgroud)

如果两个文件都链接在一起,void ReallyBigFunction<int>()则会丢弃一个文件,从而导致浪费的编译时间和目标文件大小.

为了不浪费编译时间和目标文件大小,有一个extern关键字使编译器无法编译模板函数.当且仅当您知道它在其他地方使用相同的二进制文件时,您应该使用它.

更改source2.cpp到:

// source2.cpp

#include "header.h"
extern template void ReallyBigFunction<int>();
void something2()
{
    ReallyBigFunction<int>();
}
Run Code Online (Sandbox Code Playgroud)

将导致以下目标文件:

source1.o
    void something1()
    void ReallyBigFunction<int>() // compiled just one time

source2.o
    void something2()
    // No ReallyBigFunction<int> here because of the extern
Run Code Online (Sandbox Code Playgroud)

当这两者都链接在一起时,第二个目标文件将只使用第一个目标文件中的符号.不需要丢弃,也不需要浪费编译时间和目标文件大小.

这应该仅在项目中使用,例如在多次使用模板时vector<int>,您应该extern在除一个源文件之外的所有文件中使用.

这也适用于类和函数作为一个,甚至模板成员函数.

  • "如果你知道它在其他地方使用相同的二进制文件." 这既不充分也不必要.您的代码"格式错误,无需诊断".您不能依赖于另一个TU的隐式实例化(允许编译器对其进行优化,就像内联函数一样).必须在另一个TU中提供显式实例化. (81认同)
  • 我想指出这个答案可能是错的,我被它咬了.幸运的是约翰内斯的评论有很多选票,这一次我更加关注它.我只能假设这个问题的绝大多数选民实际上没有在多个编译单元中实现这些类型的模板(就像我今天所做的那样)......至少对于clang来说,唯一可靠的方法是将这些模板定义放入你的头!被警告! (28认同)
  • @ JohannesSchaub-litb,您能详细说明一下还是提供更好的答案?我不确定我是否完全理解你的反对意见. (6认同)
  • @Dani:到目前为止我读过的extern模板的最佳解释! (3认同)
  • @codekiddy:我不知道Visual Studio的含义是什么.如果您希望大多数c ++ 11代码正常工作,那么您应该使用更兼容的编译器. (2认同)
  • @StevenLu,我认为他们说另一个TU必须在"template void ReallyBigFunction <int>();"的形式中进行显式实例化.(和extern一样,但没有extern."我认为他们说如果你依赖它被隐式实例化,就像思考"source1.cpp"必须隐式实例化它,因为它使用它,有时它不会正常工作. (2认同)
  • 另外,您可以将“ extern template ...”放在“ header.h”中,并创建一个“ header.cpp”以明确实例化它。如果将“外部模板”与显式实例化一起使用,则将其定义为不执行任何操作。至少在gcc 7.2.0和clang 5.0.0中有效,我在VS中还没有使用过。 (2认同)

seh*_*ehe 44

维基百科有最好的描述

在C++ 03中,只要在转换单元中遇到完全指定的模板,编译器就必须实例化模板.如果模板在许多翻译单元中使用相同类型进行实例化,则可能会显着增加编译时间.在C++ 03中无法阻止这种情况,因此C++ 11引入了外部模板声明,类似于外部数据声明.

C++ 03有这种语法来强制编译器实例化一个模板:

  template class std::vector<MyClass>;
Run Code Online (Sandbox Code Playgroud)

C++ 11现在提供了这种语法:

  extern template class std::vector<MyClass>;
Run Code Online (Sandbox Code Playgroud)

它告诉编译器不要在此转换单元中实例化模板.

警告: nonstandard extension used...

Microsoft VC++过去已经使用了这个功能的非标准版本(在C++ 03中).编译器警告这一点,以防止需要在不同编译器上编译的代码的可移植性问题.

查看链接页面中的示例,看它的工作方式大致相同.您可以预期消息将随着MSVC的未来版本而消失,当然除非同时使用其他非标准编译器扩展.

  • "... __告诉编译器不要在这个翻译单元中实例化模板." 我不认为这是真的.类定义中定义的任何方法都计为内联,因此如果STL实现使用`std :: vector`的内联方法(非常确定所有这些方法都有),`extern`就没有效果. (3认同)

Cir*_*四事件 14

extern template 仅当模板声明完整时才需要

这在其他答案中有所暗示,但我认为没有给予足够的重视。

这意味着在 OP 示例中,extern template无效,因为标题上的模板定义不完整:

  • void f();: 只是声明,没有正文
  • class foo: 声明方法f()但没有定义

所以我建议extern template在这种特殊情况下只删除定义:如果类完全定义,你只需要添加它们。

例如:

模板头文件

template<typename T>
void f();
Run Code Online (Sandbox Code Playgroud)

模板Cpp

template<typename T>
void f(){}

// Explicit instantiation for char.
template void f<char>();
Run Code Online (Sandbox Code Playgroud)

主程序

#include "TemplHeader.h"

// Commented out from OP code, has no effect.
// extern template void f<T>(); //is this correct?

int main() {
    f<char>();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

编译和查看符号nm

g++ -std=c++11 -Wall -Wextra -pedantic -c -o TemplCpp.o TemplCpp.cpp
g++ -std=c++11 -Wall -Wextra -pedantic -c -o Main.o Main.cpp
g++ -std=c++11 -Wall -Wextra -pedantic -o Main.out Main.o TemplCpp.o
echo TemplCpp.o
nm -C TemplCpp.o | grep f
echo Main.o
nm -C Main.o | grep f
Run Code Online (Sandbox Code Playgroud)

输出:

TemplCpp.o
0000000000000000 W void f<char>()
Main.o
                 U void f<char>()
Run Code Online (Sandbox Code Playgroud)

然后从man nm我们看到这U意味着未定义,因此定义确实只在TemplCpp需要时保留。

所有这些都归结为完整标头声明的权衡:

  • 优点:
    • 允许外部代码将我们的模板与新类型一起使用
    • 如果我们对对象膨胀没有问题,我们可以选择不添加显式实例化
  • 缺点:
    • 在开发该类时,标头实现更改将导致智能构建系统重建所有包含器,这可能是许多文件
    • 如果我们想避免对象文件膨胀,我们不仅需要进行显式实例化(与不完整的头声明相同),还需要添加extern template每个包含程序,程序员可能会忘记这样做

更多示例显示在:显式模板实例化 - 何时使用?

由于编译时间在大型项目中非常重要,我强烈建议使用不完整的模板声明,除非外部各方绝对需要使用他们自己复杂的自定义类重用您的代码。

在这种情况下,我会首先尝试使用多态来避免构建时间问题,并且只有在可以显着提高性能的情况下才使用模板。

在 Ubuntu 18.04 中测试。


小智 5

模板的已知问题是代码膨胀,这是在调用类模板特化的每个模块中生成类定义的结果。为了防止这种情况,从 C++0x 开始,可以在类模板特化前使用关键字extern

#include <MyClass>
extern template class CMyClass<int>;
Run Code Online (Sandbox Code Playgroud)

模板类的显式实例应该只发生在单个翻译单元中,最好是带有模板定义的那个 (MyClass.cpp)

template class CMyClass<int>;
template class CMyClass<float>;
Run Code Online (Sandbox Code Playgroud)