Time Shield Library
C++ library for working with time
Loading...
Searching...
No Matches
Рекомендации по владению заголовками и файлами реализации

Этот документ задаёт переносимую политику для организации .hpp, .ipp и .tpp файлов в C++ проектах.

Текст намеренно написан в обобщённом виде, чтобы его можно было перенести в другой репозиторий с минимальными правками.

Канонический язык

Канонической версией этого документа является английская:

Agent playbook:

  • agents/header-implementation-guidelines.md
  • agents/header-implementation-guidelines-RU.md

Назначение

Используйте эти рекомендации, когда нужно решить:

  • что должно жить в .hpp, .tpp и .ipp
  • нужен ли подсистеме единый агрегирующий заголовок
  • где располагать общие STL и доменные зависимости
  • когда forward declarations полезны, а когда только засоряют код
  • как держать build-mode helper'ы и implementation-only зависимости у настоящей границы реализации

Роли файлов

.hpp

.hpp используется для:

  • публичных объявлений
  • публичных типов и конфигурации
  • владения публичным include-контрактом
  • подключения template-visible реализации при необходимости

Из заголовка должно быть понятно, является ли он:

  • самостоятельным leaf-header
  • или частью aggregate-first подсистемы

.tpp

.tpp используется для template-visible реализации.

Правила:

  • определения шаблонов должны быть видимы там, где шаблон может быть инстанцирован
  • такой код должен жить либо прямо в .hpp, либо в .tpp, который всегда подключается из .hpp
  • нельзя прятать consumer-instantiated templates за implementation-only путями подключения

.ipp

.ipp используется для нетemplate-реализации, которую удобно хранить рядом с объявлением.

Типичные сценарии:

  • общая реализация для header-only и compiled-library режимов
  • inline-capable helper bodies
  • определения методов вне класса, которые концептуально принадлежат тому же заголовку

Модель владения

Реализация, всегда подключаемая из заголовка

Такой вариант подходит только для кода, который действительно должен принадлежать заголовку:

  • шаблоны
  • маленькие leaf helpers
  • короткие inline-методы
  • логика, которую осознанно нужно показывать во всех translation units

Этот подход уместен, когда файл действительно должен работать как самостоятельный leaf-header.

Реализация с явным ownership

Явный ownership нужен там, где реализацию не надо раздавать во все consumer TU:

  • большие нетemplate-методы
  • код, который должен эмититься один раз compiled ownership translation unit
  • общая реализация для header-only и static-library режимов

В этой модели:

  • публичный заголовок объявляет интерфейс
  • файл реализации находится рядом
  • compiled ownership TU отвечает за one-TU emission в non-header-only сборках

Aggregate entry header и leaf header

У каждой подсистемы должен быть осознанно выбран основной include-подход.

Aggregate-first подсистема

Единый entry header уместен, если:

  • подсистема обычно используется целиком
  • несколько leaf headers естественно разделяют общий include-context
  • централизация зависимостей делает структуру понятнее

В этой модели:

  • обычный consumer должен подключать агрегирующий заголовок
  • общие STL и доменные include'ы можно собирать там
  • leaf headers не нужно искусственно превращать в самостоятельные public entrypoints

Standalone-leaf-first подсистема

Самодостаточные leaf headers уместны, если:

  • отдельные заголовки реально ожидаются к независимому использованию
  • leaf types переиспользуются в разных несвязанных частях кода
  • широкий агрегатор создавал бы лишнюю связанность

В этой модели:

  • каждый leaf-header сам владеет зависимостями своего публичного контракта
  • always-included реализация допустимее, если leaf-header действительно задуман как самодостаточный

Размещение зависимостей

Общие STL и доменные include'ы

Централизуйте общие include'ы только тогда, когда у подсистемы действительно есть канонический aggregate entry header.

Это оправдано, если:

  • несколько leaf headers зависят от одного и того же небольшого набора STL headers
  • агрегирующий заголовок уже является intended public entrypoint
  • централизация уменьшает дублирование и не скрывает ownership

Не стоит централизовать зависимости только ради экономии нескольких строк include, если подсистема построена вокруг самостоятельных leaf headers.

Implementation-only helper'ы

Build-mode helper'ы, implementation macros и похожая инфраструктура должны находиться на границе реализации.

Предпочтительное размещение:

  • в .ipp, который реально их использует
  • либо в его непосредственном owning .hpp, если это действительно самая узкая корректная граница

Не размазывайте такие helper'ы через широкие aggregate headers, если это не требуется всем корректным путям использования.

Forward declarations

Используйте forward declarations только там, где они дают реальную структурную пользу:

  • уменьшают неизбежную связанность
  • помогают чисто разорвать циклы
  • позволяют удержать стабильный leaf interface компактным

Избегайте их, если они в основном:

  • скрывают явные зависимости
  • делают код чувствительным к include-order
  • добавляют визуальный шум без заметной архитектурной пользы

Если у подсистемы уже есть канонический aggregate entry header, обычно лучше предпочесть ясную и плоскую структуру зависимостей, а не механически максимизировать число forward declarations.

Границы private / vendor реализации

Код приложения не должен подключать private implementation files зависимостей, чтобы обойти границы public API.

Если нужная возможность недоступна через публичные заголовки:

  • либо корректно расширьте саму зависимость
  • либо держите недостающий ownership локально в приложении

Не стоит обходить границу, напрямую подключая vendor .ipp или другие private implementation details из application-кода.

Как тестировать include-контракт

Тесты должны проверять тот include-контракт, который проект действительно собирается поддерживать.

Для aggregate-first подсистем:

  • добавляйте public-facing тесты через aggregate entry header
  • не закрепляйте случайно более сильный standalone-leaf контракт, если он не был целью

Для white-box тестов:

  • прямое подключение leaf headers допустимо
  • такие тесты должны сами явно объявлять свои prerequisites
  • internal tests не должны молча переопределять public include policy подсистемы

Практический checklist

Перед тем как считать новую header-layout схему завершённой, проверьте:

  • template-visible код доступен из заголовков
  • ownership .ipp задан явно и последовательно
  • подсистема осознанно выбрала aggregate-first или standalone-leaf-first модель
  • общие зависимости централизованы только там, где это соответствует выбранной модели
  • implementation-only helper'ы находятся рядом с реальной границей реализации
  • forward declarations используются по структурной необходимости, а не по привычке
  • тесты проверяют intended include contract, а не случайно возникший