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


602

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

我是否可以针对基类,作为成员持有的类,通过引用传递给成员函数的类等进行此操作?


14
我迫切希望将此名称重命名为“ 我应该在何时”,并适当地更新答案……
deworde 2015年

12
@deworde当您说何时“应该”时,您是在征求意见。
AturSams

@deworde是我的理解,您希望尽可能使用前向声明,以缩短构建时间并避免循环引用。我能想到的唯一例外是,当包含文件包含typedef时,在这种情况下,需要重新定义typedef(并冒着更改风险)与包括整个文件(及其递归include)之间的权衡。
Ohad Schneider

@OhadSchneider从实际的角度来看,我不是我的标头的狂热爱好者。÷
deworde

基本上总是要求您包括一个不同的标头才能使用它们(在这里构造函数参数的decl是大罪魁祸首)
deworde

Answers:


962

让自己处于编译器的位置:当您向前声明一个类型时,编译器只知道该类型存在;它对其大小,成员或方法一无所知。这就是为什么它被称为不完整类型的原因。因此,您不能使用该类型声明成员或基类,因为编译器将需要知道该类型的布局。

假设以下向前声明。

class X;

这是您可以做的和不能做的。

使用不完整的类型可以做什么:

  • 声明一个成员为不完整类型的指针或引用:

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

    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>

记录良好的模板应在其文档中指出其参数的所有要求,包括是否需要完整的类型。


4
很好的答案,但请在下面查看我不同意的工程观点。简而言之,如果您不包括接受或返回的不完整类型的标头,则您必须对标头的使用者施加不可见的依赖关系,而后者必须知道它们需要哪些其他标头。
安迪·邓特

2
@AndyDent:是的,但是标头的使用者只需要包含他实际使用的依赖项,因此这遵循C ++的原则:“您只为使用的内容付费”。但是的确,对于希望标头是独立的用户来说可能不方便。
Luc Touraille

8
这套规则忽略了一个非常重要的情况:您需要一个完整的类型来实例化标准库中的大多数模板。需要特别注意这一点,因为违反规则会导致未定义的行为,并且可能不会导致编译器错误。
James Kanze

12
+1表示“将自己置于编译器的位置”。我想象“正在编译的人”留着胡子。
PascalVKooten 2013年

3
@JesusChrist:确实:当您按值传递对象时,编译器需要知道其大小才能进行适当的堆栈操作;当传递指针或引用时,编译器不需要对象的大小或布局,而只需要地址的大小(即指针的大小),而不依赖于所指向的类型。
Luc Touraille 2014年

45

主要规则是,您只能向前声明其内存布局(以及成员函数和数据成员)不需要在您对其进行前声明的文件中知道的类。

这将排除基类以及除通过引用和指针使用的类以外的任何类。


6
几乎。您也可以在函数原型中将“普通”(即,非指针/引用)不完整类型称为参数或返回类型。
j_random_hacker

我想用作头文件中定义的类的成员的类呢?我可以转发声明吗?
Igor Oks 09年

1
是的,但是在那种情况下,您只能使用对向前声明的类的引用或指针。但是,它确实允许您有成员。
Reunanen


28

除了对不完整类型的指针和引用,您还可以声明函数原型,这些函数原型指定不完整​​类型的参数和/或返回值。但是,您不能定义一个参数或返回类型不完整的函数,除非它是指针或引用。

例子:

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

19

到目前为止,没有一个答案描述何时可以使用类模板的前向声明。所以,就到这里。

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

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>;

2
“到目前为止,没有答案描述何时可以向前声明类模板。” 是不是仅仅因为语义XX<int>是完全一样的,只有在任何实质性的方式向前声明语法不同,所有但1线你的答案额达的只是采取吕克的和s/X/X<int>/g?真的需要吗?还是我错过了一个与众不同的微小细节?有可能,但我在视觉上进行了几次比较,看不到任何东西……
underscore_d

谢谢!该编辑添加了大量有价值的信息。我将不得不阅读几次以完全理解它……或者可能使用通常更好的策略,直到我对真实代码感到非常困惑,然后回到这里!我怀疑我将能够使用它来减少各个地方的依赖性。
underscore_d

4

在只使用Pointer或对类的引用的文件中,并且不应以那些Pointer /引用调用任何成员/成员函数。

class Foo;// forward声明

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

我们可以使用Foo类型的参数和/或返回值声明(但不能定义)函数。

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


4

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

具体来说,我对您期望界面用户必须知道的隐含合同有疑问。

如果您要返回或接受引用类型,那么您只是在说它们可以通过指针或引用传递,而它们可能仅通过前向声明才知道。

