C++ Volatile Placement New

17

如何在一个volatile指针上执行placement new操作。

例如,我想要做类似这样的事情:

volatile SomeStruct Object;
volatile SomeStruct* thing = &Object;
new (thing) SomeStruct(/*arguments to SomeStruct's constructor*/);

如果没有volatile关键字,我知道这将起作用......但是如果变量是volatile的,我该怎么做呢?

注意:

放置new定义如下:

void* operator new(size_t memoryRequested, void* pointer)
{
  return pointer;
}

(顺便说一下,这是GCC的实现方式):

// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }

问题在于我正在尝试将类型为 volatile SomeStruct*thing 转换为不允许的 void*

例如,如果我将 new 运算符更改为:

void* operator new(size_t memoryRequested, volatile void* pointer)
{
  return (void*)pointer;
} 

它可以编译,但会引发未定义的行为。


2
嗯...你不能这样做。 - Kerrek SB
7
你能否解释一下你为什么认为你需要这个? - Lightness Races in Orbit
5
我们都知道最终会发现这是一个被多个线程修改的变量,他认为volatile很重要。 - M.M
2
如果中断被禁用,则不需要使用“volatile”,因为不存在中断的可能性...你做错了。你链接的那个人之所以能够得逞,是因为他只在“char”上使用了volatile,而没有在更复杂的对象上使用。 - M.M
显示剩余22条评论
3个回答

8
我想说你可以这样做:
new (const_cast<SomeStruct*>(thing)) volatile SomeStruct(...);

但我并不确定这是否有效。问题在于,由于分配函数返回一个 void* 用于构造 volatile SomeStruct 对象,对内存的访问可能没有易失性语义,从而导致未定义行为。
因此,我不确定是否可以使用放置 new 在易失性限定的内存块中构造对象。但是,假设内存最初是一个非易失性的 char 数组,则这似乎是正确的解决方案。

