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

Different World flac

Alan Walker. 2018. Writer: Alan Walker;Fredrik Borch Olsen;James Njie;Marcus Arnbekk;Gunnar Greve Pettersen;K-391;Corsak;Shy Martin;Magnus Bertelsen.
2 Ariana Grande

​Thank U, Next flac

Ariana Grande. 2018. Writer: Crazy Mike;Scootie;Victoria Monét;Tayla Parx;TBHits;Ariana Grande.
3 Anne-Marie

Rewrite The Stars flac

Anne-Marie. 2018. Writer: Benj Pasek;Justin Paul.
4 Mesto

Wait Another Day flac

Mesto. 2018.
5 Fitz And The Tantrums

HandClap flac

Fitz And The Tantrums. 2017. Writer: Fitz And The Tantrums;Eric Frederic;Sam Hollander.
6 Conor Maynard

How You Love Me flac

Conor Maynard. 2018. Writer: Yoshi Breen;Thom Bridges;Hardwell;Rik Annema;Conor Maynard;Cimo Fränkel;Snoop Dogg.
7 Clean Bandit

Baby flac

Clean Bandit. 2018. Writer: Jack Patterson;Kamille;Jason Evigan;Matthew Knott;Marina;Luis Fonsi.
8 Rita Ora

Let You Love Me flac

Rita Ora. 2018. Writer: Rita Ora;Easyfun;Fred Gibson;Noonie Bao;LotusIV;Ilsey Juber.
9 (G)I-DLE

POP/STARS flac

(G)I-DLE. 2018. Writer: Riot Music Team;Harloe.
10 Mark Ronson

Nothing Breaks Like A Heart flac

Mark Ronson. 2018. Writer: Thomas Brenneck;Maxime Picard;Ilsey Juber;Conor Szymanski;Clement Picard;Mark Ronson;Miley Cyrus.
11 ZAYN

Good Years flac

ZAYN. 2018. Writer: Anthony Hannides;Michael George Hannides;Khaled Rohaim;ZAYN;Herbie Crichlow.
12 Big Boi

Out At Night flac

Big Boi. 2018. Writer: Jack Patterson;John Ryan;Julian Bunetta;Big Boi.
13 Imagine Dragons

Bad Liar flac

Imagine Dragons. 2018. Writer: Jorgen Odegard;Daniel Platzman;Ben McKee;Wayne Sermon;Aja Volkman;Dan Reynolds.
14 The Chainsmokers

Beach House flac

The Chainsmokers. 2018. Writer: Andrew Taggart.
15 Clean Bandit

Mama flac

Clean Bandit. 2018. Writer: Jason Evigan;Jack Patterson;Grace Chatto;Ellie Goulding;Caroline Ailin.
16 Ava Max

Sweet But Psycho flac

Ava Max. 2018. Writer: Ava Max;TIX;Cirkut;Madison Love;Cook Classics.
17 BTS

Waste It On Me (Slushii Remix) flac

BTS. 2018. Writer: Steve Aoki;Jeff Halavacs;Ryan Ogren;Michael Gazzo;Nate Cyphert;Sean Foreman;RM.
18 Bhad Bhabie

Playboy Style flac

Bhad Bhabie. 2018. Writer: Jack Patterson;Grace Chatto;Bhad Bhabie;George Astasio;Jason Pebworth;Jon Shave;Ryan Alan;Alex Oriet;David Phelan.
19 Cat Dealers

My Way flac

Cat Dealers. 2018.
20 Cmc

As Long As I'm With You flac

Cmc. 2018.

Related questions

Hot questions

Language

Popular Tags