当您返回不完整的类型时,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

我认为有一个重要的原则,即标头应提供足够的信息以使用它,而不必依赖其他标头。这意味着在使用标头声明的任何函数时,标头应该能够包含在编译单元中,而不会引起编译器错误。

除了

  1. 如果外部的依赖是期望的行为。与其使用条件编译,不如使用有据可查的要求它们提供自己的标头声明X。这是使用#ifdefs的替代方法,并且是引入模拟或其他变体的有用方法。

  2. 重要的区别是某些模板技术,其中明确不要求您实例化它们,只是提到了某人不会对我说鬼话。


“我认为有一个重要原则,标头应提供足够的信息以使用它,而不必依赖其他标头。” -阿德里安·麦卡锡(Adrian McCarthy)对纳文的回答发表评论时提到了另一个问题。这提供了一个合理的理由,即使对于当前未使用模板的类型,也不遵循您的“应提供足够的信息以使用”原则。
Tony Delroy 2014年

3
您正在谈论什么时候应该(或者不应该)使用前向声明。但是,这完全不是这个问题的重点。这是关于(例如)想打破循环依赖问题时的技术可能性。
JonnyJD 2014年

1
I disagree with Luc Touraille's answer因此,请给他写评论,如果需要的话,请提供指向博客文章的链接。这不能回答所问的问题。如果每个人都对X的工作方式提出疑问,那么合理的答案与X的不同或在限制我们应限制使用X的范围内进行辩论的话,我们几乎没有真正的答案。
underscore_d

3

我遵循的一般规则是除非必要,否则不要包含任何头文件。因此,除非我将类的对象存储为类的成员变量,否则我不会将其包括在内,我只会使用前向声明。


2
这会破坏封装并使代码变脆。为此,您需要知道类型是typedef还是具有默认模板参数的类模板的类,并且如果实现发生更改,则需要在使用前向声明的任何位置进行更新。
阿德里安·麦卡锡

@AdrianMcCarthy是正确的,一种合理的解决方案是在其声明其内容的标头中包含一个正向声明标头,该标头也应由拥有该标头的任何人拥有/维护/运送。例如:iosfwd标准库标头,其中包含iostream内容的前向声明。
Tony Delroy 2014年

3

只要不需要定义(例如指针和引用),就可以避免使用前向声明。这就是为什么大多数情况下您会在标头中看到它们的原因,而实现文件通常会提取相应定义的标头。


0

当您要使用其他类型(类)作为类的成员时,通常将需要在类头文件中使用前向声明。您不能在头文件中使用前向声明的类方法,因为C ++当时还不知道该类的定义。这是您必须移入.cpp文件的逻辑,但是如果您使用的是模板功能,则应将其缩减为仅使用模板的部分,然后将该功能移至标题中。


这是没有道理的。不能有不完整类型的成员。任何类的声明都必须提供所有用户需要了解的有关其大小和布局的所有信息。它的大小包括其所有非静态成员的大小。向前声明成员将使用户不​​知道其大小。
underscore_d

0

认为向前声明将使您的代码得以编译(创建了obj)。但是,除非找到定义,否则链接(exe创建)将不会成功。


2
为什么2个人对此表示支持?您不是在说这个问题。您的意思是正常的-不是函数的前向声明。问题是关于类的前向声明。正如您所说的“向前声明将使您的代码可以进行编译”,请帮我一个忙:compile class A; class B { A a; }; int main(){},让我知道它的运行方式。当然不会编译。所有正确的答案,在这里解释和精确的,限于环境中向前声明有效的。相反,您已经写了一些完全不同的东西。
underscore_d

0

我只想添加一个重要的事情,您可以使用Luc Touraille的答案中未提到的转发类来做。

使用不完整的类型可以做什么:

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

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

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


“转发的类”和“声明的类”可能被误认为是两个非常不同的事物。您所写的内容直接来自Luc答案中隐含的概念,因此尽管添加了明确的说明会引起很好的评论,但我不确定它是否可以作为答案的依据。
underscore_d

0

因此,Luc Touraille已经很好地解释了在何处使用该类,而不使用该类的前向声明。

我要补充一点,为什么我们需要使用它。

我们应该尽可能使用Forward声明以避免不必要的依赖注入。

由于将#include头文件添加到多个文件中,因此,如果我们将头文件添加到另一个头文件中,则会在源代码的各个部分中添加不必要的依赖项注入,可以通过在可能的情况下将#include.cpp文件添加到文件中而不是添加到另一个头文件中来避免这种情况。在头.h文件中尽可能使用类转发声明。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.