C++ 访问类私有成员变量

2020.07.01
SF-Zhou

首先介绍一下类的数据成员指针。当对类的数据成员执行取址操作时,可以获得类的数据成员指针。本质上是附带类型信息的地址偏移。这部分可以参考文献 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);
}

References

  1. "Pointers to data members", C++ Reference
  2. "Member access operators", C++ Reference
  3. "Explicit instantiation", C++ Reference