【问题标题】:Templates: Use forward declarations to reduce compile time?模板:使用前向声明来减少编译时间?
【发布时间】:2010-10-08 00:09:51
【问题描述】:

我必须处理一个包含许多模板类的库,这些类当然都是在头文件中实现的。现在我正试图找到一种方法来减少令人难以忍受的长编译时间,因为我几乎必须将整个库包含在我的每个编译单元中。

尽管有模板,但是否有可能使用前向声明?我正在尝试以下示例中的内容,例如,我试图绕过#include <vector>,但它给了我一个链接器错误,因为push_back 未定义。

#include <iostream>

namespace std {
  template<class T>
  class vector {
  public:
    void push_back(const T& t);
  };
}

int main(int argc, char** argv) {
  std::vector<int>* vec = new std::vector<int>();
  vec->push_back(3);
  delete vec;
  return EXIT_SUCCESS;
}

$ g++ fwddecl.cpp
ccuqbCmp.o(.text+0x140): In function `main':
: undefined reference to `std::vector<int>::push_back(int const&)'
collect2: ld returned 1 exit status

我尝试了一次预编译的头文件,但这根本没有改变编译时间(我确实确保它们确实被加载而不是真正的头文件)。但是如果你们都说预编译的头文件应该是要走的路,那么我会再试一次。

更新: 有人说不值得前向声明 STL 类。我要强调的是,上面的 STL vector 只是一个例子。我并没有真正尝试前向声明 STL 类,但它是关于我必须使用的某些库的其他大量模板化的类。

更新 2: 有没有办法让上面的例子真正编译和链接正确? Logan 建议使用-fno-implicit-templates 并将template class std::vector&lt;int&gt; 放在某个地方,大概是放在一个单独的.cpp 文件中,该文件使用-fno-implicit-templates 编译,但我仍然遇到链接器错误。同样,我试图了解它对std::vector 的工作原理,以便我可以将其应用于我实际使用的模板类。

【问题讨论】:

  • 在您的示例中,您没有转发任何声明。您所做的只是在命名空间 std 中创建一个名为 vector 的模板类。然后你没有定义你在其中声明的 push_back 方法。因此链接器错误。
  • 第二个埃文解释为什么前向声明 std::vector 不起作用(您在尖括号内至少缺少一个参数)。尝试使用您自己编写的模板类,您知道它没有默认模板参数。

标签: c++ templates linker compilation forward-declaration


【解决方案1】:

您不能像这样转发声明类的“部分”。即使可以,您仍然需要在某处实例化代码,以便您可以链接它。有很多方法可以处理它,你可以让自己成为一个带有通用容器(例如向量)实例化的小库并将它们链接起来。然后你只需要编译例如矢量 一次。要实现这一点,您需要使用 -fno-implicit-templates 之类的东西,至少假设您坚持使用 g++ 并使用 template class std::vector&lt;int&gt; 显式实例化库中的模板


所以,一个真实的工作示例。这里我有 2 个文件,a.cp​​p 和 b.cpp

a.cpp:

#include <vector> // still need to know the interface
#include <cstdlib>

int main(int argc, char **argv) {
  std::vector<int>* vec = new std::vector<int>();
  vec->push_back(3);
  delete vec;
  return EXIT_SUCCESS;
}

所以现在我可以用-fno-implicit-templates 编译a.cpp:

g++ -fno-implicit-templates -c a.cpp

这会给我一个 a.o.如果我然后我尝试链接 a.o 我得到:

g++ a.o
/usr/bin/ld: Undefined symbols:
std::vector<int, std::allocator<int> >::_M_insert_aux(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&)
void std::_Destroy<int*, std::allocator<int> >(int*, int*, std::allocator<int>)
collect2: ld returned 1 exit status

不好。所以我们转向b.cpp:

#include <vector>
template class std::vector<int>;
template void std::_Destroy(int*,int*, std::allocator<int>);
template void std::__uninitialized_fill_n_a(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, unsigned long, int const&, std::allocator<int>);
template void std::__uninitialized_fill_n_a(int*, unsigned long, int const&, std::allocator<int>);
template void std::fill(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, int const&);
template __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > > std::fill_n(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, unsigned long, int const&);
template int* std::fill_n(int*, unsigned long, int const&);
template void std::_Destroy(__gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >, std::allocator<int>);

现在你对自己说,这些额外的模板是从哪里来的?我看到了template class std::vector&lt;int&gt;,这很好,但是其余的呢?简短的回答是,这些东西的实现必然有点混乱,当你手动实例化它们时,这些混乱的一部分会泄露出来。您可能想知道我是如何弄清楚我需要实例化什么的。好吧,我使用了链接器错误;)。

