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

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

我是否可以为基类,为作为成员持有的类,为通过引用传递给成员函数的类,等等这样做?

287743 次浏览

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

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

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

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

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

在文件中,只使用指向类的指针或引用。指针/引用不能调用任何成员/成员函数。

使用class Foo;//向前声明

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

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

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

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

例子:

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

以下是你能做的和不能做的。

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

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

    class Foo {
    X *p;
    X &r;
    };
    
  • Declare functions or methods which accept/return incomplete types:

    void f1(X);
    X    f2();
    
  • Define functions or methods which accept/return pointers/references to the incomplete type (but without using its members):

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

What you cannot do with an incomplete type:

  • Use it as a base class

    class Foo : X {} // compiler error!
    
  • Use it to declare a member:

    class Foo {
    X m; // compiler error!
    };
    
  • Define functions or methods using this type

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Use its methods or fields, in fact trying to dereference a variable with incomplete type

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

When it comes to templates, there is no absolute rule: whether you can use an incomplete type as a template parameter is dependent on the way the type is used in the template.

For instance, std::vector<T> requires its parameter to be a complete type, while boost::container::vector<T> does not. Sometimes, a complete type is required only if you use certain member functions; this is the case for std::unique_ptr<T>, for example.

A well-documented template should indicate in its documentation all the requirements of its parameters, including whether they need to be complete types or not.

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

Lakos区分类的使用

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

我从来没见过比这更简洁的发音:)

我写这篇文章是作为一个单独的回答,而不仅仅是评论,因为我不同意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. 如果这个外部依赖是desired行为。不使用条件编译,你可以有一个良好记录的要求,要求它们提供自己的声明x的头文件。这是使用#ifdefs的另一种选择,可以是引入模拟或其他变体的有用方法。

  2. 重要的区别是一些模板技术,你不需要明确地实例化它们,提到这些只是为了避免有人对我刻薄。

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

类模板可以被声明为:

template <typename> struct X;

遵循接受的答案的结构,

以下是你能做的和不能做的。

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

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

    template <typename T>
    class Foo {
    X<T>* ptr;
    X<T>& ref;
    };
    
  • Declare a member to be a pointer or a reference to one of its incomplete instantiations:

    class Foo {
    X<int>* ptr;
    X<int>& ref;
    };
    
  • Declare function templates or member function templates which accept/return incomplete types:

    template <typename T>
    void      f1(X<T>);
    template <typename T>
    X<T>    f2();
    
  • Declare functions or member functions which accept/return one of its incomplete instantiations:

    void      f1(X<int>);
    X<int>    f2();
    
  • Define function templates or member function templates which accept/return pointers/references to the incomplete type (but without using its members):

    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; }
    
  • Define functions or methods which accept/return pointers/references to one of its incomplete instantiations (but without using its members):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
    
  • Use it as a base class of another template class

    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.
    
  • Use it to declare a member of another class template:

    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.
    
  • Define function templates or methods using this type

    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
    }
    

What you cannot do with an incomplete type:

  • Use one of its instantiations as a base class

    class Foo : X<int> {} // compiler error!
    
  • Use one of its instantiations to declare a member:

    class Foo {
    X<int> m; // compiler error!
    };
    
  • Define functions or methods using one of its instantiations

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
    
  • Use the methods or fields of one of its instantiations, in fact trying to dereference a variable with incomplete type

    class Foo {
    X<int>* m;
    void method()
    {
    m->someMethod();      // compiler error!
    int i = m->someField; // compiler error!
    }
    };
    
  • Create explicit instantiations of the class template

    template struct X<int>;
    

我只是想补充一件重要的事情,你可以用Luc Touraille的回答中没有提到的转发类来做。

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

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

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

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

正如Luc Touraille已经很好地解释了在哪里使用和不使用类的前向声明。

我再补充一下为什么我们需要用它。

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

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