我有两个结构体struct Temp,并且struct MyTemp具有相同的内存布局,并且我想将类型的返回值转换struct Temp为类型struct MyTemp,例如,在 C++ 中,我可以在一行中(没有任何临时变量)实现此目标,如下所示:

#include <utility>
struct Temp {
    int a;
    int b;
};
struct MyTemp {
    int a;
    int b;
};
struct Temp get_temp(int a, int b) {
    struct Temp temp = {
        .a = a,
        .b = b,
    };
    return temp;
}
int main() {
    struct MyTemp mt = reinterpret_cast<MyTemp &&>(std::move(get_temp(1, 2)));
}

但是,我在 C 中找不到等效代码,例如,以下 C 代码无法编译:

struct Temp {
    int a;
    int b;
};
struct MyTemp {
    int a;
    int b;
};
struct Temp get_temp(int a, int b) {
    struct Temp temp = {
        .a = a,
        .b = b,
    };
    return temp;
}
int main() {
    struct MyTemp mt = *(struct MyTemp *)&get_temp(1, 2);
    // error: cannot take the address of an rvalue of type 'struct Temp'
}

所以我的问题是,有没有办法做我在 C++ 中做过的事情?如果没有,为什么?为什么可以std::move绕过这个麻烦?

5

  • 5
    我猜是 UB。在 C++20 中,我会写。在 C 中,应该使用MyTemp mt = std::bit_cast<MyTemp>(get_temp(1,2))普通的旧方法。memcpy


    – 


  • 4
    您试图解决的实际问题是什么?为什么需要这样的转换?它将如何解决原始问题?目前,您的问题很大程度上是一个


    – 

  • 1
    在 C 中,您不能直接获取右值(如函数返回的临时值)的地址,这就是您的代码无法编译的原因。但是,在 C++ 中,std::move 可以工作,因为它将返回值转换为 xvalue(到期值),并且 reinterpret_cast 允许您在结构之间进行转换,因为它们具有相同的布局。


    – 

  • ISO/IEC 9899 对强制类型转换的定义如下:“在表达式前面加上带括号的类型名称会将表达式的值转换为指定类型的非限定版本。这种构造称为强制类型转换。指定不进行转换的强制类型转换对表达式的类型或值没有影响。”


    – 

  • 我认为 C++ 中没有任何约束可以保证这一点 TempMyTemp具有相同的布局。


    – 


最佳答案
3

在 C 中,将内存重新解释为不同类型的标准方法是复制字节或使用联合。这两种方法都不会对单纯的值(非左值的值)进行操作。您需要使用临时对象,如下所示:

struct MyTemp m;
memcpy(&m, (struct Temp []) { get_temp(1, 2) }, sizeof m);

或者:

struct MyTemp m = (union { struct Temp t; struct MyTemp m; }) { get_temp(1, 2) } .m;

但是,任何有这种“需要”的程序都应该重新审视,以重新设计它来避免这种需要。

您收到的错误是因为函数返回的值不是左值(即它不指定对象),因此没有地址可取。您可以通过将返回值分配给临时值来解决此问题:

struct Temp t = get_temp(1, 2);
struct MyTemp mt = *(struct MyTemp *)&t;

然而,这是一种严格的别名违规,因为不允许通过指向另一种类型的结构的指针重新解释一种类型的结构。

可以做的是使用union这两个结构之一来进行所需的重新解释:

union MyUnion {
    struct Temp t;
    struct MyTemp mt;
};

union MyUnion u = { .mt = get_temp(1, 2) };
print("t.a=%d, t.b=%d\n", u.t.a, u.t.b);

这是允许的,因为这两个结构具有共同的初始序列,即,它们以一个或多个相同类型的字段开头,并且在这种情况下,允许通过联合在任一结构中读取这些共同的初始成员之一。

然而,这种用例很可能表明存在设计问题。如果您的代码需要这种类型的转换,则可能应该重构它以避免进行这种类型的重新解释。

总的来说,与普遍的看法相反,C 语言实际上并不允许在各种不相关的类型之间进行疯狂的基于指针的转换。或者说,就指针转换规则而言,转换本身可能是允许的,但从此以后的结果或指针取消引用很少具有明确定义的行为。


C 语言中结构兼容性的规则非常详细和繁琐(C17 6.2.7):

(我已将标准文本重新格式化为人类可读的项目符号列表。)

如果两种类型的类型相同,则它们具有兼容类型
。/–/
此外,如果它们的标签和成员满足以下要求,则在单独的翻译单元中声明的两个结构,联合或枚举类型是兼容的:

  • 如果其中一个用标签声明,则另一个也应使用相同的标签声明。

  • 如果两者都在各自的翻译单元的任何地方完成,则适用以下附加要求:

    它们的成员之间应当一一对应,即每对相应的成员都用兼容类型声明;如果该对中的一个成员用对齐说明符声明,则另一个成员用等效对齐说明符声明;并且如果该对中的一个成员用名称声明,则另一个成员用相同的名称声明。

  • 对于两个结构体,相应的成员应当按照相同的顺序声明。

  • 对于两个结构或联合,相应的位域应具有相同的宽度。

在您的情况下,有两个不同的标签(并且也在同一个翻译单元中?)因此它会失败 – 结构不兼容,因此您不能在它们之间进行野指针转换,否则您将违反严格的别名规则,调用未定义的行为。


至于你的编译器错误,它与结构无关,而是与基本 C 有关。函数的返回值是所谓的右值,因此你不能获取它的地址。你必须先将它存储在某个地方。


至于该问题的解决方案,最明显的方法就是删除其中一个结构,因为它似乎是多余的。

如果您由于某种原因必须拥有两个具有相同成员的结构,那么您可以将typedef一个类型转换为另一个名称(那么您将无法使用“结构标签”编码样式)。

由于一些非常奇怪的要求,所有这些都失败了……将两个结构都扔进去union。这确实是令人困惑和奇怪的代码:

typedef union
{
  struct Temp {
    int a;
    int b;
  } temp;
  struct MyTemp {
    int a;
    int b;
  } my_temp;
} last_resort_t;

现在,虽然该联合对于您的翻译单元是可见的,但在“严格别名”中的例外以及称为“公共初始序列”的古怪规则下,在两个结构类型和/或联合类型之间进行疯狂的强制转换是明确定义的。但是你为什么要这样做……