C ++ Предпочтительный метод работы с реализацией для больших шаблонов


10

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

При поиске в Интернете, кажется, есть 2 мнения о лучшем способе управления шаблонами классов:

1. Вся декларация и реализация в заголовке.

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

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

Мне кажется, что это лучшее решение, но оно не находит широкого применения. Есть ли причина, по которой этот подход уступает?

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


1
Смотрите эту 9-летнюю статью на codeproject.com. Метод 3 - это то, что вы описали. Не кажется особенным, как вы думаете.
Док Браун

.. или здесь, тот же подход, статья 2014 года: codeofhonour.blogspot.com/2014/11/…
Док Браун

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()
{
    ...
}

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


2

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

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


1

Одна про монета второго варианта в том, что ваши заголовки выглядят более аккуратно.

Кон может быть, у вас может быть встроенная проверка ошибок IDE, и привязки отладчика облажались.


2-й также требует много избыточности объявления параметров шаблона, которая может стать очень многословной, особенно при использовании sfinae. И в отличие от OP, я считаю, что 2-й труднее читать, чем больше кода, особенно из-за избыточного шаблона.
Сопель

0

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

Возможно, причина того, что вы не видели такого подхода на практике, заключается в том, что вы не смотрели в нужных местах ;-)

Или - возможно, потому что это требует немного дополнительных усилий при разработке программного обеспечения. Но для библиотеки классов, эти усилия стоят ПОЛНОСТЬЮ, ИМХО, и окупаются в гораздо более простой в использовании / чтении библиотеке.

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

Вся библиотека написана с использованием этого подхода, и если вы посмотрите код, вы увидите, насколько хорошо он работает.

Заголовочные файлы примерно такие же, как файлы реализации, но они заполнены только декларациями и документацией.

Сравните удобочитаемость Stroika с вашей любимой реализацией std c ++ (gcc или libc ++ или msvc). Все они используют встроенный подход реализации в заголовке, и, хотя они написаны очень хорошо, ИМХО, они не так читаемы.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.