在C++中,是否可以提前声明一个继承另一个类的类?

90

我知道我可以这样做:

class Foo;

但我能否将一个类声明为继承自另一个类,例如:

class Bar {};

class Foo: public Bar;

一个例子用例是协变引用返回类型。

// somewhere.h
class RA {}
class RB : public RA {}

...然后在另一个不包含somewhere.h的标头文件中。

// other.h
class RA;

class A {
 public:
  virtual RA* Foo();  // this only needs the forward deceleration
}

class RB : public RA; // invalid but...

class B {
 public:
  virtual RB* Foo();  // 
}
编译器在处理声明 RB* B:Foo() 的时候,唯一需要的信息就是 RB 有一个公共基类 RA。如果你打算对从 Foo 返回的值进行任何引用操作,那么你会需要 somewhere.h。但是,如果有些客户端代码不会调用 Foo,那么它们就没必要包含 somewhere.h,这样可以显著提高编译速度。

2
@Pace 协变引用返回类型。(请参见,编辑。)但我不确定多重继承是否会破坏它。 - BCS
可能是不允许指向不完整类类型的指针的重复问题。 - Archmede
5个回答

53

前置声明仅对编译器有用,告诉它该名称的类确实存在,并且将在其他地方进行声明和定义。你不能在编译器需要有关类的上下文信息的任何情况下使用它,也不能只向编译器提供关于类的一点点信息。(通常情况下,你只能在没有其他上下文的情况下引用该类时使用前置声明,例如作为参数或返回值。)

因此,在使用它来帮助声明Foo的任何情况下,都不能预先声明Bar,而且包括基类的前置声明根本毫无意义--除了什么都不告诉你之外,还能说明什么呢?


