memmove有效类型的原位变更(类型 - 双关语)

时间:2017-10-14 14:46:58

标签: c++ type-punning

在以下问题中: What's a proper way of type-punning a float to an int and vice-versa?,结论是从整数位构造双精度的方法,反之亦然是memcpy

没关系,发现pseudo_cast转化方法有:

template <typename T, typename U>
inline T pseudo_cast(const U &x)
{
    static_assert(sizeof(T) == sizeof(U));    
    T to;
    std::memcpy(&to, &x, sizeof(T));
    return to;
}

我会像这样使用它:

int main(){
  static_assert(std::numeric_limits<double>::is_iec559);
  static_assert(sizeof(double)==sizeof(std::uint64_t));
  std::uint64_t someMem = 4614253070214989087ULL;
  std::cout << pseudo_cast<double>(someMem) << std::endl; // 3.14
}

我只是阅读标准和cppreference的解释是/也应该可以使用memmove来就地更改effective type,如下所示:

template <typename T, typename U>
inline T& pseudo_cast_inplace(U& x)
{
    static_assert(sizeof(T) == sizeof(U));
    T* toP = reinterpret_cast<T*>(&x);
    std::memmove(toP, &x, sizeof(T));
    return *toP;
}

template <typename T, typename U>
inline T pseudo_cast2(U& x)
{
    return pseudo_cast_inplace<T>(x); // return by value
}

重新解释强制转换本身对于任何指针都是合法的(只要未违反cv,cppreference/reinterpret_cast处的第5项)。然而,解除引用需要memcpy memmove (第6.9.2节),并且T和U必须是可以轻易复制的。

这合法吗?它用gcc和clang编译并做正确的事。 根据{{​​1}}明确允许memmove源和目的地重叠 到cppreference std::memmovememmove

  

对象可能重叠:复制就像角色一样   复制到临时字符数组,然后是字符   从数组复制到dest。

编辑:最初这个问题有一个由@hvd发现的微不足道的错误(导致段错误)。谢谢!问题仍然存在,这是合法的吗?

4 个答案:

答案 0 :(得分:4)

C ++不允许仅通过复制字节来构造glOrtho(-15.0, 15.0, -15.0, 15.0, -200.0, 200.0); 。首先需要构造一个对象(可能会保留其未初始化的值),并且只有在此之后才能填充其字节以生成值。这在C ++ 14中没有明确规定,但目前的C ++ 17草案包含在[intro.object]中:

  

对象由定义(6.1), new-expression (8.3.4)创建,当隐式更改联合的活动成员时(12.3) ),或创建临时对象时(7.4,15.2)。

虽然使用默认初始化构造double不会执行任何初始化,但仍然需要进行构造。您的第一个版本通过声明局部变量double来包含此构造。你的第二个版本没有。

您可以修改第二个版本,以使用展示位置T to;在先前持有new对象的同一位置构建T,但在这种情况下,当您通过{{ 1}}到U,不再需要读取构成&x值的字节,因为对象memmove已被早期展示位置x销毁1}}。

答案 1 :(得分:2)

我对标准的阅读表明这两种功能都会导致UB。

考虑:

int main()
{
    long x = 10;
    something_with_x(x*10);
    double& y = pseudo_cast_inplace<double>(x);
    y = 20;
    something_with_y(y*10);
}

由于严格的别名规则,在我看来,没有什么可以阻止编译器重新排序指令以生成代码 - 如果:

int main()
{
    long x = 10;
    double& y = pseudo_cast_inplace<double>(x);
    y = 20;
    something_with_x(x*10);   // uh-oh!
    something_with_y(y*10);
}

我认为写这个的唯一合法方式是:

template <typename T, typename U>
inline T pseudo_cast(U&& x)
{
    static_assert(sizeof(T) == sizeof(U));
    T result;
    std::memcpy(std::addressof(result), std::addressof(x), sizeof(T));
    return result;
}

实际上会导致完全相同的汇编输出(即无论如何 - 整个函数都被省略,变量本身也是如此) - 至少在gcc上有-O2

答案 2 :(得分:1)

这在 C++20 中应该是合法的。 Example in godbolt

