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

时间:2016-05-19 12:27:55

标签: 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();
}

我对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

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

3 个答案:

答案 0 :(得分: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;

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

  • U是否包含T类型的成员?否。
  • U是否包含T的const / volatile限定版本的成员?否。
  • U是否包含与T类似的成员?否。
  • U是否包含T的签名/未签名版本的成员?否。
  • U是否包含一个成员,该成员是T的签名/未签名版本的const / volatile限定版本?否。

由于所有这些都失败了,因此允许编译器假定修改pu指向的对象将修改对象t

供参考:

  

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

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

琐碎的可复制性是指复制对象的值表示以及此类副本是否代表合法对象的完整性。布局兼容性规则是关于A的值表示是否与B兼容,以便A的值可以复制到B类型的对象中。

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

答案 1 :(得分:2)

现在很明显(感谢Nicol Bolas's answer)两个简单布局兼容的结构之间的直接别名会因为严格的别名规则而调用UB。

当然你可以记忆内容,但是:

  • 根据结构尺寸可能很昂贵
  • 您只能获得一份副本(不会反映更改),除非您在完成后回复

但是......你可以在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;
}

在调用者中没有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);
}

将显示:

in foo meaning of life is 42
after foo meaning of life is 21

C对应物将使用指针而不是refs:

    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;

答案 2 :(得分:0)

我同意Nicol Bolas的回答,即使布局兼容,您无法通过其他类型访问类型。我只想添加 layout-compatible 意味着什么

N3337 9.2 / 17

  

两个标准布局结构(第9条)类型与布局兼容,如果它们具有相同数量的非静态   数据成员和相应的非静态数据成员声明顺序具有布局兼容性   类型(3.9)。

现在解释所有条款:

(请注意布局兼容类型 布局兼容标准布局结构 是两件不同的事情)

<强> 1。布局兼容类型

布局兼容类型表示相同类型

N3337 3.9 / 11:

  

如果两种类型 T1和T2属于同一类型,则 T1和T2 布局兼容类型

<强> 2。标准布局结构:

N3337 9/8

  

标准布局结构是使用 class-key 结构或 class-key 类定义的标准布局类

(或者换句话说(因为C ++引用结构,联合和类只是)它是标准布局类而不是联合)

标准布局类是:

N3337 9/7

  

标准布局类是一个类:

     
      
  • 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
  •   
  • 没有虚函数(10.3),没有虚基类(10.1),
  •   
  • 对所有非静态数据成员具有相同的访问控制(第11条),
  •   
  • 没有非标准布局基类
  •   
  • 要么在最派生类中没有非静态数据成员,要么最多只有一个基类   非静态数据成员,或者没有包含非静态数据成员的基类,
  •   
  • 没有与第一个非静态数据成员相同类型的基类。   108
  •   
     

108)这确保了具有相同类类型且属于同一最派生对象的两个子对象不是   分配在同一地址(5.10)