我刚刚接受了采访。有人问我什么是“前向声明”。然后我被问到它们是否是与提前声明相关的危险。
我无法回答第二个问题。在网上搜索并没有显示任何有趣的结果。
那么,有人知道与使用前向声明相关的任何危险吗?
我刚刚接受了采访。有人问我什么是“前向声明”。然后我被问到它们是否是与提前声明相关的危险。
我无法回答第二个问题。在网上搜索并没有显示任何有趣的结果。
那么,有人知道与使用前向声明相关的任何危险吗?
好吧,除了关于重复的问题......
...标准中至少有一个痛点。
如果你调用delete
一个指向不完整类型的指针,你会得到未定义的行为。在实践中,析构函数可能不会被调用。
我们可以使用以下命令和示例在LiveWorkSpace上看到:
// -std=c++11 -Wall -W -pedantic -O2
#include <iostream>
struct ForwardDeclared;
void throw_away(ForwardDeclared* fd) {
delete fd;
}
struct ForwardDeclared {
~ForwardDeclared() {
std::cout << "Hello, World!\n";
}
};
int main() {
ForwardDeclared* fd = new ForwardDeclared();
throw_away(fd);
}
诊断:
Compilation finished with warnings:
source.cpp: In function 'void throw_away(ForwardDeclared*)':
source.cpp:6:11: warning: possible problem detected in invocation of delete operator: [enabled by default]
source.cpp:5:6: warning: 'fd' has incomplete type [enabled by default]
source.cpp:3:8: warning: forward declaration of 'struct ForwardDeclared' [enabled by default]
source.cpp:6:11: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined
难道你不想感谢你的编译器警告你;)?
我想说任何危险都被收益所掩盖。不过有一些,主要与重构有关。
namespace
到另一个类,再加上using
指令,可能会造成严重破坏(神秘的错误,难以发现和修复)——当然,using
指令一开始是不好的,但没有代码是完美的,对吧?*考虑
template<class X = int> class Y;
int main()
{
Y<> * y;
}
//actual definition of the template
class Z
{
};
template<class X = Z> //vers 1.1, changed the default from int to Z
class Y
{};
之后该类Z
被更改为默认模板参数,但原来的前向声明仍然是int
.
定义:
//3rd party code
namespace A
{
struct X {};
}
和前向声明:
//my code
namespace A { struct X; }
//3rd party code
namespace B
{
struct X {};
}
namespace A
{
using ::B::X;
}
这显然使我的代码无效,但错误不在实际位置,并且修复至少可以说是可疑的。
前向声明是C++ 缺少模块(将在 C++17 中修复?)和使用头文件包含的症状,如果 C++ 有模块,则根本不需要前向声明。
前向声明不亚于“合同”,通过使用它,您实际上承诺您将提供某些东西的实现(在同一源文件中之后,或通过稍后链接二进制文件)。
这样做的缺点是你实际上必须遵守你的合同,这不是什么大问题,因为如果你不遵守你的合同,编译器会以某种方式提前抱怨,但在某些语言中,代码只是被执行而不需要“承诺它自己的存在”(谈到动态类型语言)
如果将指向不完整类类型的指针传递给delete
,则operator delete
可能会忽略重载。
这就是我所得到的......而且要被咬,你必须这样做,但源文件中没有其他会导致“不完整类型”编译器错误的东西。
编辑:在其他人的带领下,我想说困难(可能被认为是危险)是确保前向声明实际上与真实声明相匹配。对于函数和模板,参数列表必须保持同步。
并且您需要在删除它声明的东西时删除前向声明,否则它会闲置并破坏命名空间。但即使在这种情况下,如果它妨碍了编译器,编译器也会在错误消息中指向它。
更大的危险是没有提前声明。嵌套类的一个主要缺点是它们不能被前向声明(嗯,它们可以在封闭的类范围内,但这只是简短的)。
前向声明的唯一危险是当您在标头之外或在非共享标头中进行前向声明时,前向声明的签名与前向声明的任何内容的实际签名不同。如果您在 中执行此操作extern "C"
,则在链接时将不会有名称修改来检查签名,因此当签名不匹配时,您最终可能会出现未定义的行为。
我在Google C++ Style Guide中发现了一个有趣的片段
他们指出的危险来自在不完整类型上实现功能。通常,这编译器会抛出一个错误,但因为这些是指针,它可以通过网络溜走。
可能很难确定是否需要前向声明或完整的#include。用前向声明替换 #include 可以默默地改变代码的含义:
// b.h: struct B {}; struct D : B {}; // good_user.cc: #include "b.h" void f(B*); void f(void*); void test(D* x) { f(x); } // calls f(B*)
如果#include 被替换为B 和D 的前向decls,test() 将调用f(void*)。
前向声明的另一个危险是它更容易违反单一定义规则。假设你有 ah 前向声明class B
(应该在 bh 和 b.cpp 中),但在 a.cpp 中你实际上包含了 b2.h,它声明了一个与class B
bh 不同的,那么你会得到未定义的行为。
前向声明本身并没有那么危险,但它是一种代码味道。如果您需要前向声明,则意味着两个类是紧密耦合的,这通常是不好的。因此,这表明您的代码可能需要重构。
在某些情况下,紧密耦合是可以的,例如,状态模式实现中的具体状态可能是紧密耦合的。我会认为这没问题。但在大多数其他情况下,我会在使用前向声明之前改进我的设计。
第一种方法是重新排序我们的函数调用,因此 add 在 main 之前定义:
这样,当 main() 调用 add() 时,它就已经知道 add 是什么了。因为这是一个如此简单的程序,所以这种改变比较容易做到。然而,在一个大型程序中,试图破译哪些函数调用了哪些其他函数以便能够以正确的顺序声明它们将是非常乏味的。