【问题标题】:Does extern template prevent inlining of functions?extern 模板是否会阻止函数内联?
【发布时间】:2011-07-17 19:46:33
【问题描述】:

我并不完全清楚新的extern template 功能是如何在 C++11 中工作的。我了解它旨在帮助加快编译时间,并简化与共享库的链接问题。这是否意味着编译器甚至不解析函数体,从而强制进行非内联调用?或者它只是指示编译器在进行非内联调用时不生成实际的方法体?显然,链接时代码生成无法承受。

作为差异可能重要的具体示例,考虑一个对不完整类型进行操作的函数。

//Common header
template<typename T>
void DeleteMe(T* t) {
    delete t;
}

struct Incomplete;
extern template void DeleteMe(Incomplete*);

//Implementation file 1
#include common_header
struct Incomplete { };
template void DeleteMe(Incomplete*);

//Implementation file 2
#include common_header
int main() {
   Incomplete* p = factory_function_not_shown();
   DeleteMe(p);
}

在“实施文件 2”中,将 delete 指向 Incomplete 的指针是不安全的。所以DeleteMe 的内联版本会失败。但是,如果将其保留为实际的函数调用,并且函数本身是在“实现文件 1”中生成的,那么一切都会正常工作。

作为推论,具有类似extern template class 声明的模板类的成员函数的规则是否相同?

出于实验目的,MSVC 会为上述代码生成正确的输出,但如果删除了 extern 行,则会生成有关删除不完整类型的警告。然而,这是他们几年前引入的非标准扩展的残余,所以我不确定我能相信多少这种行为。我无法访问任何其他构建环境来进行实验 [保存 ideone 等人,但在这种情况下,仅限于一个翻译单元是相当有限的]。

【问题讨论】:

  • +1 回答一个我因正确原因而没有完全理解的问题。令人耳目一新。
  • 我喜欢这个问题,因为我在现实世界的项目中没有找到这个 C++11 功能的有用和优雅的用例,我希望从讨论中得到启发。

标签: c++ templates c++11


【解决方案1】:

外部模板背后的想法是使显式模板实例化更有用。

如您所知,在 C++03 中,您可以使用以下语法显式实例化模板:

template class SomeTemplateClass<int>;
template void foo<bool>();

这告诉编译器在当前翻译单元中实例化模板。但是,这并不能阻止隐式实例化的发生:编译器仍然必须执行所有隐式实例化,然后在链接期间再次将它们合并在一起。

例子:

// a.h
template <typename> void foo() { /* ... */ }

// a.cpp
#include "a.h"
template void foo<int>();

// b.cpp
#include "a.h"
int main()
{
    foo<int>();
    return 0;
} 

这里,a.cpp 显式实例化了foo&lt;int&gt;(),但是一旦我们去编译b.cpp,它就会再次实例化它,因为b.cpp 不知道a.cpp 无论如何都会实例化它。对于具有许多不同翻译单元进行隐式实例化的大型函数,这会显着增加编译和链接时间。它还可能导致函数被不必要地内联,从而导致严重的代码膨胀。

使用外部模板,您可以让其他源文件知道您打算显式实例化模板:

// a.h
template <typename> void foo() { /* ... */ }
extern template void foo<int>();

这样,b.cpp 不会导致 foo&lt;int&gt;() 的实例化。该函数将在a.cpp 中实例化,并将像任何普通函数一样被链接。它也不太可能被内联。

请注意,这并不能阻止内联——该函数仍然可以在链接时内联,就像一个普通的非内联函数仍然可以内联一样。

编辑:对于那些好奇的人,我只是做了一个快速测试,看看 g++ 花费了多少时间来实例化模板。我尝试在不同数量的翻译单元中实例化std::sort&lt;int*&gt;,无论实例化是否被抑制。结果是决定性的:每次 std::sort 实例化 30 毫秒。在大型项目中肯定有时间可以节省下来。

【讨论】:

  • 请注意,在 C++03 中,当您使用显式模板特化时,您可以只在头文件中声明接口,而将实现留在 cpp 文件中。这样做的缺点是任何尝试使用模板的非专业版本都会导致链接错误。
  • 感谢您的详细解释。我知道除了您指定的目标文件之外,任何目标文件中都不会有实际的函数foo&lt;int&gt;()。但是编译器是否可以选择直接删除代码而不从技术上生成该函数体?我看到你说它被内联的可能性要小得多,但这是因为它不能[直到链接时间],还是因为这只是编译器当前的编写方式?
  • 一般来说,如果你将模板的主体放在一个单独的 TU 中,那么编译器不能内联它,但链接器可以。这样做的原因是因为编译器单独编译源文件,完全独立于其他源文件,所以他们根本不知道函数体是什么样子,因此不能内联它。如果您一次编译所有源文件(在编译器的一次运行中),我相信编译器有时能够做到这一点,但我不知道详细信息。
  • @Dennis Zickefoose:编译器和链接器的分离是虚拟的,而不是物理的。代码生成发生在编译和链接时。在 Visual Studio 中,有一个完整的程序优化选项。
  • 恕我直言,答案完全是题外话,这不是 OP 要求的。 OP 询问是否允许编译器在原则上执行内联,如果它在 TU 中看到函数体但声明为 extern template
