C++ PMR完整实践:复用 C 定长池
先掌握指针、基本所有权和分离编译;C++部分另需RAII与标准容器。示例未执行;本文仅接受文本、JSON、哈希及Markdown模板验证,没有执行原生工具链。
专题入口:C++ PMR:资源协议与上游分配 · C++ PMR:传播、等价与资源生存期 · C++ PMR完整实践:复用 C 定长池 · C++ PMR排错:悬空资源、错误归还与误测
C++PMR客户端怎样串联C定长池
C++PMR客户端怎样串联C定长池:先构造slab_resource,它创建C池,再创建绑定资源的std::pmr::list;逐个插入1至6,遍历求和,退出内部作用域销毁list,最后检查live为0。C库不认识list节点类型,资源只提供原始存储;这种分工允许不同标准库节点尺寸通过上游兼容。
C++PMR项目的全部源码放在哪里
C++PMR项目的全部源码放在哪里:本页所有代码由 examples/native/p2-allocators 同源引用,README给出环境、输入输出和生命周期前提。C实践页展示完全相同文件;修改实现必须只改示例目录,再让两个页面自动展示同一内容。TEST-SPEC记录未实现注入,不把文档清单伪装成已经运行的测试。
C++PMR怎样构造真正有界的失败案例
C++PMR怎样构造真正有界的失败案例:把null_memory_resource传作上游,一个槽首次申请成功,第二次相同申请必须抛bad_alloc,第一块与计数保持有效。默认new_delete_resource允许额外内存,只有池本身有界;不能把默认资源的总内存宣称为固定上界。测试避免请求天文数字赌系统OOM。
C++PMR测试如何确认释放走对上游
C++PMR测试如何确认释放走对上游:test_resource.cpp用tracking上游统计成功分配和归还;池满小请求、1024字节大请求和超出基本对齐请求都必须在归还后使live归零。测试还验证身份等价和显式allocator复制。计数能发现部分路由错误,不能单独证明全部异常安全;构造元素抛错的注入仍在TEST-SPEC待扩展。
CMakeLists.txt(同源文件,示例未执行)
cmake_minimum_required(VERSION 3.20)
project(p2_allocators LANGUAGES C CXX)
add_library(slab STATIC slab.c)
target_include_directories(slab PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(slab PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED YES C_EXTENSIONS NO)
add_executable(allocator_demo main.cpp)
target_link_libraries(allocator_demo PRIVATE slab)
target_compile_features(allocator_demo PRIVATE cxx_std_20)
add_executable(test_slab test_slab.c)
target_link_libraries(test_slab PRIVATE slab)
add_executable(test_resource test_resource.cpp)
target_link_libraries(test_resource PRIVATE slab)
target_compile_features(test_resource PRIVATE cxx_std_20)
enable_testing()
add_test(NAME slab_contract COMMAND test_slab)
add_test(NAME resource_contract COMMAND test_resource)config.hpp(同源文件,示例未执行)
#pragma once
#include <cstddef>
inline constexpr std::size_t block_bytes = 256;
inline constexpr std::size_t block_count = 8;
inline constexpr int item_count = 6;main.cpp(同源文件,示例未执行)
#include "config.hpp"
#include "slab_resource.hpp"
#include <iostream>
#include <list>
int main() {
try {
slab_resource resource(block_bytes, block_count);
int sum = 0;
{
std::pmr::list<int> items(&resource);
for (int i = 1; i <= item_count; ++i) items.push_back(i);
for (int value : items) sum += value;
}
std::cout << "sum=" << sum << " live=" << resource.live() << '\n';
return sum == 21 && resource.live() == 0 ? 0 : 1;
} catch (const std::exception& e) {
std::cerr << e.what() << '\n'; return 1;
}
}README.md(同源文件,示例未执行)
# Fixed slab / PMR integrated teaching project
示例未执行。C11 slab 库被 C++20 memory_resource 适配器、PMR list 客户端和两个测试入口共同使用,无第三方依赖。单线程;不是通用 malloc 替代品。
输入在 config.hpp:256 字节、8 块、整数1到6;预期 allocator_demo 输出 `sum=21 live=0` 后换行,退出0。list 节点布局依标准库变化,不能承诺6次都命中池;上游保证支持更大节点。默认 new_delete_resource 允许溢出,因此整个适配器不是有界内存;用 null_memory_resource 才禁止上游成功分配。
手动步骤仅供读者,本文未执行:Windows PowerShell、Linux Bash、macOS Zsh 均在本目录执行 `cmake -S . -B build`,`cmake --build build --config Debug`,`ctest --test-dir build -C Debug --output-on-failure`。单配置产物在 build,多配置通常在 build/Debug;执行对应 allocator_demo 或 allocator_demo.exe。需预先具备 CMake >=3.20、兼容C11/C++20工具链及PMR标准库,不在站点安装或运行。
slab_create 两次 malloc;初始化失败不发布句柄。池空与不支持的大小/对齐走上游,释放按池块起点等值归属决定路径,不能仅根据大小重算,因为池空时小请求也会走上游。分配与释放的 bytes/alignment 必须相同,指针必须来自当前等价资源且尚未释放。资源先构造、容器后构造,容器先销毁。析构检查未归还计数并 terminate 是教学诊断,不代替调用契约。
所有 C 句柄禁止按值复制、并发访问、外部修改元数据。malloc backing 按 max_align_t 对齐,stride向上取整;不承诺扩展对齐。free list 索引存外部元数据,位图记录当前借出;重复归还只在未重新分配期间可检测。无法判断伪造、悬空指针或复用后旧别名;调用者必须保证传入的指针值可合法求值。slab_give 不销毁C++对象,容器负责先析构元素再释放存储。
测试是完整源码,TEST-SPEC 列出预期和未实现故障注入。没有声称 native AST、编译、CMake、测试、性能或三平台运行成功。slab.c(同源文件,示例未执行)
#include "slab.h"
#include <stdint.h>
#include <stdlib.h>
struct slab {
unsigned char *storage;
size_t width, stride, count, head, live;
size_t next[64];
uint64_t used;
};
size_t slab_alignment(void) { return _Alignof(max_align_t); }
slab *slab_create(size_t width, size_t count) {
size_t a = slab_alignment(), stride;
slab *s;
if (!width || !count || count > 64 || width > SIZE_MAX - (a - 1)) return NULL;
stride = ((width + a - 1) / a) * a;
if (stride > SIZE_MAX / count) return NULL;
s = malloc(sizeof *s);
if (!s) return NULL;
s->storage = malloc(stride * count);
if (!s->storage) { free(s); return NULL; }
s->width = width; s->stride = stride; s->count = count;
s->head = 0; s->live = 0; s->used = 0;
for (size_t i = 0; i < count; ++i) s->next[i] = i + 1;
return s;
}
void *slab_take(slab *s, size_t bytes, size_t a) {
size_t i;
if (!s || !bytes || !a || (a & (a - 1)) ||
bytes > s->width || slab_alignment() % a || s->head == s->count) return NULL;
i = s->head; s->head = s->next[i];
s->used |= UINT64_C(1) << i; ++s->live;
return s->storage + i * s->stride;
}
static size_t locate(const slab *s, const void *q) {
/* Equality only; no subtraction/order comparison with unrelated pointers. */
for (size_t i = 0; i < s->count; ++i)
if (q == (const void *)(s->storage + i * s->stride)) return i;
return s->count;
}
int slab_owns(const slab *s, const void *q) {
return s && q && locate(s, q) != s->count;
}
int slab_give(slab *s, void *q) {
size_t i;
if (!s || !q) return -1;
i = locate(s, q);
if (i == s->count || !(s->used & (UINT64_C(1) << i))) return -1;
s->used &= ~(UINT64_C(1) << i);
s->next[i] = s->head; s->head = i; --s->live;
return 0;
}
size_t slab_live(const slab *s) { return s ? s->live : 0; }
int slab_destroy(slab *s) {
if (!s) return 0;
if (s->live) return -1;
free(s->storage); free(s); return 0;
}slab.h(同源文件,示例未执行)
#ifndef TEACHING_SLAB_H
#define TEACHING_SLAB_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct slab slab;
/* Single-thread only. count in [1,64], block_bytes > 0.
Owns malloc storage; supports fundamental alignment only. */
slab *slab_create(size_t block_bytes, size_t count);
/* Returns NULL for unsupported request or exhaustion; never changes state on failure. */
void *slab_take(slab *, size_t bytes, size_t alignment);
/* q must be NULL or a determinate pointer value to still-live storage.
Rejects non-block starts and duplicate returns while the slab exists.
Does NOT validate dangling/forged pointers or detect stale aliases after reuse. */
int slab_give(slab *, void *q);
int slab_owns(const slab *, const void *q);
size_t slab_live(const slab *);
size_t slab_alignment(void);
/* Busy returns -1 without freeing. NULL is accepted. */
int slab_destroy(slab *);
#ifdef __cplusplus
}
#endif
#endifslab_resource.hpp(同源文件,示例未执行)
#pragma once
#include "slab.h"
#include <cstddef>
#include <exception>
#include <memory_resource>
#include <new>
#include <stdexcept>
class slab_resource final : public std::pmr::memory_resource {
slab *pool_;
std::pmr::memory_resource *upstream_;
std::size_t upstream_live_ = 0;
void *do_allocate(std::size_t bytes, std::size_t alignment) override {
if (void *p = slab_take(pool_, bytes, alignment)) return p;
void *p = upstream_->allocate(bytes, alignment);
++upstream_live_; // counts successful upstream allocations only
return p;
}
void do_deallocate(void *p, std::size_t bytes, std::size_t alignment) override {
// Preconditions: original pointer, size and alignment; not arbitrary pointer validation.
if (slab_owns(pool_, p)) {
if (slab_give(pool_, p) != 0) std::terminate();
} else {
upstream_->deallocate(p, bytes, alignment);
--upstream_live_;
}
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
public:
explicit slab_resource(std::size_t width, std::size_t count,
std::pmr::memory_resource *upstream = std::pmr::new_delete_resource())
: pool_(nullptr), upstream_(upstream) {
if (!upstream) throw std::invalid_argument("null upstream");
pool_ = slab_create(width, count);
if (!pool_) throw std::bad_alloc();
}
slab_resource(const slab_resource&) = delete;
slab_resource& operator=(const slab_resource&) = delete;
~slab_resource() override {
if (upstream_live_ || slab_destroy(pool_) != 0) std::terminate();
}
std::size_t live() const noexcept { return slab_live(pool_) + upstream_live_; }
};TEST-SPEC.md(同源文件,示例未执行)
# Pending execution / expectations
示例未执行。test_slab 和 test_resource 是可执行测试源码,CHECK 不受 NDEBUG 影响,失败返回源码行号(进程退出码截断可因平台不同)。
- C:0/65块与SIZE_MAX溢出配置拒绝;不支持请求不改状态;两块耗尽;busy destroy 保持句柄有效;外部活对象和内部非起点拒绝;重复归还拒绝;释放后LIFO复用;全部归还可销毁。
- C++:池满的小请求、超大请求、扩展对齐转上游;释放路由正确;不同池即使同上游也不相等;显式allocator复制选择目标资源;null上游确定抛bad_alloc且状态不变;所有容器先销毁,再检查计数归零。
- 待扩展故障注入:包装malloc使第二次分配失败,记录第一次释放;多次上游抛错;元素构造抛错后节点归还。现有源码没有这些注入,不把人工推演说成测试覆盖。
- 禁止把无效pmr deallocate、释放后访问或异源swap当作可恢复测试;它们先违反接口前提。ASan/UBSan若读者另行执行只能提供部分证据;复用后旧指针可能仍指向有效backing,需代际句柄设计才能拒绝。test_resource.cpp(同源文件,示例未执行)
#include "slab_resource.hpp"
#include <list>
#include <type_traits>
#define CHECK(x) do { if (!(x)) return __LINE__; } while (0)
class tracking final : public std::pmr::memory_resource {
void *do_allocate(std::size_t n, std::size_t a) override {
void *p = std::pmr::new_delete_resource()->allocate(n, a); ++live; return p;
}
void do_deallocate(void *p, std::size_t n, std::size_t a) override {
std::pmr::new_delete_resource()->deallocate(p, n, a); --live;
}
bool do_is_equal(const std::pmr::memory_resource& r) const noexcept override { return this == &r; }
public: int live = 0;
};
int main() {
tracking upstream;
slab_resource r(256, 1, &upstream), other(256, 1, &upstream);
CHECK(r.is_equal(r) && !r.is_equal(other));
void *p = r.allocate(16, 1), *q = r.allocate(16, 1);
CHECK(upstream.live == 1); // exhaustion forwards even supported request
r.deallocate(q, 16, 1); r.deallocate(p, 16, 1);
void *large = r.allocate(1024, 1);
CHECK(upstream.live == 1); r.deallocate(large, 1024, 1);
void *over = r.allocate(32, slab_alignment() * 2);
CHECK(upstream.live == 1); r.deallocate(over, 32, slab_alignment() * 2);
{
std::pmr::list<int> a(&r); a.push_back(7);
std::pmr::list<int> explicit_copy(a, &other);
CHECK(explicit_copy.get_allocator().resource() == &other);
CHECK(explicit_copy.front() == 7);
}
slab_resource bounded(16, 1, std::pmr::null_memory_resource());
void *only = bounded.allocate(1, 1);
bool failed = false;
try { (void)bounded.allocate(1, 1); } catch (const std::bad_alloc&) { failed = true; }
CHECK(failed && bounded.live() == 1); bounded.deallocate(only, 1, 1);
CHECK(upstream.live == 0 && r.live() == 0 && other.live() == 0);
static_assert(!std::allocator_traits<std::pmr::polymorphic_allocator<int>>::propagate_on_container_move_assignment::value);
return 0;
}test_slab.c(同源文件,示例未执行)
#include "slab.h"
#include <stdint.h>
#include <stdio.h>
#define CHECK(x) do { if (!(x)) return __LINE__; } while (0)
int main(void) {
slab *s = slab_create(17, 2);
void *a, *b; int foreign = 0;
CHECK(s);
CHECK(!slab_create(0, 2)); CHECK(!slab_create(1, 0));
CHECK(!slab_take(s, 0, 1)); CHECK(!slab_take(s, 1, 0));
CHECK(!slab_create(SIZE_MAX, 2));
CHECK(!slab_create(1, 65));
CHECK(!slab_take(s, 18, 1)); CHECK(!slab_take(s, 1, 3));
a = slab_take(s, 17, slab_alignment());
b = slab_take(s, 1, 1);
CHECK(a && b && a != b && slab_live(s) == 2);
CHECK(!slab_take(s, 1, 1)); CHECK(slab_destroy(s) == -1);
CHECK(slab_give(s, &foreign) == -1);
CHECK(slab_give(s, (unsigned char *)a + 1) == -1);
CHECK(slab_give(s, a) == 0); CHECK(slab_give(s, a) == -1);
CHECK(slab_take(s, 1, 1) == a); // stale alias cannot be detected after reuse
CHECK(slab_give(s, a) == 0); CHECK(slab_give(s, b) == 0);
CHECK(slab_destroy(s) == 0);
puts("slab contracts: expected pass"); return 0;
}独立练习
将默认上游换成null资源后,如何使失败与标准库节点尺寸无关?
参考答案(先独立推演)
直接对resource申请1字节、1对齐,单槽先占满再申请第二次,捕获bad_alloc并检查首块仍可归还;不要依赖list内部节点大小。
一手资料与边界
2026-10-17核对 WG14 C11 N1570 6.2.8、6.5.6、6.5.8与7.22.3;C++资源协议工作草案用于溯源,滚动草案的新接口不自动纳入C++20;Allocator要求用于核对传播边界。国内资料核读 Microsoft中文memory_resource概览,平台实现说明不推广为所有标准库。项目未编译、未执行,普通静态模板通过不能证明内存安全。
继续阅读
- 知识专题
- 排错
- 知识专题