在没有不确定行为的情况下将浮点缓冲区重用为doubles

问题描述 投票:25回答:6

在一个特定的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*>会导致未定义的行为,并且无法使用memcpy或浮点/双精度union而不复制数据并分配额外的空间,这违背了目的,在我的情况下碰巧成本很高(并且在C ++中不允许使用联合进行类型调整)。

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

c++ strict-aliasing type-punning
6个回答
10
投票

我认为以下代码是实现此目的的一种有效方法(它实际上只是有关该想法的一个小示例):

#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)从函数中返回新获得的浮点指针:

a)

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. 接下来要提到的更关键的一点是,允许新放置的对象具有内存开销。因此,允许该实现在返回数组的前面放置一些元数据。如果发生这种情况,那么天真地计算出多少双打会适合我们的记忆显然是错误的。问题是,我们不知道该实现将为特定的调用事先获取多少字节。但这对于调整我们知道将适合剩余存储空间的double的数量是必要的。此处(https://stackoverflow.com/a/8721932/3783662)是Howard Hinnant提供测试摘要的另一篇SO帖子。我使用在线编译器对此进行了测试,发现对于普通的可破坏类型(例如,doubles),开销为0。对于更复杂的类型(例如,std :: string),开销为8个字节。但是,这可能会影响您的平台/编译器。预先用Howard的代码片段进行测试。

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

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

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

还有一个看起来有趣的附加扇区:

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

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

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

因此,现在我们可能会争辩说,由于双打是微不足道的,我们是否需要采取一些措施使微不足道的物体栩栩如生并改变实际的生物?我说是的,因为我们最初是为浮点数获取存储的,而通过双指针访问存储将违反严格的别名。因此,我们需要告诉编译器实际类型已更改。讨论的最后3点颇具争议。您可以发表自己的看法。您现在掌握了所有信息。


7
投票

您可以通过两种方式实现。

第一:

void set(float *buffer, size_t index, double value) {
    memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double));
}
double get(const float *buffer, size_t index) {
    double v;
    memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double));
    return v;
}
void f(float *buffer) {
    // here, use set and get functions
}

第二:代替float *,您需要分配一个“无类型” char[]缓冲区,并使用new放置将浮点数或双精度数放入其中:

template <typename T>
void setType(char *buffer, size_t size) {
    for (size_t i=0; i<size/sizeof(T); i++) {
        new(buffer+i*sizeof(T)) T;
    }
}
// use it like this: setType<float>(buffer, sizeOfBuffer);

然后使用此访问器:

template <typename T>
T &get(char *buffer, size_t index) {
    return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T)));
}
// use it like this: get<float>(buffer, index) = 33.3f;

第三种方式可能类似于phön的答案(请参阅我在该答案下的评论),不幸的是,由于this problem,我无法提出适当的解决方案。


2
投票

这是一种不那么令人恐惧的替代方法。

您说,

...如果没有...分配额外的空间,则不可能实现浮点/双精度联合,这超出了目的,在我看来是昂贵的...

所以每个联合对象都包含两个而不是一个浮点数。

static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double.");
union double_or_floats
{
    double d;
    float f[2];
};

void f(double_or_floats* buffer)
{
    // Use buffer of doubles as scratch space.
    buffer[0].d = 1.0;
    // Done with the scratch space.  Start filling the buffer with floats.
    buffer[0].f[0] = 1.0f;
    buffer[0].f[1] = 2.0f;
}

当然,这会使索引编制变得更加复杂,并且必须修改调用代码。但这没有开销,而且显然很正确。


1
投票

tl; dr完全不使用别名别名,除非您告诉编译器您将在命令行中使用。


最简单的方法可能是找出哪个编译器开关禁用严格的别名并将其用于所涉及的源文件。

需要,是吗?


对此有更多思考。尽管有关于放置新内容的所有内容,但这是唯一安全的方法。

为什么?

