首先介绍一下类的数据成员指针。当对类的数据成员执行取址操作时,可以获得类的数据成员指针。本质上是附带类型信息的地址偏移。这部分可以参考文献 1。
#include <cassert>
#include <cstdint>
#include <iostream>
struct A {
int x;
int y;
double z;
};
int main() {
int A::*a = &A::x; // a 的类型为 `int A::*`
int A::*b = &A::y;
double A::*c = &A::z;
A o;
o.*a = 10; // 可以使用 obj.* 运算符访问
o.*(&A::y) = 20;
assert(o.x == 10);
assert(o.y == 20);
a = b; // 同类型可以赋值,本质上是地址偏移
assert(sizeof(a) == sizeof(void *)); // 与指针大小一致
assert(*(uintptr_t *)(&a) == 4); // 偏移量为 4
*(uintptr_t *)(&a) = 0; // 也就可以强行修改值了
assert(o.*a == 10); // 偏移量为 0 时会访问 A::x
}
那么如果有类私有数据成员指针,就可以访问对应的私有变量了,但 C++ 并不允许直接对类私有成员取址:
#include <cassert>
#include <cstdint>
#include <iostream>
class A {
public:
int X() { return x_; }
int Y() { return y_; }
private:
int x_;
int y_;
};
int main() {
A o;
int A::*a;
*(uintptr_t *)(&a) = 0;
o.*a = 10;
assert(o.X() == 10);
*(uintptr_t *)(&a) = 4;
o.*a = 20;
assert(o.Y() == 20);
// a = &A::x_; // not allowed
}
幸运的是 C++ 模版类的显式实例化会忽略成员访问说明符,参考文献 3:
Explicit instantiation definitions ignore member access specifiers: parameter types and return types may be private.
这也就允许传入类私有成员变量地址。如此设计的原因暂不得知,但利用该规则就可以实现访问任意类的私有成员变量:
#include <cassert>
#include <iostream>
class A {
public:
int X() { return x_; }
private:
int x_;
};
int A::*FiledPtr();
template <int A::*M>
struct Rob {
friend int A::*FiledPtr() { return M; }
};
template struct Rob<&A::x_>;
int main() {
A o;
o.*FiledPtr() = 10;
assert(o.X() == 10);
}
说它是奇技淫巧不为过,但某些场景下确实需要这样的黑科技。例如 folly 库中实现的 atomic_shared_ptr
,就需要访问标准库 std::shared_ptr
的私有引用计数成员进行计数的修改。
这种方法还是需要定义几个辅助类,如果想访问标准库中的类私有成员,有没有更简单、直接的技巧呢?有的 :D
#include <iostream>
#define private public
#include <memory>
#undef private
int main() {
std::shared_ptr<int> a;
a._M_refcount; // gcc, access private member
}
宏替换 private
并不总是有效,不建议在实际项目中使用,仅适用于单元测试场景。实际项目中可以使用 access_private,其头文件中定义了一些访问私有成员 / 函数的宏,原理仍然是模版类显式实例化:
#include <cassert>
#include "access_private.hpp"
class A {
int m_i = 3;
};
ACCESS_PRIVATE_FIELD(A, int, m_i)
void foo() {
A a;
auto &i = access_private::m_i(a);
assert(i == 3);
}