不能直接用 std::atomic 存指针加标记,因指针对齐位虽可利用,但 std::atomic 不提供 fetch_or/fetch_and 等原子位操作,且直接 store/load 后位运算会引发竞态;正确做法是用 CAS 循环模拟,将指针左移空出低位存标记。

为什么不能直接用 std::atomic<:uintptr_t></:uintptr_t> 存指针加标记
因为 std::uintptr_t 是整型,而指针可能有对齐位(比如最低 2–3 位恒为 0),这些位正好能塞标记;但直接用 std::atomic<:uintptr_t></:uintptr_t> 操作时,若不保证原子读-改-写(如 CAS)的完整性,会丢标记或破坏指针。更关键的是:**std::atomic<:uintptr_t></:uintptr_t> 不提供 fetch_or/fetch_and 等带返回值的原子位操作**(除非底层平台支持),而标记/清除往往依赖这类操作。
正确做法:用 std::atomic<:uintptr_t></:uintptr_t> + CAS 循环模拟原子位操作
核心思路是把指针地址左移若干位(比如 2 位),空出低位放标记,所有读写都通过 compare_exchange_weak 实现。常见错误是直接 store 或 load 后手动位运算——这会导致竞态。
- 假设用最低 2 位做标记:
const std::uintptr_t TAG_MASK = 0x3; - 存储指针时先清标记:
ptr_val = (reinterpret_cast<:uintptr_t>(p) >> 2) (确保对齐)</:uintptr_t> - 设置标记用 CAS:循环尝试将目标位置的值从
old改为old | TAG_MASK,失败则重试 - 读取指针时记得右移还原:
reinterpret_cast<t>(val </t>
示例(标记 bit0):
std::atomic<std::uintptr_t> tagged_ptr{0};
void set_marked(T* p) {
std::uintptr_t desired, expected;
do {
expected = tagged_ptr.load(std::memory_order_acquire);
desired = (reinterpret_cast<std::uintptr_t>(p) & ~1ULL) | 1ULL;
} while (!tagged_ptr.compare_exchange_weak(expected, desired,
std::memory_order_release, std::memory_order_acquire));
}注意内存序和平台对齐约束
std::uintptr_t 在多数平台是 64 位,但某些嵌入式平台可能是 32 位;若指针本身 >32 位,直接截断会崩溃。更隐蔽的问题是:x86-64 上 compare_exchange_weak 对 8 字节 std::uintptr_t 是原子的,但 ARM64 要求地址 8 字节对齐,否则可能触发未定义行为。
立即学习“C++免费学习笔记(深入)”;
- 务必检查
alignof(std::uintptr_t)和目标指针的对齐方式 - 标记位数不能超过指针自然对齐位数(例如 4 字节对齐 → 最多用低 2 位)
-
std::memory_order_acquire用于读标记后访问指针所指数据,release用于写标记前完成指针初始化
比裸 std::atomic<:uintptr_t></:uintptr_t> 更稳妥的替代方案
如果只是需要“指针+1bit 标记”,std::atomic<:shared_ptr>></:shared_ptr> 或 std::atomic<:unique_ptr>></:unique_ptr> 都不适用(它们不是无锁的)。但可以封装一层:
- 用
std::atomic<uint64_t></uint64_t>(显式大小)代替std::uintptr_t,避免平台差异 - 借助
std::atomic_ref(C++20)配合对齐好的联合体,把指针和标记映射到同一块内存 - 实际项目中,建议优先用成熟无锁库如
libcds的gc::guarded_ptr,它已处理 ABA、内存回收等边界问题
无锁标记指针真正难的不是存标记,而是标记之后怎么安全释放内存——这通常要搭配 Hazard Pointer 或 RCU 才能闭环。单纯原子操作只是第一步。


















