Коли ви пишете шаблонний клас 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()
{
...
}
Про:
- Дуже зручне використання (просто включіть заголовок).
Con:
- Інтерфейс та реалізація методу змішані. Це "просто" проблема читабельності. Деякі вважають це нездійсненним, оскільки він відрізняється від звичайного .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()
{
...
}
Про:
- Дуже зручне використання (просто включіть заголовок).
- Інтерфейс та визначення методів розділені.
Con:
- Високий вплив на перебудову (такий же, як (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()
{
...
}
Про:
- Зменшує вплив на перебудову так само, як і розділення .h / .cpp.
- Інтерфейс та визначення методів розділені.
Con:
- Незручне використання: додаючи
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) може бути хорошим варіантом, якщо ви змусите розробників прийняти додаткове включення.