前言
取一个对象的地址,最自然的写法是&obj。但在泛型代码里,这个表达式不一定可靠:如果类型重载了operator&,&obj调用的就是这个重载,返回值可能是任意类型、任意值,甚至是一个空指针。C++11 在<memory>中加入了std::addressof,它承诺返回对象的真实地址,绕过operator&的重载。
这个函数很小,小到很多人只记住「取地址用它」,却说不清它为什么存在、标准签名里的删除重载是做什么的、编译器内部又是怎么实现的。本文从&的语义出发,解释标准签名、经典实现中的两次reinterpret_cast,以及现代编译器使用的内置函数,最后给出适用场景和边界。
需要说明:不同标准库的实现细节不同,本文展示的是 libstdc++ 一类实现的经典写法,目的是解释原理,不代表所有版本逐字相同。实际行为以所用标准库的头文件和官方文档为准。
一、&为什么不够
对内置类型,&x的结果就是x的地址,类型是T*。但对类类型,&是一个可重载的运算符。标准允许类定义operator&,而且不限制它的返回类型。这就产生了一个矛盾:泛型代码需要「对象的地址」,而&表达的是「这个类型作者定义的取地址操作」。
// 适用于 C++17
#include <iostream>
#include <memory>
#include <string>
struct Sneaky {
Sneaky* operator&() { return nullptr; }
const Sneaky* operator&() const { return nullptr; }
};
template <typename T>
void inspect(T& value) {
std::cout << "operator& is null: " << (&value == nullptr) << '\n';
std::cout << "addressof is null : " << (std::addressof(value) == nullptr) << '\n';
}
int main() {
Sneaky s;
inspect(s);
int x = 42;
int* p = std::addressof(x);
*p = 7;
std::cout << x << '\n';
const std::string text = "hello";
const std::string* cp = std::addressof(text);
std::cout << cp->size() << '\n';
return 0;
}在inspect里,&value触发Sneaky::operator&,得到空指针;std::addressof(value)则返回对象真正所在的地址。对int和const std::string,两者的结果相同,区别只在「类型是否重载了&」。
这也解释了一个常见的模板陷阱:容器、序列化库、智能指针的构造函数如果内部用&elem保存元素地址,遇到重载了operator&的元素类型就会拿到错误指针。标准库在需要真实地址的地方使用std::addressof,正是为了不依赖用户类型对&的定义。
二、标准签名与删除的重载
std::addressof在<memory>中声明,C++11 起可用。它的标准形式可以概括为:
// 标准签名示意(C++17 起为 constexpr)
template <class T>
constexpr T* addressof(T& arg) noexcept;
// 阻止对右值取地址
template <class T>
const T* addressof(const T&&) = delete;第一点要注意返回类型是T*,其中T可以带const、volatile限定。传入const int左值时,T推导为const int,返回const int*;不会偷偷去掉限定符。
第二点是那个被删除的重载。为什么需要它?假设没有这个删除声明,调用std::addressof(std::string("tmp"))时,T&形参无法绑定到右值,但若实参本身是const右值,模板参数可以推导为const T,于是const T&能绑定到临时对象,函数就会返回一个指向「函数调用结束后即销毁的临时对象」的指针。这个指针立刻悬垂。删除const T&&的重载,让这类调用在编译期直接失败,而不是留下运行期隐患。
noexcept表示这个操作不会抛异常;取地址本身也不涉及内存分配。C++17 起它被标记为constexpr,因此可以在常量表达式中使用:
// 适用于 C++17
#include <memory>
constexpr int kValue = 7;
constexpr const int* kPtr = std::addressof(kValue);
static_assert(*kPtr == 7, "addressof should be usable in constant expressions");三、实现拆解:从字符引用到内置函数
在编译器尚未提供专用内置函数的年代,libstdc++ 等实现用一组reinterpret_cast和const_cast完成取址。核心只有一行,但每一步都值得拆开看:
// 经典实现示意(libstdc++ 旧版本写法),仅用于讲解原理
#include <cstddef>
namespace detail {
template <typename T>
T* addressOf(T& value) noexcept {
return reinterpret_cast<T*>(
&const_cast<char&>(
reinterpret_cast<const volatile char&>(value)));
}
} // namespace detail从内向外读:
reinterpret_cast<const volatile char&>(value):把对象重新解释为「对const volatile char的引用」。字符类型可以访问任意对象的内存表示(object representation),这一步让我们拿到对象首字节的视图,同时保留 cv 限定。const_cast<char&>(...):去掉const和volatile,得到普通的char&。const_cast只能增删 cv 限定,不能改变底层类型,所以这里类型仍是char。&:对char&取地址,得到char*,指向对象首字节。由于此时是内置类型的char,不会触发任何用户重载。reinterpret_cast<T*>(...):把char*转回对象指针。指针指向的仍是同一块内存,只是类型恢复为T*。
这四步能成立,依赖两条规则:字符类型可以别名访问对象表示;指针在「对象指针 →char*→ 原对象指针」的往返转换后能恢复原值。标准对后者的措辞有前提(对齐等),而std::addressof的行为由标准库保证,所以调用者不需要自己重复这套转换。不要在自己的代码里照抄这个实现来替代std::addressof,因为它的可移植性依赖于具体实现细节,而标准库已经替你处理了。
现代实现更直接。libstdc++ 和 libc++ 在编译器支持时使用内置的__builtin_addressof,MSVC 也有类似的编译器内置机制:
// 现代标准库的常见写法(GCC/Clang),具体以实际头文件为准
template <typename T>
constexpr T* addressof(T& arg) noexcept {
return __builtin_addressof(arg);
}内置函数由编译器直接生成取址指令,不经过任何运算符重载,也能在常量表达式中求值。它还能正确处理volatile对象和带有特殊operator&的类型。对使用者来说,两种实现的语义相同,差别只在编译器是否支持、以及是否能在constexpr上下文中使用。
四、使用场景与边界
std::addressof最常见的场景是泛型库内部:
- 容器在构造元素、计算元素地址时,不能假设元素类型没有重载
operator&。 - 序列化或反射框架需要把对象地址交给底层缓冲区。
- 智能指针、分配器在把对象引用转换为指针时,需要真实地址。
- 模板函数在转发或保存引用时,需要避免用户自定义
&的干扰。
| 场景 | &obj | std::addressof(obj) |
|---|
| 内置类型 | 真实地址 | 真实地址 |
重载了operator&的类 | 调用重载,结果不确定 | 真实地址,绕过重载 |
const对象 | 返回const T* | 返回const T* |
| 右值临时对象 | 通常无法取址 | 被删除的重载拒绝编译 |
| 位域 | 不允许取址 | 不允许取址 |
边界也要讲清楚。std::addressof只解决「取真实地址」,不解决生命周期:它返回裸指针,不延长对象寿命,也不参与所有权管理。对已经被销毁的对象调用它是未定义行为;对end()迭代器解引用后再取地址同样是未定义行为。它也不能访问私有成员——你仍然需要一个能命名的对象或引用,访问控制照常生效。
另外,std::addressof对函数也适用:传入函数名时,模板参数推导为函数类型,返回指向函数的指针。这一点在泛型代码里偶尔有用,但不要把它和成员函数指针混淆,后者需要&Class::member的语法。
常见坑点
- ❌
auto p = std::addressof(std::string("temp"));
✅ 右值会被删除的重载拒绝;先把对象绑定到具名变量,再取地址。
- ❌ 以为
std::addressof能绕过private访问控制,直接取私有成员的地址。
✅ 它只绕过operator&重载;访问控制仍由编译器检查。
- ❌ 对位域写
std::addressof(obj.bitfield)。
✅ 位域没有独立地址,先复制到局部变量再取地址。
- ❌ 在泛型代码里用
&value保存元素地址。
✅ 用std::addressof(value),避免元素类型重载operator&。
- ❌ 对
v.end()解引用后取地址,试图获得「末尾位置」。
✅ 解引用尾后迭代器是未定义行为;需要下标就用size()和合法索引。
- ❌ 自己用
reinterpret_cast写一套取址函数,并认为它和标准库等价。
✅ 直接调用std::addressof,实现细节交给标准库。
- ❌ 忘记包含
<memory>,依赖其他头文件间接引入。
✅ 显式包含<memory>,不要依赖传递包含。
- ❌ 把
std::addressof返回的指针当成拥有所有权的资源去delete。
✅ 它只是观察者指针;所有权仍由原对象或智能指针管理。
总结
| 要点 | 说明 |
|---|
| 头文件 | <memory>,C++11 起 |
| 作用 | 返回对象真实地址,绕过重载的operator& |
| 返回类型 | T*,保留const/volatile限定 |
| 右值 | const T&&重载被删除,防止返回悬垂指针 |
| 常量求值 | C++17 起为constexpr |
| 经典实现 | 字符引用 +const_cast+ 两次reinterpret_cast |
| 现代实现 | 编译器内置__builtin_addressof一类机制 |
std::addressof是标准库「把细节收进一个函数」的典型例子:接口只有一个参数、一个返回值,却把运算符重载、cv 限定、右值拒绝和常量求值都处理掉了。理解它的实现不是为了重写它,而是为了在泛型代码里知道什么时候&不可信,以及为什么标准库内部总是对它格外小心。