首页 > 解决方案 > 在没有未定义行为的情况下重用双精度浮点缓冲区

问题描述

在一个特定的 C++ 函数中,我碰巧有一个指向浮点数的大缓冲区的指针,我想暂时使用它来存储一半的双精度数。有没有一种方法可以将此缓冲区用作存储双打的临时空间,标准也允许(即不是未定义的行为)?

总之,我想要这样:

void f(float* buffer)
{
  double* d = reinterpret_cast<double*>(buffer);
  // make use of d
  d[i] = 1.;
  // done using d as scratch, start filling the buffer
  buffer[j] = 1.;
}

据我所知,没有简单的方法可以做到这一点:如果我理解正确,由于类型别名和使用或浮点/双精度reinterpret_cast<double*>,这样会导致未定义的行为memcpyunion如果不复制数据并分配额外的空间,就无法,这会失败在我的情况下,目的和恰好是昂贵的(并且在 C++ 中不允许使用联合进行类型双关语)。

可以假设浮点缓冲区已正确对齐以将其用于双精度。

标签: c++strict-aliasingtype-punning

解决方案


我认为下面的代码是一种有效的方法(它实际上只是一个关于这个想法的小例子):

#include <memory>

void f(float* buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];

    // we have started the lifetime of the doubles.
    // "d" is a new pointer pointing to the first double object in the array.        
    // now you can use "d" as a double buffer for your calculations
    // you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed"       
    d[0] = 1.;
    // do some work here on/with the doubles...


    // conceptually we need to destory the doubles here... but they are trivially destructable

    // now we need to start the lifetime of the floats again
    new (buffer) float[10];  


    // here we are unsure about wether we need to update the "buffer" pointer to 
    // the one returned by the placement new of the floats
    // if it is nessessary, we could return the new float pointer or take the input pointer
    // by reference and update it directly in the function
}

int main()
{
    float* floats = new float[10];
    f(floats, sizeof(float) * 10);
    return 0;
}

重要的是您只使用从放置新收到的指针。并且重要的是放置新的花车。即使是无操作构造,您也需要重新开始浮动的生命周期。

忘记std::launderreinterpret_cast评论。新的安置将为您完成这项工作。

编辑:确保在 main 中创建缓冲区时正确对齐。

更新:

我只是想更新评论中讨论的内容。

  1. 提到的第一件事是我们可能需要将最初创建的浮点指针更新为重新放置新的浮点返回的指针(问题是最初的浮点指针是否仍然可以用于访问浮点,因为浮点数现在是通过附加新表达式获得的“新”浮点数)。

为此,我们可以 a) 通过引用传递浮点指针并更新它,或者 b) 从函数返回新获得的浮点指针:

一个)

void f(float*& buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];    
    // do some work here on/with the doubles...
    buffer = new (buffer) float[10];  
}

b)

float* f(float* buffer, std::size_t buffer_size_in_bytes)
{
    /* same as inital example... */
    return new (buffer) float[10];  
}

int main()
{
    float* floats = new float[10];
    floats = f(floats, sizeof(float) * 10);
    return 0;
}
  1. 接下来要提到的更重要的一点是placement-new 允许有内存开销。因此允许实现在返回的数组前面放置一些元数据。如果发生这种情况,那么天真地计算有多少双打会适合我们的记忆显然是错误的。问题是,我们不知道实现将预先为特定调用获取多少字节。但这对于调整我们知道将适合剩余存储空间的双打数量是必要的。在这里(https://stackoverflow.com/a/8721932/3783662) 是另一个 SO 帖子,其中 Howard Hinnant 提供了一个测试片段。我使用在线编译器对此进行了测试,发现对于普通的可破坏类型(例如双精度数),开销为 0。对于更复杂的类型(例如 std::string),开销为 8 个字节。但这可能因您的平台/编译器而异。使用 Howard 的代码片段预先对其进行测试。

  2. 对于为什么我们需要使用某种放置 new 的问题(通过 new[] 或单个元素 new):我们可以以任何我们想要的方式转换指针。但最后——当我们访问值时——我们需要使用正确的类型以避免违反严格的别名规则。简单来说:只有在指针给定的位置确实存在指针类型的对象时才允许访问对象。那么如何让物体栩栩如生?标准说:

https://timsong-cpp.github.io/cppwp/intro.object#1

“当隐式更改联合的活动成员或创建临时对象时,通过定义、新表达式创建对象。”

还有一个看起来很有趣的额外部门:

https://timsong-cpp.github.io/cppwp/basic.life#1

“如果一个对象是一个类或聚合类型,并且它或它的一个子对象由一个普通的默认构造函数以外的构造函数初始化,则称它具有非空初始化。类型 T 的对象的生命周期开始于:

  • 获得具有适合类型 T 的对齐和大小的存储,并且
  • 如果对象具有非空初始化,则其初始化完成”

所以现在我们可能会争辩说,因为替身是微不足道的,我们是否需要采取一些行动来使微不足道的对象栩栩如生并改变实际的生命对象?我说是的,因为我们最初获得了浮点数的存储空间,并且通过双指针访问存储空间会违反严格的别名。所以我们需要告诉编译器实际类型已经改变。整个最后一点 3 是相当有争议的讨论。你可以形成你自己的意见。您现在掌握了所有信息。


推荐阅读