我正在寻找何时允许在另一个类的头文件中对一个类进行前向声明的定义:
我是否可以针对基类,作为成员持有的类,通过引用传递给成员函数的类等进行此操作?
我正在寻找何时允许在另一个类的头文件中对一个类进行前向声明的定义:
我是否可以针对基类,作为成员持有的类,通过引用传递给成员函数的类等进行此操作?
Answers:
让自己处于编译器的位置:当您向前声明一个类型时,编译器只知道该类型存在;它对其大小,成员或方法一无所知。这就是为什么它被称为不完整类型的原因。因此,您不能使用该类型声明成员或基类,因为编译器将需要知道该类型的布局。
假设以下向前声明。
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>
。
记录良好的模板应在其文档中指出其参数的所有要求,包括是否需要完整的类型。
主要规则是,您只能向前声明其内存布局(以及成员函数和数据成员)不需要在您对其进行前声明的文件中知道的类。
这将排除基类以及除通过引用和指针使用的类以外的任何类。
除了对不完整类型的指针和引用,您还可以声明函数原型,这些函数原型指定不完整类型的参数和/或返回值。但是,您不能定义一个参数或返回类型不完整的函数,除非它是指针或引用。
例子:
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
到目前为止,没有一个答案描述何时可以使用类模板的前向声明。所以,就到这里。
可以将类模板转发声明为:
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>;
X
和X<int>
是完全一样的,只有在任何实质性的方式向前声明语法不同,所有但1线你的答案额达的只是采取吕克的和s/X/X<int>/g
?真的需要吗?还是我错过了一个与众不同的微小细节?有可能,但我在视觉上进行了几次比较,看不到任何东西……
我将其作为一个单独的答案而不是仅仅作为评论,是因为我不同意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
我认为有一个重要的原则,即标头应提供足够的信息以使用它,而不必依赖其他标头。这意味着在使用标头声明的任何函数时,标头应该能够包含在编译单元中,而不会引起编译器错误。
除了
如果外部的依赖是期望的行为。与其使用条件编译,不如使用有据可查的要求它们提供自己的标头声明X。这是使用#ifdefs的替代方法,并且是引入模拟或其他变体的有用方法。
重要的区别是某些模板技术,其中明确不要求您实例化它们,只是提到了某人不会对我说鬼话。
I disagree with Luc Touraille's answer
因此,请给他写评论,如果需要的话,请提供指向博客文章的链接。这不能回答所问的问题。如果每个人都对X的工作方式提出疑问,那么合理的答案与X的不同或在限制我们应限制使用X的范围内进行辩论的话,我们几乎没有真正的答案。
我遵循的一般规则是除非必要,否则不要包含任何头文件。因此,除非我将类的对象存储为类的成员变量,否则我不会将其包括在内,我只会使用前向声明。
当您要使用其他类型(类)作为类的成员时,通常将需要在类头文件中使用前向声明。您不能在头文件中使用前向声明的类方法,因为C ++当时还不知道该类的定义。这是您必须移入.cpp文件的逻辑,但是如果您使用的是模板功能,则应将其缩减为仅使用模板的部分,然后将该功能移至标题中。
认为向前声明将使您的代码得以编译(创建了obj)。但是,除非找到定义,否则链接(exe创建)将不会成功。
class A; class B { A a; }; int main(){}
,让我知道它的运行方式。当然不会编译。所有正确的答案,在这里解释和精确的,限于环境中向前声明是有效的。相反,您已经写了一些完全不同的东西。
我只想添加一个重要的事情,您可以使用Luc Touraille的答案中未提到的转发类来做。
使用不完整的类型可以做什么:
定义接受/返回不完整类型的指针/引用并将该指针/引用转发给另一个函数的函数或方法。
void f6(X*) {}
void f7(X&) {}
void f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }
一个模块可以通过一个向前声明的类的对象传递给另一个模块。