template <typename T, typename U>
requires (
    sizeof(U) >= sizeof(T) and 
    std::alignment_of_v<T> <= std::alignment_of_v<U> and 
    std::is_trivially_copyable_v<T> and
    std::is_trivially_destructible_v<U>
)
[[nodiscard]] T& reinterpret_object(U& obj)
{
    // Get access to object representation
    std::byte* bytes = reinterpret_cast<std::byte*>(&obj); 
    
    // Copy object representation to temporary buffer.
    // Implicitly create a T object in the destination storage. The lifetime of U object ends.
    // Copy temporary buffer back.
    void* storage = std::memmove(bytes, bytes, sizeof(T));
    
    // Storage pointer value is 'pointer to T object', so we are allowed to cast it to the proper pointer type.
    return *static_cast<T*>(storage);
}
  • reinterpret_cast 允许指向不同的指针类型 (7.6.1.10)

    <块引用>

    对象指针可以显式转换为不同类型的对象指针。

  • 允许通过 std::byte* 指针访问对象表示(7.2.1

    <块引用>

    如果程序尝试通过类型与以下类型之一不相似的泛左值访问对象的存储值,则行为未定义

    • char、unsigned char 或 std :: byte 类型。
  • std::memmove 就像复制到临时缓冲区一样,可以隐式创建对象 (21.5.3)

    <块引用>

    函数 memcpy 和 memmove 是信号安全的。 在将字符序列复制到目标之前,这两个函数都会在目标存储区域中隐式创建对象 ([intro.object])。

    隐式对象创建在 (6.7.2) 中描述

    <块引用>

    某些操作被描述为在指定的存储区域内隐式创建对象。 对于每个被指定为隐式创建对象的操作,该操作在其指定的存储区域中隐式创建并启动零个或多个隐式生命周期类型([basic.types])对象的生命周期,如果这样做会导致程序有明确的行为。 如果没有这样一组对象会给程序定义的行为,则程序的行为是未定义的。 如果多个这样的对象集会给程序定义的行为,则未指定创建哪个这样的对象集。 [注 4:此类操作不会启动本身不是隐式生命周期类型的此类对象的子对象的生命周期。 — 尾注]

    <块引用>

    此外,在指定的存储区域内隐式创建对象后,一些操作被描述为生成一个指向合适的创建对象的指针。 这些操作选择一个隐式创建的对象,其地址是存储区域的起始地址,并产生一个指向该对象的指针值,如果该值会导致程序具有定义的行为。 如果没有这样的指针值会给程序定义的行为,则程序的行为是未定义的。 如果多个这样的指针值会给程序定义的行为,则未指定产生哪个这样的指针值。

    未指定 std::memmove 是这样的函数,其返回的指针值将是指向隐式创建对象的指针。 但这是有道理的。

  • (7.6.1.9) 允许返回指向新对象的指针

    <块引用>

    “指向 cv1 void 的指针”类型的纯右值可以转换为“指向 cv2 T 的指针”类型的纯右值,其中 T 是对象类型,而 cv2 与 cv 限定相同,或者比 cv 限定更高, cv1。 如果原始指针值表示内存中一个字节的地址 A 并且 A 不满足 T 的对齐要求,则结果指针值是未指定的。 否则,如果原始指针值指向对象 a,并且有一个 T 类型的对象 b(忽略 cv 限定)与 a 的指针可相互转换,则结果是指向 b 的指针。 否则,转换后指针值不变。

    如果 std::memmove 没有返回可用的指针值,std::launder<T>(reinterpret_cast<T*>(bytes)) (17.6.5) 应该能够产生这样的指针值。

附加说明:

  • 我不能 100% 确定所有 requires 是否正确或缺少某些条件。

  • 为了获得零开销,编译器必须优化 std::memmove(gcc 和 clang 似乎做到了)。

  • 原始对象的生命周期结束(6.7.3

    <块引用>

    程序可以通过重用对象占用的存储空间或显式调用对象的析构函数或伪析构函数([expr.prim.id.dtor])来结束任何对象的生命周期。

    这意味着使用原始名称或指向它的指针或引用将导致未定义的行为。

    可以通过重新解释 reinterpret_object<U>(reinterpret_object<T>(obj)) 来“恢复”对象,这应该允许使用旧引用 (6.7.3)

    <块引用>

    如果在一个对象的生命周期结束后,在该对象所占用的存储空间被重用或释放之前,在原对象所占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用,或原始对象的名称将自动引用新对象,一旦新对象的生命周期开始,可用于操作新对象,如果原始对象是透明的可替换(见下文)新对象。 如果满足以下条件,对象 o1 可以透明地替换为对象 o2:

    <块引用>
    • o2 占用的存储空间正好覆盖了 o1 占用的存储空间,并且
    • o1 和 o2 属于同一类型(忽略顶级 cv 限定符),并且
    • o1 不是一个完整的 const 对象,并且
    • o1 和 o2 都不是潜在重叠的子对象 ([intro.object]),并且
    • 要么 o1 和 o2 都是完整对象,要么 o1 和 o2 分别是对象 p1 和 p2 的直接子对象,并且 p1 可以透明地替换为 p2。
  • 对象表示应该是“兼容的”,将原始对象的字节解释为新对象的字节会产生“垃圾”甚至陷阱表示。

答案 3 :(得分:0)

在实际类型为double时访问uint64_t未定义的行为,因为编译器永远不会认为double类型的对象可以共享地址类型为uint64_t intro.object的对象:

  

除非对象是零字段或零大小的基类子对象,否则该对象的地址是它占用的第一个字节的地址。   如果一个嵌套在另一个中,或者如果至少一个是基类,那么具有重叠生命周期而非位字段的两个对象 a b 可能具有相同的地址零大小的子对象,它们是不同类型的;否则,他们有不同的地址。