C++ placement new 以定义构造顺序创建全局对象 - 这种用法是否正确?

C++ placement new to create global objects with defined construction order - Is this usage correct?

我正在使用 Arduino 框架。 为避免动态内存问题(堆下溢和堆栈溢出),Arduino 广泛使用全局对象。我认为这是很好的做法,我想继续使用这种模式。

同时,我想对那些全局对象使用依赖注入,即一些对象需要在其构造函数中注入其他对象。

但是在c++中构造全局对象没有定义的顺序。

为了克服这个问题,我想我可以使用放置新运算符并将对象构造到全局对象的内存中。我构建了一个模板,其唯一目的是为我想在全局内存 space.

中创建的任何类型 T 保留内存

为了保留实际内存 space,我有一个成员 buffer_,我将其声明为 long 的数组,以确保内存与任何对象完全对齐。 但这会导致有关对齐问题的警告。

另一方面,使用 char 的数组可以完美地工作而不会发出警告。 但我认为对于任何 T.

正确对齐的可能性要小得多

问题: 为什么字符数组显然正确对齐但长数组却不是?

以下代码显示了预订模板,第二个片段显示了如何使用它:

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

下面是模板的用法和放置新运算符:

//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()
{
    //...
}

您看到的警告与对齐无关,而是键入双关语。类型双关是指具有两个不同类型指针(long* 和 T*)的同一内存位置。

c++ language reference 开始,只有少数特殊类型编译器 不能 发出警告(char 是其中一种特殊类型):

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.

这解释了为什么编译器在缓冲区类型为 long[] 时发出警告,而在使用 char[].

时不发出警告

要将 char[] 正确对齐到 T 的对齐方式,您应该使用 alignas() 说明符。这将导致编译器将您的 char[] 对齐为 T。例如你的 ObjectMemory class 可以修改如下:

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