【解决方案2】:

使用extern template class 似乎并不能防止内联。我将通过一个例子来说明这一点,它有点复杂,但我能想到的最简单的。

在文件a.h中我们定义了模板类CFoo

#ifndef A_H
#define A_H
#include <iostream>

template <typename T> class CFoo{
  public: CFoo(){
      std::cout << "CFoo Constructor, edit 0" << std::endl;
    }
};

extern template class CFoo<int>;
#endif

在 a.h 的末尾,我们使用 extern template class CFoo&lt;int&gt; 向任何带有 #include a.h 的翻译单元表明它不需要为 CFoo 生成任何代码。这是我们做出的承诺,CFoo 的所有事物都将顺利链接。

在文件 c.cpp 我们有,

#include "a.h"

void run(){
  CFoo<int> cf;
}

由于extern template classpromise' at the end of a.h, the translation unit of c.cpp does not需要'为类CFoo生成任何代码。

最后我们在b.cpp中声明一个main函数,

void run();
int main(){
  run();
  return 0;
}

b.cpp 没有什么花哨的,我们只是声明void run(),它将在链接时链接到翻译单元b.cpp 的实现。为了完整起见,这里是一个makefile

cflags = -std=c++11 -O1

b : b.o a.o c.o
  g++ ${cflags} b.o a.o c.o -o b

b.o : b.cpp 
  g++ ${cflags} -c b.cpp -o b.o

c.o : c.cpp 
  g++ ${cflags} -c c.cpp -o c.o

a.o : a.cpp a.h
  g++ ${cflags} -c a.cpp -o a.o

clean:
  rm -rf a.o b.o c.o b

使用这个makefile编译和链接一个可执行文件a,它在运行时输出``CFoo Constructor, edit 0''。但请注意!在上面的示例中,我们似乎没有在任何地方声明CFoo&lt;int&gt;CFoo&lt;int&gt; 肯定没有在翻译单元 b.cpp 上声明,因为标题没有出现在该翻译单元上,并且翻译单元 c.cpp 被告知它不需要实现 CFoo。那到底是怎么回事?

对 makefile 进行一项更改:将 -O1 替换为 -O0,然后 make clean make

现在,链接调用导致错误(使用 gcc 4.8.4)

c.o: In function `run()':
c.cpp:(.text+0x10): undefined reference to `CFoo<int>::CFoo()'

如果一开始没有内联,这是我们预期的错误。至少这是我得出的结论,非常欢迎进一步的想法。

要获得与 -O1 的链接,我们需要信守承诺并提供 CFoo 的实现,我们在文件 a.cpp 中提供了这一点

#include "a.h"
template void foo<int>();

我们现在可以保证 CFoo 出现在 a.cpp 的翻译单元上,并且我们的承诺将得到遵守。顺便说一句,请注意 a.cpp 中的 template void foo&lt;int&gt;() 通过包含 a.h 在 extern template void foo&lt;int&gt;() 前面,这没有问题。

最后,我发现这种不可预测的优化依赖行为令人讨厌,因为这意味着如果没有内联,对 ah 的修改和 a.cpp 的重新编译可能不会像预期的那样反映在 run() 中(尝试更改 Foo 构造函数的标准输出并重新制作)。

【讨论】:

    【解决方案3】:

    这是一个有趣的例子:

    #include <algorithm>
    #include <string>
    
    extern template class std::basic_string<char>;
    int foo(std::string s)
    {
        int res = s.length();
        res += s.find("some substring");
        return res;
    }
    

    当在 -O3 使用 g++-7.2 编译时,这会产生对 string::find 的非内联调用,但会产生对 string::size 的内联调用。

    虽然没有 extern 模板,但实际上所有内容都是内联的。 Clang 具有相同的行为,MSVC 在任何情况下几乎都无法内联任何内容。

    所以答案是:这取决于,编译器可能对此有特殊的启发式。

    【讨论】:

      猜你喜欢
      • 2020-02-15
      • 2014-12-24
      • 2012-09-23
      • 1970-01-01
      • 1970-01-01
      • 2013-07-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多