我什么时候可以使用前向声明?

我正在寻找何时允许我在另一个类的头文件中进行类的前向声明的定义:

我是否允许为一个基类,一个类的成员,一个通过引用等方式传递给成员函数的类来做呢?

把自己置于编译器的位置:当你转发声明一个types时,所有的编译器知道这个types是存在的; 它对它的大小,成员或方法一无所知。 这就是为什么它被称为不完整的types 。 因此,不能使用该types来声明成员或基类,因为编译器需要知道该types的布局。

假设以下前向声明。

class X; 

这是你可以做什么,不可以做什么。

你可以用一个不完整的types来做什么:

  • 声明一个成员是一个指针或对不完整types的引用:

     class Foo { X *pt; X &pt; }; 
  • 声明接受/返回不完整types的函数或方法:

     void f1(X); X f2(); 
  • 定义接受/返回不完整types的指针/引用(但不使用其成员)的函数或方法:

     void f3(X*, X&) {} X& f4() {} X* f5() {} 

你不能用不完整的types做什么:

  • 用它作为基类

     class Foo : X {} // compiler error! 
  • 用它来声明一个成员:

     class Foo { X m; // compiler error! }; 
  • 定义使用这种types的函数或方法

     void f1(X x) {} // compiler error! X f2() {} // compiler error! 
  • 使用它的方法或字段,实际上是试图用一个不完整的types来解引用一个variables

     class Foo { X *m; void method() { m->someMethod(); // compiler error! int i = m->someField; // compiler error! } }; 

当涉及到模板时,没有绝对的规则:是否可以使用不完整的types作为模板参数取决于模板中使用的types的方式。

例如, std::vector<T>要求它的参数是一个完整的types,而boost::container::vector<T>则不需要。 有时,只有使用某些成员函数才需要完整types; 例如, std::unique_ptr<T>就是这种情况 。

一个有详细logging的模板应该在其文档中指出其参数的所有要求,包括它们是否需要是完整的types。

主要的规则是你只能前向声明那些在你前转的文件中不需要知道内存布局(因此成员函数和数据成员)的类。

这将排除基类和任何东西,但通过引用和指针使用的类。

Lakos区分课堂使用情况

  1. 只有名义上的 (前向声明就足够了)
  2. (需要类定义)。

我从来没有见过它更简洁:)

除了不完整types的指针和引用外,还可以声明指定参数和/或返回不完整types的值的函数原型。 但是,除非是指针或引用,否则不能定义具有不完整参数或返回types的函数。

例子:

 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 

在你只使用指针或引用类的文件中,并且不应该调用那些指针/引用的成员/成员函数。

class Foo; //前向声明

我们可以声明types为Foo *或Foo&的数据成员。

我们可以声明(但不定义)带有参数的函数和/或返回Footypes的值。

我们可以声明Footypes的静态数据成员。 这是因为静态数据成员是在类定义之外定义的。

迄今为止的答案都没有描述何时可以使用类模板的前向声明。 所以,在这里呢。

类模板可以被转发声明为:

 template <typename> struct X; 

按照最被接受的答案的结构,

这是你可以做什么,不可以做什么。

你可以用一个不完整的types来做什么:

  • 声明成员是另一个类模板中的指针或对不完整types的引用:

     template <typename T> class Foo { X<T> *ptr; X<T> &ref; }; 
  • 声明一个成员是一个指针或对其中一个不完整实例的引用:

     class Foo { X<int> *ptr; X<int> &ref; }; 
  • 声明接受/返回不完整types的函数模板或成员函数模板:

     template <typename T> void f1(X<T>); template <typename T> X<T> f2(); 
  • 声明接受/返回其不完整实例之一的函数或成员函数:

     void f1(X<int>); X<int> f2(); 
  • 定义接受/返回不完整types(但不使用其成员)的指针/引用的函数模板或成员函数模板:

     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. 
  • 定义使用此types的函数模板或方法

     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 } 

你不能用不完整的types做什么:

  • 使用其实例之一作为基类

     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! 
  • 使用其实例之一的方法或字段,实际上是试图用一个不完整的types解引用一个variables

     class Foo { X<int> *m; void method() { m->someMethod(); // compiler error! int i = m->someField; // compiler error! } }; 
  • 创build类模板的显式实例

     template struct X<int>; 

只要你不需要定义(思考指针和引用),你可以摆脱前向声明。 这就是为什么大多数情况下,你会在头文件中看到它们,而实现文件通常会为合适的定义提取头文件。

我遵循的一般规则是不包括任何头文件,除非必须。 所以,除非我将类的对象作为我的类的成员variables存储,否则我将不使用前面的声明。

我写这是一个单独的答案,而不仅仅是一个评论,因为我不同意Luc Touraille的回答,不是基于合法性,而是强大的软件和误解的危险。

具体来说,我希望您的界面用户必须知道隐含的合同。

如果你正在返回或接受引用types,那么你只是说他们可以通过一个指针或引用,他们可能反过来只知道通过前向声明。

当你返回一个不完整的typesX f2(); 那么你就是说你的调用者必须具有X的完整types规范。他们需要它来在调用站点创buildLHS或临时对象。

同样,如果你接受一个不完整的types,调用者必须构造参数的对象。 即使该对象作为函数的另一个不完整types返回,调用站点也需要完整的声明。 即:

 class X; // forward for two legal declarations X returnsX(); void XAcceptor(X); XAcepptor( returnsX() ); // X declaration needs to be known here 

我认为这是一个重要的原则,即头文件应该提供足够的信息来使用它,而不需要其他头文件的依赖。 这意味着头文件应该能够被包含在编译单元中,而不会在使用它声明的任何函数时导致编译器错误。

  1. 如果这个外部依赖是所需的行为。 而不是使用条件编译,你可以有一个很好的文档要求提供他们自己的头声明X.这是一个替代使用#ifdefs,可以是一个有用的方式来介绍嘲笑或其他变种。

  2. 重要的区别是一些模板技术,你明确不希望实例化它们,只是提到有人不会跟我一起暴躁。

当你想使用其他types(类)作为类的成员时,你通常会在类头文件中使用前向声明。 你不能在头文件中使用forward-declared类的方法 ,因为C ++在这个时候还不知道那个类的定义。 这是逻辑,你必须进入.cpp文件,但如果你正在使用模板函数,你应该减less他们只使用该模板的部分,并将该函数的头部。

认为前向声明会让你的代码编译(obj被创build)。 然而,链接(exe创build)将不会成功,除非find定义。

我只是想添加一个重要的事情,你可以做一个没有在吕克Touraille答案中提到的转发类。

你可以用一个不完整的types来做什么:

定义接受/返回指向不完整types的指针/引用的函数或方法,并将指针/引用转发给另一个函数。

 void f6(X*) {} void f7(X&) {} void f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); } 

一个模块可以将一个前向声明类的对象传递给另一个模块。