Когда можно использовать предварительное объявление?
Я ищу определение, когда я могу использовать предварительное объявление класса в заголовочном файле другого класса.
Разрешено ли мне использовать предварительное объявление для базового класса, для класса, который является членом, для класса, передаваемого в функцию-член по ссылке и т.д.?
5 ответ(ов)
Основное правило заключается в том, что вы можете лишь предварительно объявлять классы, чья структура памяти (а значит, и члены функций, и данные-члены) не должны быть известны в файле, где вы делаете это предварительное объявление.
Это исключает базовые классы и все классы, которые используются иначе как через ссылки и указатели.
В дополнение к указателям и ссылкам на неполные типы, вы также можете объявлять прототипы функций, которые указывают параметры и/или возвращаемые значения, имеющие неполные типы. Однако вы не можете определить функцию, принимающую параметр или имеющую возвращаемый тип как неполной, если это не указатель или ссылка.
Примеры:
struct X; // Предварительное объявление X
void f1(X* px) {} // Разрешено: указатель всегда можно использовать
void f2(X& x) {} // Разрешено: ссылку всегда можно использовать
X f3(int); // Разрешено: возвращаемое значение в прототипе функции
void f4(X); // Разрешено: параметр в прототипе функции
void f5(X) {} // НАРУШЕНИЕ: *определения* требуют полных типов
Таким образом, в C++ вы можете использовать неполные типы в объявлениях, однако при определении функций вы обязаны использовать полные типы (за исключением указателей и ссылок).
Я пишу это в качестве отдельного ответа, а не просто комментария, потому что не согласен с ответом Люка Турайля, не по причинам правомерности, а из соображений надежности программного обеспечения и риска неверной интерпретации.
В частности, у меня есть проблема с подразумеваемым контрактом относительно того, что вы ожидаете от пользователей вашего интерфейса, чтобы они знали.
Если вы возвращаете или принимаете ссылочные типы, то вы фактически говорите, что они могут передать указатель или ссылку, о которых они могут знать только через предварительное объявление.
Когда вы возвращаете неполный тип X f2();
, вы говорите, что ваш вызывающий должен иметь полное определение типа X. Это необходимо для того, чтобы создать объект слева от знака равенства или временный объект в месте вызова.
Аналогично, если вы принимаете неполный тип, вызывающий должен был создать объект, который является параметром. Даже если этот объект был возвращен как другой неполный тип из функции, в месте вызова необходимо полное объявление. Например:
class X; // предварительное объявление для двух законных деклараций
X returnsX();
void XAcceptor(X);
XAcceptor(returnsX()); // определение X должно быть известно здесь
Я считаю, что есть важный принцип: заголовочный файл должен предоставлять достаточно информации, чтобы его можно было использовать без зависимости от других заголовков. Это означает, что заголовок должен быть включен в единицу компиляции без возникновения ошибки компилятора, когда вы используете какие-либо функции, которые он объявляет.
За исключением
- Если эта внешняя зависимость является желательным поведением. Вместо использования условной компиляции, вы могли бы иметь хорошо документированное требование, чтобы пользователи предоставляли свой собственный заголовочный файл, объявляющий X. Это альтернатива использованию #ifdef и может быть полезным способом введения моков или других вариантов.
- Важно отметить некоторые шаблонные техники, где явно не ожидается, что вы будете их инстанцировать, упоминаю это, чтобы кто-то не стал со мной спорить.
В файл, где вы используете только указатели или ссылки на класс, и никакие члены или функции не должны вызываться через эти указатели или ссылки, вы можете воспользоваться предварительным объявлением класса с помощью class Foo;
.
Вы можете:
- Объявить члены данных типа
Foo*
илиFoo&
. - Объявить (но не определить) функции с параметрами и/или возвращаемыми значениями типа
Foo
. - Объявить статические члены данных типа
Foo
. Это возможно, потому что статические члены данных определяются вне определения класса.
Таким образом, предварительное объявление класса позволяет вам использовать его в различных контекстах, не вдаваясь в детали реализации.
Общее правило, которому я следую, заключается в том, чтобы не включать заголовочные файлы, если в этом нет необходимости. Таким образом, если я не сохраняю объект класса в качестве член- переменной своего класса, я не буду его подключать, а просто воспользуюсь предварительным объявлением.
Что такое Правило трёх?
Разница между const int*, const int * const и int * const?
Имеют ли круглые скобки после имени типа значение при использовании new?
`std::wstring` против `std::string`: когда использовать и в чем разница?
Что означает 'const' в конце объявления метода класса?