2
标准可能根本不涉及这个问题, volatile 限定符用于限定对象,但对象在构造函数完成之前不存在。 - M.M
1
@M.M 如果你有一个volatile char数组,即使它们还没有被初始化(在这种情况下它们只是具有不确定值),你也有volatile char对象。但是,放置new函数很有趣,它似乎免除了其他情况适用的某些规则。所以我真的不确定。 - Brian Bi
@Brian:不,我不这么认为。你结束了先前占据该空间的对象的生命周期,但是如果你结束空间本身(即char数组)的生命周期,那么你重新使用它来创建新对象就不会被很好地定义!但我始终觉得放置新规则在最好的情况下是部分模糊的,所以呃 :( - Lightness Races in Orbit
@LightnessRacesinOrbit 不,你不会结束空间的生命周期,你将结束占据该空间的 char 数组(被视为一个完整的对象)的生命周期,然后该空间将不再具有类型,因为那里已经没有对象了。但是我同意你的观点,这是标准中规范性较差的领域之一,我不能确定自己是否正确。 - Brian Bi
即使这是有效的,仍然存在一个问题,即构造函数不能是volatile,因此对象构造函数将通过非易失性lvalue写入易失性空间而导致UB。 - M.M
显示剩余7条评论

7
我知道如果没有volatile关键字,这将起作用......但我如何使用volatile变量完成此操作?
放置new涉及在给定位置构建对象。 cv-qualifiers仅在对象构造后应用。 constvolatile只适用于对象构造后。从C ++标准(草案)[class.ctor / 3]在此处

可以为constvolatileconst volatile对象调用构造函数。在正在构建的对象上不应用constvolatile语义([dcl.type.cv])。当最终导出对象([intro.object])的构造函数结束时,它们会生效。

任何试图去除volatile的尝试都会导致未定义行为,请参见cppreference here

通过非const访问路径修改const对象并通过非volatile glvalue引用volatile对象会导致未定义行为。

另请参见[expr.const.cast / 6]
考虑到volatile和放置new的使用,问题中(以及一些注释)的断言是需要将对象用于信号处理程序并映射到内存中的特定位置。
不过还有一些替代方案......
如果不需要特定位置,则最好不要使用放置new,而只需在声明对象的位置添加volatile限定符;
struct SomeStruct {
    /*...*/
};
// ...
volatile SomeStruct Object;

如果需要同时使用放置 newvolatile,请重新排序它们的使用方式。按照需求构建对象,然后添加限定符;
SomeStruct Object;
// ...
void* p = &Object; // or at the required location
volatile SomeStruct* p2 = new (p) SomeStruct;
< p >这个结构体必须是易失性的吗?结构体volatile部分可以被内部化/抽象化,数据的cv-qualifiers不需要一开始就暴露给客户端,这将在struct内部处理;

struct SomeStruct {
    volatile int data;
    void DoSomething()
    {
        data = 42;
    }
};

SomeStruct Object;
/* ... */
void* p = &Object;
auto p2 = new (p) SomeStruct{};
p2->DoSomething();

内部化易失性对象的初始化,另一种选择是允许SomeStruct根据需要进行惰性初始化(或重新初始化/重置)。考虑到某些明显的限制,这可能不太可行。
struct SomeStruct {
    void Initialise() volatile
    {
        /*...*/
    }
}

0

我认为这可能会对你正在尝试实现的内容有所帮助。现在,我向您展示的模板类是使用Windows平台锁定线程编写的,您可以根据需要修改此类以适用于其他操作系统-平台。它仅用作说明如何实现上述语义的示例。这个类可以在Visual Studio 2015 CE中编译、运行并以0的代码退出。该类依赖于<Windows.h>头文件,用于使用CRITICAL_SECTIONEnterCriticalSection()LeaveCriticalSection()InitializeCriticalSection()DeleteCriticalSection()。如果其他库(如boost库)中有这些的替代方法,则可以轻松编写此类以实现相同的功能。该类旨在在跨多个线程工作时将用户定义的类对象锁定为易失性。

VolatileLocker.h

#ifndef VOLATILE_LOCKER_H
#define VOLATILE_LOCKER_H

#include <Windows.h>

template<typename T>
class VolatileLocker {
private:
    T*  m_pObject;
    CRITICAL_SECTION* m_pCriticalSection;

public:
    VolatileLocker( volatile T& objectToLock, CRITICAL_SECTION& criticalSection );
    ~VolatileLocker();

    T* operator->();

private:
    VolatileLocker( const VolatileLocker& c ); // Not Implemented
    VolatileLocker& operator=( const VolatileLocker& c ); // Not Implemented

}; // VolatileLocker

#include "VolatileLocker.inl"

#endif // VOLATILE_LOCKER_H

VolatileLocker.inl

// ----------------------------------------------------------------------------
// VolatileLocker()
// Locks A Volatile Variable So That It Can Be Used Across Multiple Threads Safely
template<typename T>
VolatileLocker<T>::VolatileLocker( volatile T& objectToLock, CRITICAL_SECTION& criticalSection ) :
    m_pObject( const_cast<T*>( &objectToLock ) ),
    m_pCriticalSection( &criticalSection ) {
    EnterCriticalSection( m_pCriticalSection );
} // VolatileLocker

// ----------------------------------------------------------------------------
// ~VolatileLocker()
template<typename T>
VolatileLocker<T>::~VolatileLocker() {
    LeaveCriticalSection( m_pCriticalSection );
} // ~VolatileLocker

// ----------------------------------------------------------------------------
// operator->()
// Allow The Locked Object To Be Used Like A Pointer
template <typename T>
T* VolatileLocker<T>::operator->() {
    return m_pObject;
} // operator->

VolatileLocker.cpp

#include "VolatileLocker.h"

现在这里是主要的运行应用程序,它使用了模板化的易失性锁类和放置新操作符。

#include <iostream>
#include "VolatileLocker.h"

static CRITICAL_SECTION s_criticalSection;

class SomeClass {
private:
    int m_value;

public:
    explicit SomeClass( int value ) : m_value( value ) {}

    int getValue() const { return m_value; }

}; // SomeClass

int main() {
    InitializeCriticalSection( &s_criticalSection ); // Initialize Our Static Critical Section

    SomeClass localStackObject( 2 ); // Create A Local Variable On The Stack And Initialize It To Some Value

    // Create A Pointer To That Class And Initialize It To Null.
    SomeClass* pSomeClass = nullptr;
    // Not Using Heap Here, Only Use Local Stack For Demonstration, So Just Get A Reference To The Stack Object
    pSomeClass = &localStackObject;

    // Here Is Our Pointer / Reference To Our Class As A Volatile Object 
    // Which Is Also Locked For Thread Safety Across Multiple Threads
    // And We Can Access The Objects Fields (public variables, methods) via
    // the VolatileLocker's overloaded ->() operator.
    std::cout << VolatileLocker<SomeClass>( *pSomeClass, s_criticalSection )->getValue() << std::endl;

    // Placement New Operator On Our Pointer To Our Object Using The Class's Constructor
    new (pSomeClass) SomeClass( 4 );

    // Again Using The Volatile Locker And Getting The New Value.
    std::cout << VolatileLocker<SomeClass>( *pSomeClass, s_criticalSection )->getValue() << std::endl;

    // Here Is The Interesting Part - Let's Check The Original Local Stack Object
    std::cout << localStackObject.getValue() << std::endl;

    // Cleaning Up Our Critical Section.
    DeleteCriticalSection( &s_criticalSection );
    return 0;
} // main

输出

2
4
4

注意:

需要注意的是,初始本地堆栈变量本身并不是易失性的。如果您尝试将堆栈变量声明为易失性,并直接使用它:

volatile SomeClass localStackObject( 2 );
SomeClass* pSomeClass = nullptr;
pSomeClass = &localStackObject; // Invalid - volatile SomeClass* cannot be assigned to an entity of type SomeClass*

如果你试图通过直接使用易失性局部变量来解决这个问题,你仍然可以将它与VolatileLocker一起使用,但你将无法像这个片段显示的那样使用放置new。

std::cout << VolatileLocker<SomeClass>( localStackObject, s_criticalSection )->getValue() << std::endl; // Line Okay - Notice using object directly and no dereferencing.

// However when we get to this line of code here:
new (localStackObject) SomeClass( 4 ); // Does Not Compile. There Is No Instance Of Operator New To Match The Argument List

// To Fix That We Can Do This:
new ( const_cast<SomeClass*>( &localStackObject) ) SomeClass( 4 ); // This Will Compile

然而,使用这种设计方法访问任何成员时,您必须使用VolatileLocker来访问类的方法,因此不能直接使用localStackObject。

// This Is Invalid:
std::cout << localStackObject.getValue() << std::endl; 

// Use This Instead:   
std::cout << VolatileLocker<SomeClass>( localStackObject, s_criticalSection )->getValue() << std::endl;

作为一个重要的提示,请注意这个类最初是针对具体的Windows平台设计的,然而,只需将CRITICAL_SECTION替换为任何可用的跨平台等效函数,就可以轻松地以跨平台模块化的方式编写此模板类的概念。

以下是在Linux/Unix系统上工作的参考答案: stackoverflow/multithreading/linux

以下是在Mac/Apple系统上工作的参考答案: stackoverflow/multithreading/mac

以下是编写跨平台模块化等效项的参考资料:

  1. cppreference/thread
  2. cppreference/condition_variable

线程安全和易失性变量是有区别的......我不认为这对我有用,因为我在嵌入式设备上......它没有线程等概念......但是由于中断,我确实需要一个易失性变量。 - DarthRubik
哦,好的;但是模板类仍然保留了指向传递到其构造函数中的对象类的指针,该构造函数采用易失类型的引用或地址。该类的设计旨在实现线程安全,因为它用于OpenGL应用程序。我相信有方法可以修改此类以适合您的需求。如果我有更多时间,我可以研究一下面向嵌入式设备的OpenGL API,看看我能找到什么。我不是100%确定,但可能有一种方法可以使用c++源代码中的asm代码块来完成这个任务。(...more) - Francis Cugler
在我的源代码中,我使用这个类在我的OpenGL Shader引擎中,我的Game类包含一个GameState对象类型的volatile变量。由于游戏在多个状态之间切换,比如菜单,加载,游戏玩法,积分等等,并且我设置了OpenGL和其他对象以支持多线程,因此我使用这个类来获取或更改属于Game Class对象的GameState,其中gameState是volatile的,所以我最终传递指向我的Game类的指针以检索游戏状态对象,但我没有在我的代码中使用Placement New。 - Francis Cugler
我对你的问题很感兴趣,这让我开始思考如何实现它。 - Francis Cugler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接