前置声明的危险性是什么?

32

我刚参加了一次面试。我被问到什么是“前向声明(forward declaration)”后,被问及是否存在与前向声明相关的危险。

我对第二个问题无法回答。在网上搜索也没有找到任何有趣的结果。

那么,有人知道使用前向声明是否存在任何危险吗?


3
前向声明一个 类型 不会带来任何危险,但会使该类型成为编译器中的 不完全类型,这会限制您在特定 TU 中使用该类型的方式。然而,这绝对不是一种限制。 - Alok Save
@thang 在面试中提供这样的答案肯定会让你被雇佣 :P - Dariusz
9个回答

22

除了重复问题以外...

...标准中至少有一个痛点。

如果你在指向不完整类型的指针上调用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

难道你不想感谢编译器提醒你吗 ;) ?


由广大网友投票通过,谢谢!读者也应该查看其他答案。 - Offirmo
1
@Offirmo:不要感到任何压力去接受任何答案。这是你的问题,只有当答案满足你时才应该接受它,而你肯定不必接受得票最多的答案。这是你的问题,你做主。 - Matthieu M.
我没有表达清楚。这个问题是“开放式”的,不针对特定的问题。你的答案非常好,概述了似乎是最大的危险(大多数人都同意)。所以你应该成为答案。现在Luchian的罕见模板案例也很有趣,应该阅读以获得完整的答案。我不能选择多个答案,所以请每个人一起点赞Luchian的答案和这个答案。 - Offirmo
@Offirmo:那我们给Luchian的答案点个赞,这样它就能靠近顶部了 ;) - Matthieu M.

10

前置声明是C++缺少模块的症状(在C++17中将被修复?),通过头文件包含使用,如果C++有了模块,就根本不需要前置声明。

前置声明不亚于“合同”,通过使用它,您实际上承诺会提供某些东西的实现(在同一源文件中之后,或者稍后链接二进制文件)。

这样做的缺点是,您实际上必须遵循您的合同,这不是什么大问题,因为如果您不遵循您的合同,编译器将会提前发出警告,但在一些语言中,代码可以在不需要“承诺其存在”的情况下执行(说的是动态类型语言)。


8
我认为任何危险都被利益所超越了。但是有些问题与重构有关。
  • 更改类名会影响所有前向声明。当然,这也涉及到包含文件,但错误会在不同的位置生成,因此更难发现。
  • 将类从一个命名空间移动到另一个命名空间,并结合使用指令,可能会引起混乱(神秘的错误,难以发现和修复)——当然,使用指令本身就不好,但没有代码是完美的,对吧?*
  • 模板——要转发声明模板(尤其是用户定义的模板),您需要签名,这会导致代码重复。

请考虑

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

这显然使我的代码无效了,但错误并不在实际位置,所做的修复措施可以说是靠运气。


4
“wreak havoc” 的意思是“造成严重破坏或混乱”,而不是“wreck hazard”。请注意区分两者的意思。 - Konrad Rudolph
2
在面试中,我不会提及打开第三方命名空间并添加对它们类的前向声明。 - Potatoswatter

4
如果将指向不完整类类型的指针传递给delete,则可能会忽略operator delete重载。
这就是我得到的所有信息……为了避免被咬,您必须在源文件中仅进行“不完整类型”编译器错误的操作,而不做其他任何事情。
编辑:跟随其他人的意见,我会说困难(可能被视为危险)在于确保前向声明实际上与真实声明相匹配。对于函数和模板,必须保持参数列表同步。
当删除声明的内容时,您需要删除前向声明,否则它会留在命名空间中并使其变得混乱。但即使在这种情况下,如果它妨碍了编译器,编译器也会在错误消息中指出它。
更大的危险是没有前向声明。嵌套类的主要缺点是它们不能被前向声明(好吧,在封闭类作用域内可以,但那只是短暂的)。

3

仅在以下情况下,提前声明某些内容会存在风险:当您在头文件之外或非共享头文件中进行提前声明时,并且提前声明的签名与实际被提前声明的内容的签名不同。如果您在 extern "C" 中这样做,则没有名称重整来检查链接时间的签名,因此当签名不匹配时,可能会出现未定义行为。


1
我在Google C++ Style Guide中发现了一个有趣的片段。
他们指出的危险来自于对不完整类型实现函数。通常情况下,编译器会抛出错误,但因为这些是指针,所以可能会被忽略。

It can be difficult to determine whether a forward declaration or a full #include is needed. Replacing an #include with a forward declaration can silently change the meaning of code:

// 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*)

If the #include was replaced with forward decls for B and D, test() would call f(void*).


0
前向声明的另一个危险是它更容易违反“单一定义规则”。假设您有一个.h文件,其中前向声明了class B(应该在b.h和b.cpp中),但实际上在a.cpp中包含了声明不同的class B的b2.h文件,则会导致未定义行为。

0

前置声明本身并不危险,但它是一种代码异味。如果您需要前置声明,则意味着两个类紧密耦合,这通常是不好的。因此,这表明您的代码可能需要重构。

有些情况下,紧密耦合是可以接受的,例如状态模式实现中的具体状态可能会紧密耦合。我认为这是可以接受的。但在大多数其他情况下,在使用前置声明之前,我会改进我的设计。


-2
第一种方法是重新排列我们的函数调用,使add在main之前定义:
这样,当main()调用add()时,它已经知道add是什么。因为这是一个非常简单的程序,所以这个改变相对容易实现。然而,在一个大型程序中,试图解析哪些函数调用了哪些其他函数,以便按正确的顺序声明它们将会非常繁琐。

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