Я ищу определение того, когда мне разрешено выполнять форвардное объявление класса в файле заголовка другого класса:
Мне разрешено делать это для базового класса, для класса, содержащегося в качестве члена, для класса, переданного функции-члена по ссылке и т.д.?
Поставьте себя в позицию компилятора: когда вы пересылаете объявление типа, все компиляторы знают, что этот тип существует; он ничего не знает о его размере, членах или методах. Вот почему он называется неполным типом. Поэтому вы не можете использовать тип для объявления члена или базового класса, так как компилятору необходимо знать макет типа.
Предполагая следующее форвардное объявление.
class X;
Здесь вы можете и не можете делать.
Что вы можете сделать с неполным типом:
Объявить элемент как указатель или ссылку на неполный тип:
class Foo {
X *pt;
X &pt;
};
Объявлять функции или методы, которые принимают/возвращают неполные типы:
void f1(X);
X f2();
Определить функции или методы, которые принимают/возвращают указатели/ссылки на неполный тип (но не используют его элементы):
void f3(X*, X&) {}
X& f4() {}
X* f5() {}
Что вы не можете сделать с неполным типом:
Используйте его как базовый класс
class Foo : X {} // compiler error!
Используйте его, чтобы объявить участника:
class Foo {
X m; // compiler error!
};
Определить функции или методы с помощью этого типа
void f1(X x) {} // compiler error!
X f2() {} // compiler error!
Используйте его методы или поля, на самом деле пытается разыменовать переменную с неполным типом
class Foo {
X *m;
void method()
{
m->someMethod(); // compiler error!
int i = m->someField; // compiler error!
}
};
Когда дело доходит до шаблонов, абсолютного правила нет: можно ли использовать неполный тип в качестве параметра шаблона, зависит от способа использования типа в шаблоне.
Например, std::vector<T>
требует, чтобы его параметр был полным, а boost::container::vector<T>
- нет. Иногда полный тип требуется, только если вы используете определенные функции-члены; Например, это относится к std::unique_ptr<T>
.
Хорошо документированный шаблон должен указывать в своей документации все требования к его параметрам, в том числе, должны ли они быть полными типами или нет.
Главное правило состоит в том, что вы можете только forward-declare классы, чей макет памяти (и, следовательно, функции-члены и члены данных) не обязательно должны быть известны в файле, который вы пересылаете, объявите его.
Это исключает базовые классы и все, кроме классов, используемых с помощью ссылок и указателей.
Lakos различает использование класса
Я никогда не видел, чтобы это произносилось более кратко:)
Помимо указателей и ссылок на неполные типы, вы также можете объявить прототипы функций, которые указывают параметры и/или возвращаемые значения, которые являются неполными. Тем не менее, вы не можете определить функцию с неполным параметром или возвращаемым типом, если это не указатель или ссылка.
Примеры:
struct X; // Forward declaration of X
void f1(X* px) {} // Legal: can always use a pointer
void f2(X& x) {} // Legal: can always use a reference
X f3(int); // Legal: return value in function prototype
void f4(X); // Legal: parameter in function prototype
void f5(X) {} // ILLEGAL: *definitions* require complete types
Ни один из ответов до сих пор не описывает, когда можно использовать форвардное объявление шаблона класса. Итак, вот оно.
Шаблон класса может быть перенаправлен как:
template <typename> struct X;
Следуя структуре принятого ответа,
Здесь то, что вы можете и чего не можете сделать.
Что вы можете сделать с неполным типом:
Объявите элемент как указатель или ссылку на неполный тип в другом шаблоне класса:
template <typename T>
class Foo {
X<T>* ptr;
X<T>& ref;
};
Объявить элемент как указатель или ссылку на одно из своих неполных экземпляров:
class Foo {
X<int>* ptr;
X<int>& ref;
};
Объявлять шаблоны функций или шаблоны функций-членов, которые принимают/возвращают неполные типы:
template <typename T>
void f1(X<T>);
template <typename T>
X<T> f2();
Объявлять функции или функции-члены, которые принимают/возвращают одно из своих неполных экземпляров:
void f1(X<int>);
X<int> f2();
Определить шаблоны функций или шаблоны функций-членов, которые принимают/возвращают указатели/ссылки на неполный тип (но не используют его элементы):
template <typename T>
void f3(X<T>*, X<T>&) {}
template <typename T>
X<T>& f4(X<T>& in) { return in; }
template <typename T>
X<T>* f5(X<T>* in) { return in; }
Определите функции или методы, которые принимают/возвращают указатели/ссылки на одну из своих неполных экземпляров (но не используют ее элементы):
void f3(X<int>*, X<int>&) {}
X<int>& f4(X<int>& in) { return in; }
X<int>* f5(X<int>* in) { return in; }
Используйте его как базовый класс другого класса шаблонов
template <typename T>
class Foo : X<T> {} // OK as long as X is defined before
// Foo is instantiated.
Foo<int> a1; // Compiler error.
template <typename T> struct X {};
Foo<int> a2; // OK since X is now defined.
Используйте его для объявления члена другого шаблона класса:
template <typename T>
class Foo {
X<T> m; // OK as long as X is defined before
// Foo is instantiated.
};
Foo<int> a1; // Compiler error.
template <typename T> struct X {};
Foo<int> a2; // OK since X is now defined.
Определение шаблонов функций или методов с использованием этого типа
template <typename T>
void f1(X<T> x) {} // OK if X is defined before calling f1
template <typename T>
X<T> f2(){return X<T>(); } // OK if X is defined before calling f2
void test1()
{
f1(X<int>()); // Compiler error
f2<int>(); // Compiler error
}
template <typename T> struct X {};
void test2()
{
f1(X<int>()); // OK since X is defined now
f2<int>(); // OK since X is defined now
}
Что вы не можете сделать с неполным типом:
Используйте один из своих экземпляров как базовый класс
class Foo : X<int> {} // compiler error!
Используйте одно из своих экземпляров, чтобы объявить участника:
class Foo {
X<int> m; // compiler error!
};
Определять функции или методы с использованием одного из его экземпляров
void f1(X<int> x) {} // compiler error!
X<int> f2() {return X<int>(); } // compiler error!
Используйте методы или поля одного из его экземпляров, на самом деле пытается разыменовать переменную с неполным типом
class Foo {
X<int>* m;
void method()
{
m->someMethod(); // compiler error!
int i = m->someField; // compiler error!
}
};
Создание явных экземпляров шаблона класса
template struct X<int>;
X
и X<int>
абсолютно одинакова, и только декларируемый вперед синтаксис отличается любым существенным образом, причем все, кроме 1 строки вашего ответа, равносильны только принятию Люка и s/X/X<int>/g
? Это действительно нужно? Или я пропустил крошечную деталь, которая отличается? Это возможно, но я несколько раз визуально сравнил и ничего не вижу ...
В файле, в котором вы используете только указатель или ссылку на класс. И никакая функция-член/член не должна вызываться, думал, что это указатель/ссылка.
с class Foo;
//forward declare
Мы можем объявлять элементы данных типа Foo * или Foo &.
Мы можем объявлять (но не определять) функции с аргументами и/или возвращаемыми значениями типа Foo.
Мы можем объявлять статические элементы данных типа Foo. Это связано с тем, что статические члены данных определяются вне определения класса.
До тех пор, пока вам не нужно определение (мыслители и ссылки), вы можете уйти с передовыми объявлениями. Вот почему в большинстве случаев вы увидите их в заголовках, в то время как файлы реализации обычно вытягивают заголовок для соответствующих определений.
Я пишу это как отдельный ответ, а не просто комментарий, потому что я не согласен с Люком Торайлом, а не на основании законности, но для надежного программного обеспечения и опасности неправильного толкования.
В частности, у меня есть проблема с подразумеваемым контрактом того, что вы ожидаете от пользователей вашего интерфейса.
Если вы возвращаете или принимаете ссылочные типы, то вы просто говорите, что они могут пройти через указатель или ссылку, которую они могут в свою очередь знать только через декларацию вперед.
Когда вы возвращаете неполный тип X f2();
, вы говорите, что ваш вызывающий должен иметь полную спецификацию типа X. Они нуждаются в ней для создания LHS или временного объекта при вызове сайт.
Аналогично, если вы принимаете неполный тип, вызывающий должен построить объект, являющийся параметром. Даже если этот объект был возвращен как еще один неполный тип из функции, сайту вызова требуется полное объявление. то есть:.
class X; // forward for two legal declarations
X returnsX();
void XAcceptor(X);
XAcepptor( returnsX() ); // X declaration needs to be known here
Я думаю, что существует важный принцип, согласно которому заголовок должен предоставлять достаточную информацию для использования без зависимостей, требующих других заголовков. Это означает, что заголовок должен быть включен в блок компиляции, не вызывая ошибки компилятора при использовании любых функций, которые он объявляет.
За исключением
Если эта внешняя зависимость является желаемой. Вместо использования условной компиляции у вас может быть хорошо документированное требование для их подачи собственного заголовка, объявляющего X. Это альтернатива использованию #ifdefs и может быть полезным способом введения mocks или других вариантов.
Важным отличием является то, что некоторые шаблонные методы, в которых вы явно НЕ должны их создавать, упомянули просто так, что кто-то не смущает меня.
Общее правило, которое я придерживаюсь, - это не включать заголовочный файл, если только не нужно. Поэтому, если я не храню объект класса в качестве переменной-члена моего класса, я его не буду включать, я просто буду использовать декларацию forward.
Я просто хочу добавить одну важную вещь, которую вы можете сделать с переадресованным классом, не упомянутым в ответе Люка Торайля.
Что вы можете сделать с неполным типом:
Определить функции или методы, которые принимают/возвращают указатели/ссылки на неполный тип и пересылать указатели/ссылки к другой функции.
void f6(X*) {}
void f7(X&) {}
void f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }
Модуль может проходить через объект прямого объявленного класса другому модулю.
Возьмите его, чтобы форвардное объявление получило ваш код для компиляции (создается объект obj). Однако связывание (создание exe) не будет успешным, если определения не найдены.
class A; class B { A a; }; int main(){}
, и дайте мне знать, как это происходит. Конечно, это не скомпилируется. Все правильные ответы здесь объяснить , почему и точные, ограниченные условия , в которых предобъявление действительно. Вы вместо этого написали это о чем-то совершенно ином.
Обычно вы хотите использовать форвардное объявление в заголовочном файле классов, если хотите использовать другой тип (класс) в качестве члена класса. Вы не можете использовать методы, объявленные вперед, в заголовочном файле, потому что С++ пока не знает определения этого класса. Эта логика вам нужно переместить в .cpp файлы, но если вы используете шаблонные функции, вы должны уменьшить их только до той части, которая использует шаблон, и переместить эту функцию в заголовок.