C ++ Кращий метод роботи з реалізацією для великих шаблонів


10

Зазвичай при оголошенні класу C ++ найкраще застосовувати лише декларацію у файлі заголовка та розміщувати реалізацію у вихідному файлі. Однак, схоже, що ця модель дизайну не працює для шаблонових класів.

Якщо ви шукаєте в Інтернеті, існує 2 думки щодо найкращого способу управління шаблонами класів:

1. Вся заява та реалізація в заголовку.

Це досить просто, але призводить до того, що, на мій погляд, важко підтримувати та редагувати файли коду, коли шаблон стає великим.

2. Запишіть реалізацію в шаблон, включаючи файл (.tpp), включений в кінці.

Це здається кращим рішенням для мене, але, здається, не застосовується широко. Чи є причина, що цей підхід поступається?

Я знаю, що багато разів стиль коду продиктовано особистими уподобаннями чи застарілим стилем. Я починаю новий проект (перенесення старого проекту C на C ++), і я відносно новий в розробці ОО, і хотів би слідувати кращим практикам з самого початку.


1
Дивіться цю 9-річну статтю на codeproject.com. Метод 3 - це те, що ви описали. Здається, це не таке особливе, як ви вважаєте.
Док Браун

.. або тут, такий же підхід, стаття з 2014 року: codeofhonour.blogspot.com/2014/11/…
Doc Brown

2
Тісно пов’язані: stackoverflow.com/q/1208028/179910 . Gnu зазвичай використовує розширення ".tcc" замість ".tpp", але в іншому випадку це майже однаково.
Джеррі Труну

Я завжди використовував "ipp" як розширення, але багато робив те саме в написаному коді.
Себастьян Редл

Відповіді:


6

Коли ви пишете шаблонний клас 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) може бути хорошим варіантом, якщо ви змусите розробників прийняти додаткове включення.


2

Подібно до .tppідеї (яку я ніколи не бачив, як ми використовували), ми вкладаємо більшість вбудованих функціональних можливостей у -inl.hppфайл, який міститься в кінці звичайного .hppфайлу.

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


1

Одна з монет другого варіанту полягає в тому, що ваші заголовки виглядають акуратніше.

Можливо, у вас може бути вбудована перевірка помилок IDE і вкручені прив’язки налагодження.


2-го також вимагає багато надмірності декларації параметрів шаблону, що може стати дуже багатослівним, особливо при використанні sfinae. І всупереч ОП мені здається, що 2-й складніше читати більше коду, зокрема, через надлишкову котельну плиту.
Сопель

0

Я дуже віддаю перевагу підходу розміщення реалізації в окремому файлі та наявності лише документації та декларацій у файлі заголовка.

Можливо, причиною того, що ви багато не бачили такого підходу на практиці, є те, що ви не шукали потрібних місць ;-)

Або - можливо, це тому, що для розробки програмного забезпечення потрібно трохи додаткових зусиль. Але для бібліотеки класів це зусилля вартує того часу, IMHO, і окупається в набагато простішій у використанні / читанні бібліотеці.

Візьмемо для прикладу цю бібліотеку: https://github.com/SophistSolutions/Stroika/

Вся бібліотека написана з таким підходом, і якщо ви подивитесь на код, ви побачите, наскільки добре він працює.

Файли заголовків приблизно такі, як файли реалізації, але вони заповнені лише деклараціями та документацією.

Порівняйте читабельність Stroika з вашою улюбленою реалізацією std c ++ (gcc або libc ++ або msvc). Усі вони використовують вбудований підхід до заголовка, і хоча вони надзвичайно добре написані, IMHO, не як читабельні реалізації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.