嗯,如果您有两个指向相同地址的不同类型的指针,那么您已经对该地址进行了别名,并且很有可能欺骗编译器。 而且您如何为这些指针分配值也没关系。编译器不会记住这一点。

所以这是唯一安全的方法,这就是为什么我们需要std::pun


0
投票

此问题在便携式C ++中无法解决。

C ++在指针别名方面很严格。有点自相矛盾的是,它允许它在很多平台上进行编译(例如,double编号与float编号存储在不同的位置)。

不用说,如果您正在争取可移植的代码,那么您将需要重新编码所拥有的内容。第二件事是务实,接受它可以在我遇到的任何桌面系统上运行;甚至是关于编译器名称/体系结构的static_assert


-3
投票

编辑

我考虑了更多,由于我添加到原始答案中的原因,因此不能保证它是安全的。因此,我将在此处保留代码以供参考,但我不建议您使用它。

相反,请执行我上面的建议。真可惜,我更喜欢我编写的代码。我以为我在这里做得很好。

编辑2:(仅出于完整性考虑,此帖子已经死亡)

此解决方案仅适用于原始类型和POD。考虑到原始问题的范围,这是有意的。


我以为我会发布后续答案,因为@phön找到了一个比我更好的解决方案,我想整理一下并提出一些自己的想法。首先,我实际上将使用malloc()分配“主”缓冲区。这是因为:

    它给了我void *,实际上在这里是合适的。一分钟后您会明白为什么。
  • 虽然效率更高(尽管这是一个细节)。但是]
  • 如果需要(例如,对于SSE,我可以用aligned_alloc来控制缓冲区的对齐方式。
  • 确实没有不利之处。如果我想使用智能指针进行管理,则可以随时使用自定义删除器。
  • 所以void *为什么这么好?

    [因为它阻止我去做phön在他的帖子中所做的事情

,即他很想将缓冲区的使用“还原”为float数组,我认为这不是明智的。 [更好,它确实更干净-每次将缓冲区视为new数组时都使用放置Foo,然后在完成处理后让该指针悄悄地超出范围。对于POD类型,开销是最小的。实际上,在这种情况下,我希望任何不错的编译器都能完全对其进行优化,但是我还没有对此进行测试。

因此,您当然应该将所有这些都包装在一个类中,所以让我们开始吧。然后,我们不需要该自定义删除器。来。

班级:

#include <cstdlib> #include <new> #include <iostream> class SneakyBuf { public: SneakyBuf (size_t bufsize, size_t alignment = 8) : m_bufsize (bufsize) { m_buf = aligned_alloc (alignment, bufsize); if (m_buf == nullptr) throw std::bad_alloc (); std::cout << std::hex << "m_buf is at " << m_buf << "\n\n"; } ~SneakyBuf () { free (m_buf); } template <class T> T* Cast (size_t& count) { count = m_bufsize / sizeof (T); return new (m_buf) T; // no need for new [] here } private: size_t m_bufsize; void *m_buf; };

测试程序:

void do_float_stuff (SneakyBuf& sb) { size_t count; float *f = sb.Cast <float> (count); std::cout << std::hex << "floats are at " << f << "\n"; std::cout << std::dec << "We have " << count << " floats\n\n"; f [0] = 0; // ... } void do_double_stuff (SneakyBuf& sb) { size_t count; double *d = sb.Cast <double> (count); std::cout << std::hex << "doubles are at " << d << "\n"; std::cout << std::dec << "We have " << count << " doubles\n"; d [0] = 0; // ... } int main () { SneakyBuf sb (100 * sizeof (double)); do_float_stuff (sb); do_double_stuff (sb); }

输出:

m_buf is at 0x1e56c40 floats are at 0x1e56c40 We have 200 floats doubles are at 0x1e56c40 We have 100 doubles

Live demo

写在我的平板电脑上,辛苦了!

© www.soinside.com 2019 - 2024. All rights reserved.