При написании шаблонного класса C ++ у вас обычно есть три варианта:
(1) Поместите объявление и определение в заголовок.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
или
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
Pro:
- Очень удобное использование (просто включите заголовок).
Против:
- Интерфейс и метод реализации являются смешанными. Это «просто» проблема читабельности. Некоторые считают это неосуществимым, поскольку оно отличается от обычного подхода .h / .cpp. Однако следует помнить, что это не проблема для других языков, например, C # и Java.
- Высокий эффект перестроения: если вы объявляете новый класс
Foo
как член, вам необходимо включить foo.h
. Это означает, что изменение реализации Foo::f
распространяется как через заголовочные, так и исходные файлы.
Давайте подробнее рассмотрим влияние перестройки: для не шаблонных классов C ++ вы помещаете объявления в .h и определения методов в .cpp. Таким образом, при изменении реализации метода необходимо перекомпилировать только один .cpp. Это отличается для шаблонных классов, если .h содержит весь ваш код. Посмотрите на следующий пример:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
Здесь единственное использование Foo::f
внутри bar.cpp
. Однако, если вы измените реализацию Foo::f
, и то, bar.cpp
и другое qux.cpp
необходимо перекомпилировать. Реализация Foo::f
живет в обоих файлах, хотя ни одна из частей Qux
напрямую не использует ничего из Foo::f
. Для крупных проектов это может скоро стать проблемой.
(2) Поместите объявление в .h, а определение в .tpp и включите его в .h.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
#include "foo.tpp"
// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
...
}
Pro:
- Очень удобное использование (просто включите заголовок).
- Определения интерфейса и метода разделены.
Против:
- Сильное восстановление (аналогично (1) ).
Это решение разделяет объявление и определение метода в двух отдельных файлах, подобно .h / .cpp. Однако этот подход имеет ту же проблему перестройки, что и (1) , потому что заголовок напрямую включает определения методов.
(3) Поместить объявление в .h и определение в .tpp, но не включать .tpp в .h.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
Pro:
- Уменьшает влияние перестроения так же, как разделение .h / .cpp.
- Определения интерфейса и метода разделены.
Против:
- Неудобное использование: при добавлении
Foo
члена в класс Bar
, вы должны включить foo.h
в заголовок. Если вы звоните Foo::f
в .cpp, вы также должны включить foo.tpp
туда.
Такой подход уменьшает влияние перестроения, поскольку Foo::f
необходимо перекомпилировать только те файлы .cpp, которые действительно используются . Однако это имеет свою цену: все эти файлы должны быть включены foo.tpp
. Возьмите пример сверху и используйте новый подход:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
Как видите, единственное отличие заключается в дополнительном включении foo.tpp
в bar.cpp
. Это неудобно, и добавление второго включения для класса в зависимости от того, вызываете ли вы методы для него, кажется очень уродливым. Тем не менее, вы уменьшаете влияние перестроения: bar.cpp
требуется перекомпиляция только при изменении реализации Foo::f
. Файл не qux.cpp
нуждается в перекомпиляции.
Резюме:
Если вы внедряете библиотеку, вам обычно не нужно заботиться о последствиях перестройки. Пользователи вашей библиотеки берут релиз и используют его, и реализация библиотеки не меняется в повседневной работе пользователя. В таких случаях библиотека может использовать подход (1) или (2), и выбор языка зависит от вкуса.
Однако, если вы работаете с приложением или работаете с внутренней библиотекой вашей компании, код часто меняется. Таким образом, вы должны заботиться о восстановлении воздействия. Выбор подхода (3) может быть хорошим вариантом, если вы заставите своих разработчиков принять дополнительное включение.