Reusing a float buffer for doubles without undefined behaviour

André Offringa 07/11/2018. 5 answers, 665 views
c++ strict-aliasing type-punning

In one particular C++ function, I happen to have a pointer to a big buffer of floats that I want to temporarily use to store half the number of doubles. Is there a method to use this buffer as scratch space for storing the doubles, which is also allowed (i.e., not undefined behaviour) by the standard?

In summary, I would like this:

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.;
}

As far as I see there's no easy way to do this: if I understand correctly, a reinterpret_cast<double*> like this causes undefined behaviour because of type aliasing, and using memcpy or a float/double union is not possible without copying the data and allocating extra space, which defeats the purpose and happens to be costly in my case (and using a union for type punning is not allowed in C++).

It can be assumed the float buffer is correctly aligned for using it for doubles.

5 Answers


phön 07/11/2018.

I think the following code is a valid way to do it (it is really just a small example about the idea):

#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;
}

It is important that you only use the pointer you receive from placement new. And it is important to placement new back the floats. Even if it is a no-operation construction, you need to start the lifetimes of the floats again.

Forget about std::launder and reinterpret_cast in the comments. Placement new will do the job for you.

edit: Make sure you have proper alignment when creating the buffer in main.


geza 07/12/2018.

You can achieve this in two ways.

First:

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
}

Second: Instead of float *, you need to allocate a "typeless" char[] buffer, and use placement new to put floats or doubles inside:

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);

Then use this accessor:

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;

A third way could be something like phön's answer (see my comments under that answer), unfortunately I cannot make a proper solution, because of this problem.


Paul Sanders 07/12/2018.

tl;dr Don't alias pointers - at all - unless you tell the compiler that you're going to on the command line.


The easiest way to do this might be to figure out what compiler switch disables strict aliasing and use it for the source file(s) in question.

Needs must, eh?


Thought about this some more. Despite all that stuff about placement new, this is the only safe way.

Why?

Well, if you have two pointers of different types pointing to the same address then you have aliased that address and you stand a good chance of fooling the compiler. And it doesn't matter how you assigned values to those pointers. The compiler is not going to remember that.

So this is the only safe way, and that's why we need std::pun.


Bathsheba 07/12/2018.

This problem cannot be solved in portable C++.

C++ is strict when it comes to pointer aliasing. Somewhat paradoxically this allows it to compile on very many platforms (for example where, perhaps double numbers are stored in different places to float numbers).

Needless to say, if you are striving for portable code then you'll need to recode what you have. The second best thing is to be pragmatic, accept it will work on any desktop system I've come across; perhaps even static_assert on compiler name / architecture.


Paul Sanders 07/13/2018.

Edit

I thought about this some more and it's not guaranteed to be safe for the reasons I have added to my original answer. So I'll leave the code here for reference, but I don't recommend you use it.

Instead, do what I suggest above. It's a shame, I rather like the code I wrote, despite the mysterious downvotes (beats the hell out of me, I thought I did a good job here).

Edit 2: (just for completeness, this post is already dead)

This solution will only work for primitive types and POD. This is deliberate, given the scope of the original question.


I thought I'd post a follow-up answer because @phön has found a better different solution than I did and I wanted to tidy it up a bit and throw in some ideas of my own.

Please note: This is a serious post. Just because I'm feeling a bit light-hearted today, doesn't mean I'm just fooling around.

Firstly, I would actually allocate the 'master' buffer using malloc(). This is because:

  • It gives me back a void *, which is actually the appropriate here. You'll see why in a minute.
  • It is marginally more efficient (although this is a detail).
  • I can control the alignment of the buffer if I need to (for SSE, say) with aligned_alloc.

There's not really a downside to this. If I want to manage it with a smart pointer, I can always use a custom deleter.

So why is that void * so wonderful? Because it stops me from doing the kind of thing phön did in his post, i.e. he was tempted to 'revert' the use of the buffer to an array of floats and I don't think that's wise.

Better, rather - it's certainly cleaner - to use placement new every time you want to treat the buffer as an array of Foo and then let that pointer go quietly out of scope when you are done with it. The overhead is minimal, certainly for POD types. In fact, I'd expect any decent compiler to optimise it away completely in this case, but I haven't tested that.

So you should, of course, wrap all of this up in a class, so let's do that. Then we don't need that custom deleter. Here goes.

The class:

#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;
};

Test program:

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);
}

Output:

m_buf is at 0x1e56c40

floats are at 0x1e56c40
We have 200 floats

doubles are at 0x1e56c40
We have 100 doubles

Live demo.

Written on my tablet, hard work!

Related questions

Hot questions

Language

Popular Tags