我刚接受采访。我被问到什么是“前瞻性声明”。然后我被问到他们是否是与前瞻性声明有关的危险。
我无法回答第二个问题。在网上搜索没有显示任何有趣的结果。
那么,有人知道与使用前向声明有关的任何危险吗?
答案 0 :(得分:15)
那么,除了有关重复的问题......
......标准中至少有一个疼痛部位。
如果在指向不完整类型的指针上调用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
您是否要感谢您的编译器警告您;)?
答案 1 :(得分:7)
我认为任何危险都会被收益黯然失色。但有一些,主要与重构有关。
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;
}
这显然使我的代码无效,但错误不是在实际的地方而且修复至少可以说是腥。
答案 2 :(得分:4)
如果将指向不完整类类型的指针传递给delete
,则可能会忽略operator delete
重载。
这就是我得到的......而且被咬了,你必须在源文件中执行但没有别的会导致“不完整类型”的编译器错误。
编辑:在其他人的带领下,我会说难度(可能被视为危险)是确保前向声明实际上与真实声明相符。对于函数和模板,参数列表必须保持同步。
你需要在删除它声明的东西时删除前向声明,或者它位于并占据命名空间。但即使在这种情况下,编译器也会在错误消息中指出它,如果它妨碍了它。
更大的危险是不有前瞻声明。嵌套类的一个主要缺点是它们不能被前向声明(好吧,它们可以在封闭的类范围内,但这只是简短的。)
答案 3 :(得分:4)
前向声明是C ++缺失模块的症状(将在C ++ 17中修复?)并使用包含头,如果C ++有模块则根本不需要前向声明。
前瞻性声明不低于&#34;合同&#34;,通过使用您实际承诺您将提供某些内容的实现(在相同的源文件之后,或者通过以后链接二进制文件。)
缺点是你实际上必须遵守你的合同,这不是什么大问题,因为如果你不遵守你的合同,编译器会以某种方式提前抱怨,但在某些方面语言代码只需执行而不需要承诺自己的存在&#34; (谈到动态类型语言)
答案 4 :(得分:3)
向前声明某事的唯一危险是当你在标题之外或非共享标题中进行前向声明时,前向声明的签名与前向声明的任何内容的实际签名不同。如果你在extern "C"
中执行此操作,则在链接时不会检查签名,因此当签名不匹配时,最终可能会出现未定义的行为。
答案 5 :(得分:0)
前向声明的另一个危险是它更容易违反一个定义规则。假设你有前向声明class B
(应该在bh和b.cpp中),但在a.cpp中你实际上包含b2.h,它声明了不同于class B
而不是bh,那么你得到了未定义的行为。
答案 6 :(得分:0)
前瞻声明本身并不是那么危险,但这是一种代码味道。如果你需要一个前向声明,这意味着两个类紧密耦合,这通常是坏的。因此,它表明您的代码可能需要重构。
在某些情况下可以紧密耦合,例如状态模式实现中的具体状态可以紧密耦合。我会认为这很好。但在大多数其他情况下,我会在使用前瞻声明之前改进我的设计。
答案 7 :(得分:0)
它们指出的危险来自对不完整类型实现函数。通常,这会使编译器抛出错误,但是由于这些是指针,因此它可以在网络中滑动。
很难确定是前向声明还是 需要完整的#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的正向decl,则test()将调用f(void *)。
答案 8 :(得分:-2)
第一种方法是重新排序我们的函数调用,因此在main之前定义了add:
这样,当main()调用add()时,它已经知道add是什么。因为这是一个如此简单的程序,这种改变相对容易。但是,在一个大型程序中,尝试破译哪些函数调用哪些其他函数以便以正确的顺序声明它们将是非常繁琐的。