Reusing a float buffer for doubles without undefined behaviour

André Offringa 07/11/2018. 6 answers, 887 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.

6 Answers


phön 07/18/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.

Update:

I just wanted to give an update on things that were discussed in the comments.

  1. The first thing mentioned was that we may need to update the initially created float pointer to the pointer returned by the re-placement-new'ed floats (the question is whether the initially float pointer can still be used to access the floats, because the floats are now "new" floats obtained by an additional new expression).

To do this, we can either a) pass the float pointer by reference and update it, or b) return the new obtained float pointer from the function:

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. The next and more crucial thing to mention is that placement-new is allowed to have a memory overhead. So the implementation is allowed to place some meta data infront of the returned array. If that happens, the naive calculation of how many doubles would fit into our memory will be obviously wrong. The problem is, that we dont know how many bytes the implementation will aquire beforehand for the specific call. But that would be nessessary to adjust the amounts of doubles we know will fit into the remaining storage. Here ( https://stackoverflow.com/a/8721932/3783662 ) is another SO post where Howard Hinnant provided a test snippet. I tested this using an online compiler and saw that for trivial destructable types (for example doubles), the overhead was 0. For more complex types (for example std::string), there was an overhead of 8 bytes. But this may varry for your plattform/compiler. Test it beforehand with the snippet by Howard.

  2. For the question why we need to use some kind of placement new (either by new[] or single element new): We are allowed to cast pointers in every way we want. But in the end - when we access the value - we need to use the right type to avoid voilating the strict aliasing rules. Easy speaking: its only allowed to access an object when there is really an object of the pointer type living in the location given by the pointer. So how do you bring objects to life? the standard says:

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

"An object is created by a definition, by a new-expression, when implicitly changing the active member of a union, or when a temporary object is created."

There is an additional sector which may seem interesting:

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

"An object is said to have non-vacuous initialization if it is of a class or aggregate type and it or one of its subobjects is initialized by a constructor other than a trivial default constructor. The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • if the object has non-vacuous initialization, its initialization is complete"

So now we may argue that because doubles are trivial, do we need to take some action to bring the trivial objects to life and change the actual living objects? I say yes, because we initally obtained storage for the floats, and accessing the storage through a double pointer would violate strict aliasing. So we need the tell the compiler that the actual type has changed. This whole last point 3 was pretty controversial discussed. You may form your own opinion. You have all the information at hand now.


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.


Maxpm 07/18/2018.

Here's an alternative approach that's less scary.

You say,

...a float/double union is not possible without...allocating extra space, which defeats the purpose and happens to be costly in my case...

So just have each union object contain two floats instead of one.

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

Of course, this makes indexing more complicated, and calling code will have to be modified. But it has no overhead and it's more obviously correct.


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!


HighResolutionMusic.com - Download Hi-Res Songs

1 Alan Walker

On My Way flac

Alan Walker. 2019. Writer: Alan Walker;Sabrina Carpenter;Farruko.
2 CHVRCHES

Here With Me flac

CHVRCHES. 2019. Writer: Steve Mac;Martin Doherty;Marshmello;Lauren Mayberry;Iain Cook.
3 5 Seconds Of Summer

Who Do You Love flac

5 Seconds Of Summer. 2019. Writer: Andrew Taggart;Talay Riley;Oak;Sean Douglas;Luke Hemmings;Calum Hood;Ashton Irwin;Michael Clifford;Trevorious;Zaire Koalo.
4 Bonn

No Sleep flac

Bonn. 2019. Writer: Albin Nedler;Bonn;Martin Garrix.
5 Avril Lavigne

Crush flac

Avril Lavigne. 2019. Writer: Johan Carlsson;Avril Lavigne;Zane Carney.
6 Katy Perry

365 flac

Katy Perry. 2019. Writer: Zedd;Katy Perry;Caroline Ailin;Corey Sanders;Daniel Davidsen;Cutfather;Peter Wallevik.
7 Alan Walker

Are You Lonely flac

Alan Walker. 2019.
8 Jonas Brothers

Sucker flac

Jonas Brothers. 2019. Writer: Kevin Jonas;Joe Jonas;Nick Jonas;Ryan Tedder;Louis Bell;Frank Dukes.
9 Brooks

Better When You're Gone flac

Brooks. 2019. Writer: David Guetta;Emma Lov Block;Ido Zmishlany;Jackson Foote;Jeremy Dussolliet;Brooks.
10 Dido

Hurricanes flac

Dido. 2019. Writer: Dido;Rick Nowels;Rollo Armstrong.
11 DEAMN

Happy flac

DEAMN. 2019.
12 Ariana Grande

Bloodline flac

Ariana Grande. 2019. Writer: ILYA;Max Martin;Savan Kotecha;Ariana Grande.
13 IZ*ONE

Rise flac

IZ*ONE. 2019.
14 Avril Lavigne

Dumb Blonde flac

Avril Lavigne. 2019. Writer: Mitch Allan;Bonnie McKee;Nicki Minaj;Avril Lavigne.
15 Little Big Town

Don't Threaten Me With A Good Time flac

Little Big Town. 2019. Writer: Thomas Rhett;Karen Fairchild;The Stereotypes;Jesse Frasure;Ashley Gorley.
16 Ariana Grande

Make Up flac

Ariana Grande. 2019. Writer: Brian Malik Baptiste;Tayla Parx;TBHits;Victoria Monét;Ariana Grande.
17 Dzeko

Halfway There flac

Dzeko. 2019.
18 Ariana Grande

Imagine flac

Ariana Grande. 2019. Writer: JProof;Priscilla Renea;Happy Perez;Andrew "Pop" Wansel;Ariana Grande.
19 Ariana Grande

NASA flac

Ariana Grande. 2019. Writer: Ariana Grande;Scootie;Tayla Parx;TBHits;Victoria Monét.
20 Ariana Grande

Thank U, Next flac

Ariana Grande. 2019. Writer: Crazy Mike;Scootie;Victoria Monét;Tayla Parx;TBHits;Ariana Grande.

Related questions

Hot questions

Language

Popular Tags