在不同范围的结构之间进行转换

Jon*_*eld 12 c++ language-lawyer

我有兴趣在指向可能兼容的结构的指针之间进行转换.他们将使用相同的标签,相同的成员使用相同的顺序.虽然目标代码库被编译为C或C++,但为了简化这个问题,我想将其限制为仅限C++.

在这种情况下,我确信编译器的行为会合理,但我找不到支持证据表明它需要这样做.

激励代码的例子是:

#include <cstdio>

void foo(void * arg)
{
    struct example
    {
        int a;
        const char * b;
    };

    example * myarg = static_cast<example *>(arg);
    printf("meaning of %s is %d\n",myarg->b,myarg->a);
}

void bar(void)
{
    struct example
    {
        int a;
        const char * b;
    };

    example on_stack {42, "life"};
    foo(&on_stack);
}

int main(int,char**)
{
    bar();
}
Run Code Online (Sandbox Code Playgroud)

我对C++ 11标准的运气不太好.关于类的第9节建议示例将是"布局兼容的",这听起来令人鼓舞,但我无法找到结构"布局兼容"的后果的描述.特别是,我可以将一个指针指向另一个指针而不会产生后果吗?

一位同事认为"布局兼容"意味着memcpy将按预期工作.鉴于所讨论的结构也总是易于复制,以下名义上效率低下的代码可能会避免UB:

#include <cstdio>
#include <cstring>

void foo(void * arg)
{
    struct example
    {
        int a;
        const char * b;
    };

    example local;
    std::memcpy(&local, arg, sizeof(example));
    printf("meaning of %s is %d\n", local.b, local.a);
}

// bar and main as before
Run Code Online (Sandbox Code Playgroud)

实际的动机是当结构定义仅用于少量函数之间的通信时,将结构定义从全局范围中取出.我很欣赏这是一个好主意是值得商榷的.

Nic*_*las 5

[basic.lval] 10.6是否允许布局兼容类型之间的别名?不是.有问题的部分说明:

聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员)

回想一下,"前面提到的类型"是实际类型T,动态类型T,类似于动态类型的类型,动态类型的某些const/volatile限定版本,或动态类型的有符号/无符号版本.

现在,请考虑以下代码:

struct T {int i;};
struct U {int i;};

T t;
U *pu = (U*)&t;
pu->i = 5;
Run Code Online (Sandbox Code Playgroud)

现在,让我们看一下10.6.10.6问题是glvalue的类型是否U包含符合10.1-10.5资格的成员.可以?请记住,对象的动态类型tT.

  • 是否U包含类型的成员T?没有.
  • 是否U包含一个const/volatile限定版本的成员T?没有.
  • 是否U包含类似于T?的类型的成员?没有.
  • 是否U包含签名/未签名版本的成员T?没有.
  • 是否U包含一个const/volatile限定版本的签名/未签名版本的成员T?没有.

由于所有这些失败,编译器允许假设修改指向的对象pu不能修改对象t.


供参考:

无论如何,memcopy和指针别名完全相同,除了全局结构对齐.

不,他们不是.琐碎复制能力和布局兼容性的规则与别名规则完全不同.

简单的可复制性是关于复制对象的值表示的完整性以及这样的副本是否代表合法对象.布局兼容性的规则是关于值表示A是否兼容B,以便A可以将值复制到类型的对象中B.

别名是关于是否可以通过指针/引用A和指针/引用B同时访问对象.严格别名规则规定,如果编译器看到a A& a和a B& b,则允许编译器假定通过的修改a 不会影响通过引用的对象b,反之亦然.[basic.lval] 10概述了不允许编译器假设这种情况的情况.


Ser*_*sta 2

现在很清楚(感谢Nicol Bolas 的回答),由于严格的别名规则,两个简单布局兼容的结构之间的直接别名将调用 UB。

当然你可以memcopy内容,但是:

  • 根据结构大小,它可能会很昂贵
  • 您只会得到一个副本(更改不会反映出来),除非您在完成后进行内存复制

但是...您可以在 C++ 中创建指向原始值的引用结构。它将直接将成员别名为其原始类型,该类型现在已由标准完美定义。

foo 的代码可能会变成:

void foo(void * arg)
{
    struct example // only used to declare the layout
    {
        int a;
        const char * b;
    };
    struct r_example {
    int &a;
    const char *&b;
    r_example(void *ext): a(*(static_cast<int*>(ext))),
        b(*(reinterpret_cast<const char **>(
            static_cast<char*>(ext) + offsetof(example, b)))) {}
    };


    r_example myarg(arg);
    printf("in foo meaning of %s is %d\n",myarg.b,myarg.a);
    myarg.a /= 2;
}
Run Code Online (Sandbox Code Playgroud)

最后一行引入的更改在调用者中没有 UB 的情况下可见:

void bar(void)
{
    struct example
    {
        int a;
        const char * b;
    };

    example on_stack {42, "life"};
    foo(&on_stack);
    printf("after foo meaning of %s is %d\n",on_stack.b,on_stack.a);
}
Run Code Online (Sandbox Code Playgroud)

将显示:

in foo meaning of life is 42
after foo meaning of life is 21
Run Code Online (Sandbox Code Playgroud)

C 对应部分将使用指针而不是引用:

    struct p_example {
        int *a;
        const char **b;
    } my_arg;
    my_arg.a = (int *) ext;
    my_arg.b = (const char **)(((char*)ext) + offsetof(example, b));

    printf("in foo meaning of %s is %d\n",*(myarg.b),*(myarg.a));
    *(myarg.a) /= 2;
Run Code Online (Sandbox Code Playgroud)