107
“除了什么都没有告诉你?”,这句话告诉你类型是基类的子类,这并不是毫无意义的。例如,如果在基类函数中遇到一个包含子类的对象(作为“其他”对象),它将无法将该对象传递给需要基类对象的函数,因为它不知道如何将子类转换为基类,即使它可以访问子类的所有公共成员。我同意您的观点,认为这个人所要求的是不可能实现的,但我不同意您的评估,即实现它是毫无用处的。 - codetaku
@codetaku:登录以点赞您的评论(: 对于任何寻求具体示例的人,请参阅Google的C++风格指南(在“Cons”部分):https://google.github.io/styleguide/cppguide.html#Forward_Declarations - jwd
2
@codetaku - 是的!例如,您想在智能指针上使用static_assertenable_if来限制它只能用于某些类型 - 您希望在不完整类型上使用智能指针...轻松搞定! - davidbak
1
@jwd - 我不同意谷歌的编码风格。信息隐藏是有用的,至少自从Parnas描述它以来我们就知道了。如果我们有Ada的包体或limited private-但我们没有。因此,在C++中,对于pimpl惯用语,使用指向不完整类型的unique_ptr是我们能做到的最好的选择。 - davidbak

42

前向声明是声明,而不是定义。因此,需要类的声明(如指向该类的指针)只需要前向声明即可。但是,任何需要定义的内容 - 即需要知道类的实际结构的内容 - 只有前向声明将无法正常工作。

派生类肯定需要知道其父类的结构,而不仅仅是父类存在的信息,因此前向声明是不够的。


4
处理一个类的指针时,您需要了解该类的结构吗?只要您专门使用引用,就应该没问题。 - BCS
2
@BCS 是的。只要你在处理指针,你只需要前向声明,但是当你声明一个派生类时,你需要父类的定义,而不仅仅是它的声明,因为你不仅仅在处理指针。 - Jonathan M Davis
5
这会限制对象的实现,但我认为在只了解继承结构的情况下进行*D -> *B转换是可能的。 我预计它看起来会像实现虚拟基类所需的结构。 - BCS

25
不,即使你只使用指针,也无法前向声明继承。在处理指针之间的转换时,有时编译器必须了解类的细节才能正确地进行转换,多重继承就是这种情况。(您可以为仅使用单一继承的层次结构中的某些部分提供特殊情况,但这不是语言的一部分。)
考虑以下简单情况:
#include <stdio.h>
class A { int x; };
class B { int y; };
class C: public A, public B { int z; };
void main()
{ 
    C c; A *pa = &c; B *pb = &c; C *pc = &c; 
    printf("A: %p, B: %p, C: %p\n", pa, pb, pc);
}

我收到的输出(使用32位Visual Studio 2010)如下:

A: 0018F748, B: 0018F74C, C: 0018F748

对于多重继承而言,当转换相关指针时,编译器必须插入一些指针算术运算以正确进行转换。

这就是为什么即使你只处理指针,也不能前置声明继承关系的原因。

至于为什么它有用,这将在您想要使用协变返回类型而不是使用转换时提高编译时间。例如,以下代码将无法编译:

class RA;
class A             { public: virtual RA *fooRet(); };
class RB;
class B : public A  { public: virtual RB *fooRet(); };

但是这样会:

class RA;
class A             { public: virtual RA *fooRet(); };
class RA { int x; };
class RB : public RA{ int y; };
class B : public A  { public: virtual RB *fooRet(); };

当你有类型为B的对象(而不是指针或引用)时,这很有用。在这种情况下,编译器足够聪明以使用直接函数调用,而且你可以直接使用RB *的返回类型而无需转换。在这种情况下,通常我会将返回类型设置为RA *并对返回值进行静态转换。


9
我知道这是一个旧答案,但在我看来,这比那些得到更高票数和被接受的答案更有帮助,因为它没有忽略即使在不完整类型的情况下将 Foo* 转换为 Bar*(或引用)也可能是一件有用的事情。 - EvanED

2
你只需要声明 RB 而不带 : public RA (哦,还需要在类定义的末尾添加 ; )即可:
class RA;

class A {
    public:
    virtual RA* Foo();
};

class RB;

class B {
public:
    virtual RB* Foo();
};

// client includes somewhere.h
class RA {};
class RB : public RA {};

int main ()
{
    return 0;
}

然而,这并不能解决用户user1332054在答案中描述的具体问题。
其他一些答案似乎表现出了一些我想要澄清的误解:
即使我们知道定义不可能被包含,前向声明仍然是有用的。这使我们能够在库中进行许多类型推导,使其与许多其他已建立的库兼容,而不必包含它们。过多地包含库会导致太多嵌套的包含,从而可能导致编译时间爆炸。适当时,将代码设置为兼容性好,并尽可能少地包含是一个好的实践。
通常情况下,你可以定义一个类,其中包含指向仅已声明但未定义的类的指针。例如:
struct B;

struct A
{
    B * b_;

    B * foo ()
    {
        return b_;
    }

    B & foo (B * b)
    {
        return *b;
    }
};

int main ()
{
    return 0;
}

上述内容编译良好,因为编译器不需要了解B的任何信息。
下面是一个例子,可能比较难以意识到编译器需要更多的信息:
struct B;

struct A
{
    B * foo ()
    {
        return new B;
    }
};

上述问题是因为new B调用了尚未定义的B::B()构造函数。此外:
struct B;

struct A
{
    void foo (B b) {}
};

在这里,foo必须调用b的拷贝构造函数,但是该函数还没有被定义。最后:

struct B;

struct A
{
    B b;
};

在这里,我们使用默认构造函数隐式定义了 A,该构造函数调用其成员b的默认构造函数,但后者尚未被定义。我认为你明白了。

因此,就用户1332054所描述的更一般的问题而言,我实在不明白为什么不能在继承的虚函数中使用指向未定义类的指针。

然而更广泛地说,我认为通过定义而非只声明类来使其更加困难。以下是一个示例,在其中您可以在定义任何类之前就使用库中的类来执行DoCleverStuff:

// Just declare

class RA;
class RB;

class A;
class B;

// We'll need some type_traits, so we'll define them:

template <class T>
struct is_type_A
{
    static constexpr bool value = false;
};

template <>
struct is_type_A <A>
{
    static constexpr bool value = true;
};

template <class T>
struct is_type_B
{
    static constexpr bool value = false;
};

template <>
struct is_type_B <B>
{
    static constexpr bool value = true;
};

#include <type_traits>

// With forward declarations, templates and type_traits now we don't
// need the class definitions to prepare useful code:

template<class T>
typename std::enable_if<is_type_A<T>::value, RA *>::type
DoCleverStuff (T & t)
{
    // specific to A

    return t.fooRet();
}

template<class T>
typename std::enable_if<is_type_B<T>::value, RB *>::type
DoCleverStuff (T & t)
{
    // specific to B

    return t.fooRet();
}

// At some point the user *does* the include:

class RA
{
    int x;
};

class RB : public RA
{
    int y;
};

class A
{
public:
    virtual RA * fooRet()
    {
        return new RA;
    }
};

class B : public A
{
public:

    virtual RB * fooRet()
    {
        return new RB;
    }
};

int main ()
{
    // example calls:

    A a;

    RA * ra = DoCleverStuff(a);

    B b;

    RB * rb = DoCleverStuff(b);

    delete ra;
    delete rb;

    return 0;
}

-1

我认为这并不有用。考虑一下:你已经定义了一个类,Bar:

class Bar {
public:
    void frob();
};

现在你要声明一个名为Foo的类:
class Foo;

你所能做的就是构造一个指向Foo的指针。现在,假设你添加了这样的信息:Foo派生自Bar:
class Foo: public Bar;

现在你能做什么以前不能做的事情?我认为你所能做的就是接受一个指向Foo的指针并将其转换为指向Bar的指针,然后使用该指针。

void frob(Foo* f) {
    Bar *b = (Bar)f;
    b->frob();
}

然而,你必须在其他地方生成指针,这样你就可以只接受指向 Bar 的指针。

void frob(Bar* b) {
    b->frob();
}

4
Foo类:公有继承自Bar,这意味着dynamic_cast<>可以使用,不是吗?我认为这是一个重要的信息。 - Fabian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接