C++to_address指针转普通地址
to_address:C++20里那个“不声不响却救了我三次”的指针转址函数
上周帮同事排查一个自定义分配器的崩溃问题,堆栈停在 operator* 上——可那是个 unique_ptr<T, CustomDeleter>,T 还带对齐要求。我们盯着 get() 返回的指针反复确认:地址没错,对齐也没问题……直到有人嘀咕了一句:“你确定 *ptr 真的访问的是 T 的起始位置?”
那一刻我突然想起 std::to_address——它不像 std::addressof 那样常被提起,也不像 reinterpret_cast 那样让人手痒就用,但它干的活,恰恰卡在现代C++内存模型最易滑脚的那条缝里。
to_address 的核心任务很朴素:*把任意“类指针类型”安全、统一地转换成原始内存地址(`T)**。 注意,是“类指针类型”,不只是T*`。比如:
std::unique_ptr<T>std::shared_ptr<T>- 自定义迭代器(如
vector<T>::iterator) - 甚至是你自己写的
handle<T>或offset_ptr<T>
这些类型都重载了 operator-> 和 operator*,但它们内部存储的未必是裸指针——可能是偏移量、句柄索引、加密后的token,或者像 compressed_pair 那样把指针和状态挤在一个字里。
过去我们怎么处理?写模板特化?用 get() 硬提?还是赌一把 static_cast<T*>(ptr.get())?
——这些都不是错,但每一种都在悄悄绕过类型系统的设计本意:让地址计算这件事,从用户代码里消失。
to_address 就是来收这个尾的。
C++20之前,标准没提供统一接口,大家各写各的 to_raw_pointer 工具函数。有的靠 decltype(*p) 推导,有的靠 SFINAE 检测 get(),还有的直接 reinterpret_cast ——结果在 __cpp_lib_to_address >= 201907L 被标准化后,才发现:原来 std::to_address 的实现逻辑比想象中更克制。
它只做三件事:
- *若参数是裸指针(`T`),直接返回原值**;
- 若类型有
to_address成员函数(如某些智能指针扩展),调用它; - *否则,调用 `std::addressof(p)
** ——注意,是addressof,不是取地址符&,它能正确处理重载了operator&` 的类型。
这个顺序不是随意排的。它优先信任类型自身对“如何暴露地址”的语义承诺,其次才退回到通用兜底。这种分层设计,让 to_address 在面对 boost::interprocess::offset_ptr 或 folly::fbstring::iterator 这类非标但广泛使用的类型时,依然能保持行为一致。
实战中最容易踩坑的,其实是对齐敏感场景。
比如你用 aligned_alloc(64, sizeof(MyStruct)) 分配内存,再包进 unique_ptr<MyStruct, AlignedDeleter>。此时 ptr.get() 返回的地址是对齐的,但如果你写 reinterpret_cast<char*>(ptr.get()) + 8 去访问某个字段,编译器可能优化掉对齐假设——而 to_address(ptr) + 8 却明确告诉编译器:“这是从合法指针派生的地址,对齐属性继承”。
再比如容器迭代器:
std::vector<std::byte> buf(1024);
auto it = buf.begin() + 16;
auto raw = std::to_address(it); // 得到 char*,且保留 buf 的生命周期绑定语义
这里 to_address(it) 不仅比 &*it 更安全(避免 operator* 的副作用),还显式表达了“我要的是地址,不是解引用”这一意图——这对静态分析工具和人肉 review 都是友好信号。
还有个常被忽略的细节:to_address 是 constexpr 的。
这意味着你可以在编译期计算地址偏移。例如写一个 static_buffer 类型,用 to_address 把 std::array<std::byte, N> 的首地址转成目标类型的指针,并参与 if constexpr 分支判断——这在嵌入式或序列化库中,能省掉不少运行时分支。
当然,它也不是万能钥匙。
to_address 不负责生命周期管理,也不校验有效性。传入空 shared_ptr?它照样返回 nullptr。传入已释放的 unique_ptr?行为未定义——这点和 get() 完全一致。它的价值,从来不在“防错”,而在“归一”:把五花八门的指针抽象,收束到一条清晰、可读、可维护的转换路径上。
现在回头看那个崩溃问题:同事在自定义分配器里,把 to_address(ptr) 的结果直接当 void* 传给 mmap,却忘了 to_address 返回的是 T*,而 mmap 要求页对齐地址。修复很简单:
auto addr = std::to_address(ptr);
auto page_aligned = reinterpret_cast<void*>(
(reinterpret_cast<uintptr_t>(addr) / 4096) * 4096
);
关键不是这段代码多高明,而是 to_address 让我们能把“获取原始地址”这个动作,从一堆 static_cast 和 get() 调用中剥离出来,单独审视、测试、复用。
C++ 的演进常常如此:没有惊天动地的新范式,只是悄悄拧紧一颗螺丝——让指针这辆老车,在泛型与安全的双轨上,少一次脱轨。
to_address 就是这样一颗螺丝。不大,但你摸到它的时候,会突然觉得:啊,原来这里早该有个接口了。


还没有评论,来说两句吧...