9.2. 定位new与内存映射硬件

placement new(这是第7章讨论过的重要特性)有多种用途,但其中一个有趣的用途是:能将软件对象映射到内存映射硬件,从而让我们可以像操作软件一样驱动硬件。

要编写一个实际可用的示例会比较棘手,这会涉及“非可移植代码领域” —— 需要使用操作系统特定功能,来获取特定设备的地址,并讨论如何获取通常由软件驱动程序访问的内存位置的读写权限。因此,需要构建一个具有说明性的示例,并请读者们假设这个示例是完整的。

首先,假设正在为一块代号为“super_video_card”的新型显卡开发驱动程序。为了说明目的,用以下类来模拟这个显卡:

#include <cstdint>

class super_video_card {
  // ...
public:
  // 超级显卡寄存器
  volatile std::uint32_t r0{}, r1{}, r2{}, r3{};

  static_assert(sizeof(float) == 4); // sanity check

  volatile float f0{}, f1{}, f2{}, f3{};

  // 初始化显卡状态
  super_video_card() = default;

  super_video_card(const super_video_card&) = delete;
  super_video_card& operator=(const super_video_card&) = delete;

  // 可用于重置显卡状态
  ~super_video_card() = default;

  // 各种服务(为了简洁而省略)
};
// ...

这个类最重要的特性如下:

这个例子中为什么使用volatile变量?

若不熟悉volatile变量的用法,可能会疑惑为何要在内存映射硬件类中使用该限定符。其重要性在于:需要阻止编译器基于假设进行优化 —— 比如“若代码未主动修改这些变量,则其状态不会改变”,或“若代码写入后未立即读取,则可视为无效操作”。通过volatile限定,向编译器说明:“这些对象的状态变化超出你的认知范围,请勿擅自优化。”

为了简化实现,我们使用了一个将数据成员清零的构造函数和一个简单的析构函数。但在实际开发中,可以通过(默认或其他)构造函数将内存映射设备初始化为所需状态,并通过析构函数将设备重置到某个可接受状态。

通常,程序要访问内存映射硬件需要与操作系统交互,调用相关服务函数,并传入标识目标设备的参数,来获取其内存地址。在本例中,将模拟这种情况 —— 假设能直接访问一块具有正确大小和对齐要求、可读写的内存区域。操作系统函数通常会以void*类型返回原始内存地址,这也符合现实中的情况:

// 在内存中的某个位置,我们具有读/写访问权限,该位置对应着实际设备的内存映射硬件
alignas(super_video_card) char
  mem_mapped_device[sizeof(super_video_card)];

void* get_super_card_address() {
  return mem_mapped_device;
}
// ...

接下来将演示如何使用placement new将对象映射到内存映射硬件位置。需要包含<new>头文件,因为placement new在此定义。实现目标的具体步骤如下:

  1. 获取目标地址:首先取得待映射的super_video_card对象的目标硬件地址
  2. placement new构造:在目标地址通过placement new构造对象,确保其数据成员与硬件寄存器地址精确对应
  3. 对象生命周期管理:通过指针(代码中的the_card变量)操作该对象,直至其生命周期结束
  4. 显式析构替代删除:切勿对the_card调用operator delete()(内存非动态分配),但需显式调用析构函数~super_video_card()执行必要的清理/重置操作

最终实现如下:

// ...
#include <new>

int main() {
  // 将我对象映射到硬件上
  void* p = get_super_card_address();
  auto the_card =
    new(p) super_video_card{ /* 参数 */ };

  // 通过指针 the_card,使用实际的内存映射硬件
  // ...

  the_card->~super_video_card();
}

若显式调用析构函数存在风险(可能抛出异常的代码路径中),可采用带自定义删除器的std::unique_ptr(参见第5章)来安全终结super_video_card对象:

// ...
#include <new>
#include <memory>

int main() {
  // 将对象映射到硬件上
  void* p = get_super_card_address();

  std::unique_ptr<
    super_video_card,
    decltype([](super_video_card *p) {
      p->~super_video_card(); // do not call delete p!
    })
  > the_card {
    new(p) super_video_card{ /* args */ }
  };

  // 通过指针 the_card,使用实际的内存映射硬件
  // ...
  // 程序块结束时,unique_ptr 会自动调用其删除器,隐式调用 the_card->~super_video_card()
}

此实现中,std::unique_ptr仅负责终结所指对象(即super_video_card实例)的生命周期,而不会释放其内存存储空间。这种设计能确保即使the_card变量生命周期内发生异常,代码仍能保持健壮性。