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

发布于 2024-10-05 07:28:31 字数 1259 浏览 2 评论 0原文

我有以下代码(为了简单起见,省略了 include-guards):

= 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

或者只是崩溃。

如果我用 #include "foo.hpp" 替换 foo_generator.hpp 中 FOO 的前向声明,那么它就可以工作。

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

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

I have the following code (include-guards omitted for simplicity's sake):

= 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();
}

This code, as it stands, compiles perfectly fine without any warning. If my understanding is correct, it should output:

deadbeef, 12345678

But instead, it randomly displays:

12345678, 32fb23a1

Or just crashes.

If I replace the forward-declaration of FOO in foo_generator.hpp with #include "foo.hpp", then it works.

So here is my question: Does returning a forward-declared structure lead to undefined behavior ? Or what can possibly go wrong ?

Compiler used: MSVC 9.0 and 10.0 (both show the issue)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

十雾 2024-10-12 07:28:31

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

That should be fine according to 8.3.5.6: "The type of a parameter or the return type for a function declaration that is not a definition may be an incomplete class type."

摇划花蜜的午后 2024-10-12 07:28:31

我想我也遇到了同样的问题。
小返回值类型会发生这种情况,并且标头包含的顺序很重要。为了避免这种情况,不要使用返回值类型前向声明以相同的顺序包含标头

有关可能的解释,请查看以下内容:

func.h

struct Foo;
Foo func();

func.cpp

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

foo.h

struct Foo
{
    int a;
};

请注意,整个 Foo 适合单个 CPU 寄存器。

func.asm (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 中的标头顺序,我们会得到:

func.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 足够小,因此它通过寄存器返回,不需要额外的参数。

main.cpp

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

请注意,当声明 func() 时,Foo 的大小是已知的。

main.asm

; 5    :     func();

    call    ?func@@YA?AUFoo@@XZ         ; func
    mov DWORD PTR $T2548[ebp], eax

; 6    :     return 0;

因此编译器假设 func() 将通过寄存器返回值。它不会传递指向临时位置的指针来存储返回值。
但是,如果 func() 需要指针,它就会写入内存,从而破坏堆栈。

让我们更改标头顺序,以便 func.h 首先出现。

main.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 个整数,编译器将始终传递指针。

I guess I got the same problem.
It happens with small return value types and the order of headers inclusion matters. To avoid it don't use return value type forward declaration or include headers in the same order.

For a possible explanation look at this:

func.h

struct Foo;
Foo func();

func.cpp

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

foo.h

struct Foo
{
    int a;
};

Notice that whole Foo fits in a single CPU register.

func.asm (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]

When func() is declared Foo's size is unknown. It doesn't know how Foo could be returned. So func() expects pointer to return value storage as its parameter. Here it's _$ReturnUdt$. Value of Foo() is copied there.

If we change headers order in func.cpp we get:

func.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]

Now compiler knows that Foo is small enough so it is returned via register and no extra parameter needed.

main.cpp

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

Notice that here Foo's size is known when func() is declared.

main.asm

; 5    :     func();

    call    ?func@@YA?AUFoo@@XZ         ; func
    mov DWORD PTR $T2548[ebp], eax

; 6    :     return 0;

So compiler assumes func() will return value through register. It doesn't pass a pointer to temp location to store return value.
But if func() expects the pointer it writes to memory corrupting the stack.

Let's change headers order so func.h goes first.

main.asm

; 5    :     func();

    lea eax, DWORD PTR $T2548[ebp]
    push    eax
    call    ?func@@YA?AUFoo@@XZ         ; func
    add esp, 4

; 6    :     return 0;

Compiler passes the pointer that func() expects so no stack corruption results.

If Foo's size were bigger than 2 integers compiler would always pass the pointer.

东北女汉子 2024-10-12 07:28:31

它在 GCC 下对我来说工作得很好。我不知道为什么它不会,因为 foo.hpp 包含在 foo_generator.hpp 之前。

It works fine for me under GCC. I don't know why it wouldn't, since foo.hpp is included before foo_generator.hpp.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文