Как в C++ скрыть определение вспомогательных типов?

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

class MyClass1 
{
  MyStruct2 member;
  ... 
}

Обязательно ли в заголовочном файле размещать полное определение MyStruct2 перед определением MyClass1? Если обязательно, то нельзя ли задать MyStruct2 хотя бы с помощью прототипа. С одной стороны, компилятор должен знать MyStruct2, чтоб правильно понять определение MyClass1, но в то же время содержимое .h по логике является интерфейсом модуля и не хочется туда включать лишнее.

Вообще, я никак не разберусь, как организовывать модули. В модуле есть функции, которые должны быть доступны извне, а есть вспомогательные. Также есть основные и вспомогательные типы. Как правильно все это оформить, что записывать в .h, что в .cpp.

Такая схема правильная?
.h
typedef ... LocalType1;
...
class ... LocalClass1 {...};
...
typedef ... GlobalType1;
...
class ... GlobalClass1 {...};
...
GlobalFunc1(...);

.cpp
LocalFunc1(...);
...
LocalFunc1(...)
{
 ...
}
...
GlobalFunc1(...)
{
 ...
}
  • Вопрос задан
  • 976 просмотров
Решения вопроса 1
@MarkusD Куратор тега C++
все время мелю чепуху :)
В целом, твой вопрос не имеет точного ответа, т.к. вопрос затрагивает темы вкуса цветных карандашей и просто является риторическим.

Помимо всей этой воды, что я ниже изложил, еще очень стоит ознакомиться с разделом "SF: Source files" официального гайдлайна:
https://github.com/isocpp/CppCoreGuidelines/blob/m...
(Очень советую изучить весь гайдлайн от корки до корки)

По первому вопросу, коротко: если тип используется по значению - полное объявление типа обязательно, иначе можно обойтись Forward declaration.

Немного подробнее:

Существует такой принцип формирования проекта, когда каждый заголовочный файл предоставляет полную информацию о зависимостях. Forward declaration в таком случае или запрещено, или сильно порицается. Каждый заголовочный файл должен обязательно включать в себя заголовочные файлы всех зависимостей. А файл исходного кода должен включать ровно один заголовок - тот, чей интерфейс реализуется в исходном коде.
Это позволяет сразу видеть все зависимости кода, не париться с размещением файлов, не париться с транзитивными зависимостями, просто не париться, а так же существенно огребать на времени компиляции. Особенно если в проекте разрешен только #include "", а #include <> порицается.
В качестве примера можно почитать UE4.

Существует и другой принцип формирования проекта, когда файловая структура отражает макроуровень архитектуры проекта. Скажем, если проект базируется на подсистемах или слоях, каждый слой(подсистема) представляется ровно одним заголовочным мастер-файлом и своей отдельной папкой в файловой системе. Внутри этой папки расположены все внутренние заголовочные файлы подсистемы. Ни один внутренний заголовочный файл не содержит никаких зависимостей, только объявление своего интерфейса. Все зависимости (FD, другие подсистемы или слои, системные заголовки и.т.д.) описывает только мастер-файл, он же в нужной последовательности подключает все внутренние заголовки. Другие подсистемы, равно как и исходный код самой подсистемы, включают в себя только мастер-файл.
Это позволяет снять всю шелуху с внутренних заголовков и сосредоточить их текст ровно на декларации интерфейса. С другой стороны, ради использования какого-нибудь мелкого типчика всегда приходится подключать всю подсистему, т.к. внутренние заголовки подключать нельзя по правилам формирования проекта и потому что их зависимости неясны.

В обоих принципах применяется одно очень важное правило: Один класс - один комплект исходного кода с именем самого класса. Это, можно сказать, самый базовый принцип формирования проектов. Классы с инвариантом и богатым функционалом должны быть объявлены каждый в своем отдельном заголовке, имеющим имя класса. Описание функционала каждого такого класса должно лежать в своем отдельном файле исходного кода с именем описываемого класса. Иногда и вовсе требуется несколько файлов исходного кода на один класс, потому что класс выполняет слишком много функций, но разделять его нельзя.
Этот принцип нередко приводит к проблеме циклической зависимости, когда два класса ссылаются друг на друга и в каждом заголовке необходимо включение второго заголовка. В этом случае помогает или редизайн классов для ослабления зависимостей, или Forward declaration как меньшее из зол.

С точки зрения компилятора есть только один формат файла - формат исходного кода, который ему и надо обработать.
С точки зрения человека форматов файла не два, а 3 или 4:
  • .c , .cc , .cxx , .cpp , c++ - формат исходного кода, в котором стоит производить определение интерфейсов и держать все приватные инструменты (код и типы);
  • .h , .hh , .hpp - формат заголовка, в котором подключаются заголовки зависимостей и объявляется интерфейс - ровно то, что может понадобиться в другом коде или не может быть определено в файле исходного кода. И ничего больше;
  • .inl - формат вспомогательного заголовка, в котором производится определение inline функций и сложных шаблонных конструкций;
  • .inc - формат вспомогательного заголовка, в котором описываются форварды, внешние глобальные переменные, константы и прочие данные. Этот формат используется реже всего. Вместо него чаще используют формат заголовка (.h), размещая в нем весь контент .inc файла.


Если с "человеческими форматами" все должно быть хорошо понятно, то с форматом файла для компилятора стоит уяснить одну тонкость - все эти человеческие шахматы с бубнами и делением на файлы должны складываться в как можно более удобный для компиляции вид. Чем меньше одинаковых #include, тем лучше. Чем меньше #include в целом, тем лучше. Трансляция - дело итак нелегкое.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 2
Adamos
@Adamos
class MyClass1 
{
  MyStruct2 member; // компилятору необходимо полное описание структуры - она является частью класса (в частности, влияет на размер объекта в памяти)
  MyStruct2 *member; // компилятору не требуются подробности. Если этот член не используется вне класса - описание структуры спокойно может лежать только в cpp-файле этого класса, больше оно никому не понадобится.
  ... 
}
Ответ написан
Комментировать
@Johanga
class MyStruct2; // forward declaraion
class MyStruct1
{
   MyStruct2 member;
};


можно интерфейс или pImpl
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы