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

时间:2009-02-16 15:31:06

标签: c++ forward-declaration c++-faq

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

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

13 个答案:

答案 0 :(得分:906)

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

假设以下前瞻声明。

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>则不需要。{{1}}。有时,只有在使用某些成员函数时才需要完整类型;例如this is the case for std::unique_ptr<T>

文档齐全的模板应在其文档中指出其参数的所有要求,包括它们是否需要完整类型。

答案 1 :(得分:43)

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

这将排除基类和除引用和指针使用的类之外​​的任何东西。

答案 2 :(得分:32)

Lakos区分了课程用法

  1. 仅限名称(前向声明就足够了)和
  2. in-size (需要类定义)。
  3. 我从未见过它更简洁明了:)

答案 3 :(得分:27)

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

示例:

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

答案 4 :(得分:14)

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

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

template <typename> struct X;

遵循accepted answer的结构,

这是你能做什么,不能做什么。

使用不完整类型可以执行的操作:

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

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

答案 5 :(得分:5)

在只使用Pointer或引用类的文件中。如果指针/引用,则不应调用任何成员/成员函数。

class Foo; //前向声明

我们可以声明类型为Foo *或Foo&amp;。

的数据成员

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

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

答案 6 :(得分: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. 一些重要的区别是一些模板技术,你明确地不希望它们实例化它们,所以提到的只是有人不会对我嗤之以鼻。

答案 7 :(得分:3)

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

答案 8 :(得分:3)

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

答案 9 :(得分:0)

如果要将其他类型(类)用作类的成员,通常需要在类头文件中使用前向声明。您不能在头文件中使用前向声明的类 methods ,因为C ++当时还不知道该类的定义。这是你必须进入.cpp文件的逻辑,但如果你正在使用模板函数,你应该将它们减少到只使用模板的部分并将该函数移动到标题中。

答案 10 :(得分:0)

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

答案 11 :(得分:0)

我只想添加一个重要的事情,你可以用Luc Touraille的答案中没有提到的转发课程。

使用不完整类型可以执行的操作:

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

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

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

答案 12 :(得分:0)

因此,Luc Touraille已经很好地解释了在哪里使用该类以及不使用该类的前向声明。

我只补充为什么我们需要使用它。

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

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