所以现在我们编译 b.cpp

g++ -fno-implicit-templates -c b.cpp

我们得到了 b.o.链接 a.o 和 b.o 我们可以得到

g++ a.o b.o

万岁,没有链接器错误。

因此,要详细了解您更新的问题,如果这是一门自酿课程,则不一定要如此混乱。例如,您可以将接口与实现分开,例如假设我们除了 a.cpp 和 b.cpp 之外还有 c.h、c.cpp

c.h

template<typename T>
class MyExample {
  T m_t;
  MyExample(const T& t);
  T get();
  void set(const T& t);
};

c.cpp

template<typename T>
MyExample<T>::MyExample(const T& t) : m_t(t) {}
template<typename T>
T MyExample<T>::get() { return m_t; }
template<typename T>
void MyExample<T>::set(const T& t) { m_t = t; }

a.cpp

 #include "c.h" // only need interface
 #include <iostream>
 int main() {
   MyExample<int> x(10);
   std::cout << x.get() << std::endl;
   x.set( 9 );
   std::cout << x.get() << std::endl;
   return EXIT_SUCCESS;
 }

b.cpp,“库”:

 #include "c.h" // need interface
 #include "c.cpp" // need implementation to actually instantiate it
 template class MyExample<int>;

现在您将 b.cpp 编译为 b.o 一次。当 a.cpp 更改时,您只需要重新编译它并链接到 b.o.

【讨论】:

  • 优秀的答案!感谢您逐步完成:)
  • +1 长模板构建时间让我抓狂! tyvm 有关如何摆脱模板构建地狱的详细示例...
【解决方案2】:

前向声明让您可以这样做:

template <class T> class vector;

然后你可以在不定义向量的情况下声明对vector&lt;whatever&gt;的引用和指针(不包括vector的头文件)。这与常规(非模板)类的前向声明相同。

尤其是模板的问题是,您通常不仅需要类声明,还需要头文件中的所有方法定义(以便编译器可以实例化所需的模板)。显式模板实例化(可以通过-fno-implicit-templates 强制使用)是一种解决方法;您可以将您的方法定义放在源文件中(或者,按照Google Style Guide 的示例,在您不必包含的-inl.h 头文件中)然后像这样显式实例化它们:

template <class int> class vector;

请注意,您实际上并不需要-fno-implicit-templates 才能从中受益;编译器将默默地避免实例化它没有定义的任何模板,假设链接器稍后会解决它。添加-fno-implicit-templates 会使all 模板的使用更加困难(不仅仅是耗时的模板),所以我不推荐它。

您的示例代码的问题在于您没有转发声明真正的std::vector 类。如果不包含&lt;vector&gt;,您将创建自己的非标准vector 类,并且您从未定义push_back,因此编译器无需实例化。

我使用预编译的头文件效果很好;我不确定他们为什么不帮助你。您将所有不变的标头放在一个 all.h 中,对其进行预编译,并使用 strace 或类似方法验证 all.h.pch 已加载且单个头文件未加载? (如何使用strace:不是运行g++ mytest.cc,而是运行strace -o strace.out g++ mytest.cc,然后在文本编辑器中查看strace.out并搜索open(调用以查看正在读取哪些文件。)

【讨论】:

  • 你提出了一个很好的观点 wrt -fno-implicit-templates。当问题在于编译速度时,我对“前向声明”模板的想法有点分心。
【解决方案3】:

使用前向声明,您只能将成员或参数声明为指向该类型的指针或引用。您不能使用任何方法或其他需要所述类型的内脏的东西。也就是说,我发现前向声明在尝试加快编译时间时确实受到限制。我建议您进一步研究预编译头文件的可能性,因为我发现它们确实有助于缩短编译时间,尽管这是在 Windows 上使用 Visual C++ 而不是 g++。

【讨论】:

  • 如果您使用的是 cmake,cotire 插件非常适合为您处理 PCH。 github.com/sakra/cotire 超级好用而且“好用”
【解决方案4】:

&lt;iosfwd&gt; 会为您提供 iostream 类的一些前向声明,但一般而言,就 stl 模板的前向声明而言,您无能为力。

预编译的标头是要走的路。第一次编译它们时您不会注意到任何速度增加,但每次修改预编译头(或其中包含的任何内容)时,您应该只支付一次该价格。

See this question 了解有关加快编译速度的其他想法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-08
    相关资源
    最近更新 更多