Home > front end >  C placement new to create global objects with defined construction order - Is this usage correct?
C placement new to create global objects with defined construction order - Is this usage correct?

Time:04-23

I am using the Arduino framework. To avoid issues with dynamic memory (heap-underflow as well as stack-overflow), Arduino works widely with global objects. I think that is good practice and I want to continue working with this pattern.

At the same time, I want to use dependency injection for those global objects, i.e. some objects need other objects injected in their constructor.

But there is no defined order in constructing global objects in c .

To overcome that, I figured I could use the placement new operator and construct the objects into memory of global objects. I constructed a template with the sole purpose of reserving memory for any type T which I want to create in global memory space.

To reserve the actual memory space, I have a member buffer_ which I declared as an array of long to make sure the memory is perfectly aligned for any objects. But this causes warnings about alignment issues.

Using an array of char on the other hand works perfectly without warning. But I think it is much less likely to be correctly aligned for any T.

The question: Why is an array of chars apparently correctly aligned but an array of long is not?

The following code shows the reservation template and the second snipped shows how to use it:

#include <memory>

template<class T>
class ObjectMemory
{
    //long buffer_[(sizeof(T)   sizeof(long)-1)/sizeof(long)];//make sure it is aligned for long (=biggest size)
    //=> but this line creates warnings regarding alignments: 
    // warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
    //     T& operator *()  { return reinterpret_cast<T&>(*buffer_); }
    //                                                     ^~~~~~
    char buffer_[sizeof(T)]; //this line compiles without warning but looks to me less aligned than an array of longs.

public:
    ObjectMemory() = default;
    
    ObjectMemory( const ObjectMemory &c ) = delete;
    ObjectMemory& operator=( const ObjectMemory &c ) = delete;
    
    T& operator  *() { return reinterpret_cast<T&>(*buffer_); }
    T* operator ->() { return reinterpret_cast<T*>( buffer_); }
       operator T&() { return reinterpret_cast<T&>(*buffer_); }
    void destroyObject(){(reinterpret_cast<T*>(buffer_))->~T();}
};

template<class T>
void * operator new(size_t cbsize, ObjectMemory<T>& objectMemory)
{
    if(cbsize > sizeof(T))
    {
        while(true){}      //alternatively return nullptr; (will cause warnings)
    }
    return &objectMemory;
}

And here is the usage of the template and the placement new operator:

//global objects
ObjectMemory<Driver_Spi> spi;
ObjectMemory<Driver_DigitalPin> ACS_BoardAdcSpiCs;
ObjectMemory<Driver_InternalRtc> rtc;
ObjectMemory<Driver_BoardAdc> boardAdc;
ObjectMemory<Dcf77Decoder> dcf77Decoder;

// The setup() function runs once each time the micro-controller starts
void setup()
{
    //...
    // now call the constructors in correct order:

    new (spi)                   Driver_Spi();
    new (ACS_BoardAdcSpiCs)     Driver_DigitalPin(PIN_5);//CSA
    new (rtc)                   Driver_InternalRtc();
    new (boardAdc)              Driver_BoardAdc(spi, ACS_BoardAdcSpiCs);
    new (dcf77Decoder)          Dcf77Decoder(rtc, PIN_0); //DCF77_INPUT (with interrupt)
 
    //...
    boardAdc->init();
}

void loop()
{
    //...
}

CodePudding user response:

To answer the question in the title - yes from my point of view it is a great way to use C in such restricted environments. Actually, I also use the same approach for C kernel development on Windows

Regarding second - alignment is very tricky and depends on the platform for which your code is compiled. If you are not going to serialize objects and deserialize them on some different architecture just pass the alignment job to compiler (use char/uchar) it will do the right things.

CodePudding user response:

The warning that you're seeing isn't related to alignment, but instead type punning. Type punning is referring to the same memory location with two differently typed pointers (long* and T*).

From the c language reference there are only a few special types where the compiler can't issue a warning (char is one of those special types):

Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined unless one of the following is true:

  • AliasedType and DynamicType are similar.
  • AliasedType is the (possibly cv-qualified) signed or unsigned variant of DynamicType.
  • AliasedType is std::byte, (since C 17) char, or unsigned char: this permits examination of the object representation of any object as an array of bytes.

This explains why the compiler warns when the buffer is typed as a long[] and doesn't warn when using a char[].

To properly align the char[] to T's alignment you should make use of the alignas() specifier. This will cause the compiler to align your char[] as though it were a T. For example your ObjectMemory class could be modified as follows:

template<class T>
class ObjectMemory
{
    alignas(T) char buffer_[(sizeof(T)];

public:
    ObjectMemory() = default;
    
    ObjectMemory( const ObjectMemory &c ) = delete;
    ObjectMemory& operator=( const ObjectMemory &c ) = delete;
    
    T& operator  *() { return reinterpret_cast<T&>(*buffer_); }
    T* operator ->() { return reinterpret_cast<T*>( buffer_); }
       operator T&() { return reinterpret_cast<T&>(*buffer_); }
    void destroyObject(){(reinterpret_cast<T*>(buffer_))->~T();}
};
  • Related