Шаблоны: использовать форвардные объявления для сокращения времени компиляции?

Мне приходится иметь дело с библиотекой, состоящей из многих шаблонов, которые, конечно же, реализованы в файлах заголовков. Теперь я пытаюсь найти способ уменьшить невыносимо длинные сроки компиляции, которые исходят из того факта, что я в значительной степени должен включать всю библиотеку в каждую и одну из моих единиц компиляции.

Является ли использование форвардных объявлений возможностью, несмотря на шаблоны? Я пытаюсь сделать что-то в соответствии с приведенным ниже примером, где я попытался обойти #include <vector> в качестве примера, но он дал мне ошибку компоновщика, потому что push_back - undefined.

#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: Есть ли способ сделать вышеприведенный пример собственно компилировать и правильно связывать? Логан предлагает использовать -fno-implicit-templates и помещать template class std::vector<int> где-то, предположительно в отдельный файл .cpp, который скомпилируется с помощью -fno-implicit-templates, но я все еще получаю ошибки компоновщика. Опять же, я пытаюсь понять, как это работает для std::vector, чтобы затем применить его к темплатированным классам, которые я фактически использую.

Ответы

Ответ 1

Вы не можете переслать декларацию "частей" таких классов. Даже если бы вы могли, вам все равно нужно было бы создать код где-нибудь, чтобы вы могли ссылаться на него. Есть способы справиться с этим, вы можете сделать себе небольшую библиотеку с экземплярами общих контейнеров (например, вектор) и связать их. Тогда вам нужно будет когда-либо компилировать, например. вектор <Int> один раз. Чтобы реализовать это, вам нужно использовать что-то вроде -fno-implicit-templates, по крайней мере предполагая, что вы придерживаетесь g++ и явно создаете шаблон в своей библиотеке с помощью template class std::vector<int>


Итак, настоящий рабочий пример. Здесь у меня есть 2 файла, a.cpp и 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;
}

Итак, теперь я могу скомпилировать a.cpp с -fno-implicit-templates:

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<int> и это прекрасно, но как насчет остальной части? Хорошо, что короткий ответ заключается в том, что эти реализации по необходимости немного беспорядочны, и когда вы вручную их создаете, то, в свою очередь, некоторые из этих беспорядок выходят из строя. Вам, наверное, интересно, как я даже понял, что мне нужно для создания экземпляра. Ну, я использовал ошибки компоновщика;).

Итак, теперь мы компилируем b.cpp

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

И мы получаем b.o. Связывая a.o и b.o, мы можем получить

g++ a.o b.o

Ура, нет ошибок компоновщика.

Итак, чтобы подробно рассказать о вашем обновленном вопросе, если это домашний класс, он не обязательно должен быть таким грязным. Например, вы можете отделить интерфейс от реализации, например. скажем, у нас есть c.h, c.cpp, в дополнение к a.cpp и b.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, "library":

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

Ответ 2

Передовые декларации позволяют сделать это:

template <class T> class vector;

Затем вы можете объявлять ссылки и указатели на vector<whatever> без определения вектора (без включения заголовочного файла vector). Это работает так же, как форвардные объявления обычных (не шаблонных) классов.

Проблема с шаблонами в частности заключается в том, что вам обычно требуется не только объявление класса, но и все определения методов в вашем файле заголовка (чтобы компилятор мог создавать экземпляры необходимых шаблонов). Явное создание шаблона (которое вы можете принудительно использовать с помощью -fno-implicit-templates) является обходным путем для этого; вы можете поместить определения метода в исходный файл (или, следуя примеру Руководство по стилю Google, в заголовочном файле -inl.h, который вы не обязательно включать), а затем явно создавать их следующим образом:

template <class int> class vector;

Обратите внимание, что вам действительно не нужно -fno-implicit-templates, чтобы воспользоваться этим; компилятор будет молча избегать создания экземпляров любых шаблонов, для которых он не имеет определений, исходя из предположения, что компоновщик выяснит это позже. И добавление -fno-implicit-templates сделает использование всех шаблонов более сложным (не только трудоемким), поэтому я бы не рекомендовал его.

Проблема с вашим примером кода заключается в том, что вы не переадресовываете истинный класс std::vector. Не включая <vector>, вы создаете свой собственный, нестандартный класс vector, и вы никогда не определяете push_back, поэтому для компилятора не существует экземпляра.

Я использовал прекомпилированные заголовки для отличного эффекта; Я не знаю, почему они вам не помогли. Вы помещаете все ваши неизменяемые заголовки в один all.h, предварительно скомпилировали его и подтвердили с помощью strace или похожим, что all.h.pch был загружен, а отдельные файлы заголовков не были? (Как использовать strace: вместо запуска g++ mytest.cc запустите strace -o strace.out g++ mytest.cc, затем просмотрите strace.out в текстовом редакторе и выполните поиск open( вызовов, чтобы увидеть, какие файлы читаются.)

Ответ 3

С помощью форвардных объявлений вы можете объявлять только члены или параметры в качестве указателя или ссылки на этот тип. Вы не можете использовать какие-либо методы или другие вещи, которые требуют встроенных функций указанного типа. Тем не менее я обнаружил, что декларации действительно ограничивают попытку ускорить время компиляции. Я предлагаю вам изучить возможность прекомпилированных заголовков немного больше, так как я нашел, что они действительно помогают с моментами компиляции, хотя это было с использованием Visual С++ в Windows, а не g++.

Ответ 4

Есть <iosfwd>, который даст вам некоторое объявление вперед для классов iostream, но, как правило, вы не можете много сделать с шаблонами stl с точки зрения переадресации объявления.

Предварительно скомпилированные заголовки - это путь. Вы не заметите увеличения скорости при первом их компиляции, но вы должны заплатить эту цену только один раз за каждый раз, когда вы изменяете предварительно скомпилированный заголовок (или все, что включено в него).

См. этот вопрос для других идей об ускорении компиляции.