代码之家  ›  专栏  ›  技术社区  ›  ereOn

返回前向声明的结构是否未定义行为?

  •  8
  • ereOn  · 技术社区  · 14 年前

    我有以下代码(为了简单起见,省略了保护):

    = foo.hpp =

    struct FOO
    {
      int not_used_in_this_sample;
      int not_used_in_this_sample2;
    };
    

    = main.cpp =

    #include "foo_generator.hpp"
    #include "foo.hpp"
    
    int main()
    {
      FOO foo = FooGenerator::createFoo(0xDEADBEEF, 0x12345678);
    
      return 0;
    }
    

    = foo_generator.hpp =

    struct FOO; // FOO is only forward-declared
    
    class FooGenerator
    {
      public:
    
        // Note: we return a FOO, not a FOO&
        static FOO createFoo(size_t a, size_t b);
    };
    

    = foo_generator.cpp =

    #include "foo_generator.hpp"
    #include "foo.hpp"
    
    FOO FooGenerator::createFoo(size_t a, size_t b)
    {
      std::cout << std::hex << a << ", " << b << std::endl;
    
      return FOO();
    }
    

    目前,这段代码编译得非常好,没有任何警告。如果我的理解是正确的,它应该输出:

    deadbeef, 12345678
    

    但相反,它随机显示:

    12345678, 32fb23a1
    

    或者只是撞车。

    如果我替换FOO in的forward声明 福乌发电厂 具有 #include "foo.hpp" ,然后它就工作了。

    所以我的问题是:返回前向声明的结构是否会导致未定义的行为?或者有什么可能出错?

    使用的编译器:MSVC 9.0和10.0(都显示了问题)

    3 回复  |  直到 14 年前
        1
  •  7
  •   visitor    14 年前

    根据8.3.5.6,这应该很好:“非定义函数声明的参数类型或返回类型可能是不完整的类类型。”

        2
  •  3
  •   fil    13 年前

    我想我也有同样的问题。 它发生在 小返回值类型 标题包含的顺序 事项。为了避免它 不使用返回值类型转发声明 按相同顺序包含标题 .

    要获得可能的解释,请查看以下内容:

    功能h

    struct Foo;
    Foo func();
    

    功能cpp

    #include "func.h"
    #include "foo.h"
    Foo func()
    {
        return Foo();
    }
    

    食物

    struct Foo
    {
        int a;
    };
    

    注意,整个Foo都放在一个CPU寄存器中。

    功能装配(MSVS 2005)

    $T2549 = -4                     ; size = 4
    ___$ReturnUdt$ = 8                  ; size = 4
    ?func@@YA?AUFoo@@XZ PROC                ; func
    
    ; 5    :     return Foo();
    
        xor eax, eax
        mov DWORD PTR $T2549[ebp], eax
        mov ecx, DWORD PTR ___$ReturnUdt$[ebp]
        mov edx, DWORD PTR $T2549[ebp]
        mov DWORD PTR [ecx], edx
        mov eax, DWORD PTR ___$ReturnUdt$[ebp]
    

    当func()被声明时,Foo的大小未知。它不知道Foo怎么会被退回。所以func()期望指针返回值存储作为其参数。在这里 _ $ReturnUdt$。Foo()的值复制到那里。

    如果在func.cpp中更改标题顺序,则会得到:

    函数asm

    $T2548 = -4                     ; size = 4
    ?func@@YA?AUFoo@@XZ PROC                ; func
    
    ; 5    :     return Foo();
    
        xor eax, eax
        mov DWORD PTR $T2548[ebp], eax
        mov eax, DWORD PTR $T2548[ebp]
    

    现在编译器知道Foo足够小,所以它是通过寄存器返回的,不需要额外的参数。

    主.cpp

    #include "foo.h"
    #include "func.h"
    int main()
    {
        func();
        return 0;
    }
    

    注意,这里Foo的大小在func()声明时是已知的。

    主.asm

    ; 5    :     func();
    
        call    ?func@@YA?AUFoo@@XZ         ; func
        mov DWORD PTR $T2548[ebp], eax
    
    ; 6    :     return 0;
    

    所以编译器假设func()将通过寄存器返回值。它不传递指向临时位置的指针来存储返回值。 但如果func()期望它写入内存的指针损坏堆栈。

    让我们更改标题顺序,让func.h优先。

    主.asm

    ; 5    :     func();
    
        lea eax, DWORD PTR $T2548[ebp]
        push    eax
        call    ?func@@YA?AUFoo@@XZ         ; func
        add esp, 4
    
    ; 6    :     return 0;
    

    编译器传递func()所期望的指针,因此不会导致堆栈损坏。

    如果Foo的大小大于2个整数,编译器将始终传递指针。

        3
  •  1
  •   Matthew Flaschen    14 年前

    在海合会的领导下对我来说很好。我不知道为什么,因为 foo.hpp 包括在 foo_generator.hpp .