【问题标题】:What are the dangers of forward declarations?提前申报有什么危险?
【发布时间】:2013-01-17 14:33:08
【问题描述】:

我刚刚接受了采访。有人问我什么是“前向声明”。然后我被问到它们是否是与前向声明相关的危险。

我无法回答第二个问题。在网上搜索并没有显示任何有趣的结果。

那么,有人知道与使用前向声明相关的任何危险吗?

【问题讨论】:

  • 向前声明一个 type 会使该类型成为编译器的 Incomplete type 并没有危险,这限制了您在特别的TU。不过,这绝不是限制。
  • @thang 在面试中给出这样的答案肯定会让你被录用:P

标签: c++ forward-declaration


【解决方案1】:

嗯,除了重复的问题...

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

如果您在指向不完整类型的指针上调用 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:完全没有接受任何答案的压力。这是您的问题,只有当它让您满意时您才应该接受答案,并且您当然没有义务接受得票最多的答案。你的问题,你的规则。
  • 我措辞不好。这个问题是“开放的”,不是针对特定问题的。您的回答非常好,并概述了似乎最大的危险(大多数人同意)。所以你应该得到答案。现在 Luchian 的稀有模板案例也很有趣,应该阅读以获得完整的答案。我无法选择多个答案,所以请大家将 Luchian 的答案与这个答案一起投票。
  • @Offirmo:让我们支持 Luchian 的答案,然后让它更接近顶部;)
【解决方案2】:

前向声明是 C++ 缺少模块的症状(将在 C++17 中修复?)并且使用包含头文件,如果 C++ 有模块,则根本不需要前向声明。

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

这样做的缺点是您实际上必须遵守合同,这并不是什么大问题,因为如果您不遵守合同,编译器会以某种方式提前抱怨,但在某些语言中代码无需“承诺自己的存在”就可以执行(说到动态类型语言)

【讨论】:

    【解决方案3】:

    我想说任何危险都会被收益所掩盖。不过也有一些,主要与重构有关。

    • 重命名类会影响所有前向声明。这当然也带有包含,但错误是在不同的地方生成的,因此更难发现。
    • 将类从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

    *I've recently ran into this:

    原文:

    定义:

    //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】:

    如果将指向不完整类类型的指针传递给 delete,则可能会忽略 operator delete 重载。

    这就是我得到的全部......而且要被咬,你必须在源文件中这样做但没有其他任何事情会导致“不完整类型”编译器错误。

    编辑:在其他人的带领下,我想说困难(可能被认为是危险)是确保前向声明实际上与真实声明相匹配。对于函数和模板,参数列表必须保持同步。

    并且您需要在删除它声明的东西时删除前向声明,否则它会闲置并破坏命名空间。但即使在这种情况下,如果它妨碍了编译器,编译器也会在错误消息中指向它。

    更大的危险是没有有一个前向声明。嵌套类的一个主要缺点是它们不能被前向声明(嗯,它们可以在封闭的类范围内,但这只是简短的)。

    【讨论】:

      【解决方案5】:

      前向声明的唯一危险是当您在标头之外或在非共享标头中进行前向声明时,前向声明的签名与前向声明的实际签名不同。如果您在 extern "C" 中执行此操作,则不会在链接时检查签名的名称修改,因此当签名不匹配时,您最终可能会出现未定义的行为。

      【讨论】:

        【解决方案6】:

        我在Google C++ Style Guide遇到了一个有趣的sn-p

        他们指出的危险来自在不完整类型上实现函数。通常,这编译器会抛出一个错误,但因为这些是指针,它可能会漏网。

        很难确定是前向声明还是 需要完整的#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*)。

        【讨论】:

          【解决方案7】:

          前向声明的另一个危险是它更容易违反单一定义规则。假设你有 ah forward 声明 class B(应该在 bh 和 b.cpp 中),但在 a.cpp 中你实际上包含 b2.h,它声明了一个与 bh 不同的 class B,那么你会得到 undefined行为。

          【讨论】:

            【解决方案8】:

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

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

            【讨论】:

              【解决方案9】:

              第一种方法是重新排序我们的函数调用,因此 add 在 main 之前定义:

              这样,当 main() 调用 add() 时,它就已经知道 add 是什么了。因为这是一个如此简单的程序,所以这种改变比较容易做到。然而,在一个大型程序中,试图解读哪些函数调用了哪些其他函数以便能够以正确的顺序声明它们将是非常乏味的。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2012-08-30
                • 2012-01-13
                • 2012-02-10
                • 2015-03-08
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多