性能文章>访问私有成员—从技术实现的角度破坏"封装" 性>

访问私有成员—从技术实现的角度破坏"封装" 性原创

292166

你好,我是雨乐!

大约是在大二上半学期的时候接触的C语言,在下半学期开始接触C++语言。在C++教材的前几章就介绍了其特性,即:C++是一门面向对象语言,具有封装、继承和多态三个特点。后来,随着编码的增多以及工作经验的积累,对个概念的理解越来越深。编码习惯也严格按照相应的规则,该封装的时候进行封装,该继承的时候进行继承,以使得编程思维从之前的面向过程逐步过渡到面向对象。

作为开发人员,遵循编程规则本来就无可厚非,但是如果大家都遵循规则难免会有创新或者技术进步。有时候,在做某件事或者看到某个实现方案的时候,想想为什么要这么做,有没有更好的实现方案,这个编程或者做事习惯往往使得自己受益匪浅。比如,我们都知道每个线程都有一个自己的栈,线程内的局部变量出了作用域就会被释放,那么有没有可能跨线程从另外一个线程去访问该线程的局部变量呢?其实,问题不算难,我们只需要尝试即可,但往往缺少的就是这种尝试。对于C++三大特性中的封装特性,如果直接访问私有变量,则编译器会报错,那么有没有其它方式可以访问私有变量呢?

今天,不妨试着反其道而行,尝试以其他方式破坏封装性,直接访问私有变量。

从一段代码说起

代码示例如下:

#include <iostream>

class A {
 public:
  A() = default;
 private:
  int data_ = 0;
};

int main() {
  A a;
  std::cout << a.data_ << std::endl;

  return 0;
}

在gcc5.4下进行编译,不出所料,编译失败,报错如下:

test.cc: 在函数‘int main()’中:
test.cc:7:15: 错误:‘int A::data_’是私有的
   int data_ = 0;

从报错信息看,因为data_成员变量是私有的,而通过对象访问私有成员变量是不被允许的,除了通过重新定义一个公共接口,在该接口内对data_进行访问外,但是这种方式并没有实现本文的目的即破坏封装性,那么有没有其它方式呢?

第一次尝试

c++标准中有这样一段描述:

The usual access checking rules do not apply to non-dependent names used to specify template arguments of the simple-template-id of the partial specialization. [ Note: The template arguments may be private types or objects that would normally not be accessible. Dependent names cannot be checked when declaring the partial specialization, but will be checked when substituting into the partial specialization. — end note ]

也就是说模板参数可以是某个类的私有类型,所以,我们可以借助此条标准继续实现我们的目的,代码如下:

#include <iostream>

class A {
 public:
  A() = default;
 private:
  int data_ = 0;
};

template<int A::*Member>
int& GetPrivateData(A& obj) {
  return obj.*Member;
}

template int& GetPrivateData<&A::data_>(A&);

int main() {
  A obj;
  GetPrivateData<&A::data>(obj);

  return 0;
}

在上述代码中,定义了一个函数模板,其模板参数为int A::*Member,功能是返回类A中的成员变量,编译后,报错如下:

test.cc: 在函数‘int main()’中:
test.cc:7:15: 错误:‘int A::data_’是私有的
   int data_ = 0;
               ^
test.cc:22:3: 错误:在此上下文中
   GetPrivateData<&A::data_>(obj);

看来此方式还是行不通,只能另想它法。

第二次尝试

在上面的提示中,显示不能直接访问私有成员,标准提供了个方法,就是将需要访问类私有成员的函数或者类声明为friend。看到这块,你可能会想,有了friend用得着你教?😕。

很快写出如下这种代码:

class A {
 public:
  A() = default;
 private:
  int data_ = 0;
  
 friend int Access(const A &a) {
   return a.data_;
 }
};

int main() {
  A a;
  Access(a);
  
  return 0;
}

无疑,上面这种代码可以访问私有成员,但缺点是需要更改类实现,下面将介绍一种方式,其在不修改类本身定义的情况下实现访问私有成员变量。

本着大方向不变的原则,依然使用模板的方式访问私有成员,而对于上节中提示的非法访问私有成员,我也采用将对应函数声明为friend的方式。

#include <iostream>

class A {
 public:
  A() = default;
 private:
  int data_ = 0;
};

template < int A::*Member >
class Access {
 public:
   friend  int GetPrivateData(A& obj) {
     return obj.*Member;
  }
};

template  class Access<&A::data_>;

int GetPrivateData(A& );


int main() {
  A obj;
  GetPrivateData(obj);

  return 0;
}

编译 & 运行,OK!!!

另辟蹊径

在上一节实现中,使用了friend进行访问控制,所以在考虑有没有不使用friend的方式,于是在网上进行搜索查阅,如下:

class A {
 public:
  A(int num) : data_(num) {};
 private:
  int data_ = 0;
};

template <typename PtrType>
class Access {
 public:
 inline static PtrType ptr;
};

template <auto T>
struct PtrTaker {
    struct Transferer {
        Transferer() {
            Access<decltype(T)>::ptr = T;
        }
    };
    inline static Transferer tr;
};

template class PtrTaker<&A::data_>; // 显示实例化

int main() {
  A a{10};

  int b = a.*Access<int A::*>::ptr;
  
  return 0;
}

说真的,看到这种实现方式的时候,一脸懵逼,尤其是对模板用的不多的情况下,阅读这短短几十行代码用了一天时间,其间也跟@Chunel骏哥哥一起讨论,奈何太挫了,只能硬着头皮自己研究,也跟群里的大佬们一起讨论了下,再结合自己的理解,分析下这块:

1、因为用到了inline 变量以及模板参数为auto,所以上述代码在cpp17上才可以运行。

2、以&A::data_作为模板参数,对类模板PtrTaker进行显示实例化,在显示实例化的时候,虽然不创建对象,但是对于其中存在的静态变量依然会进行初始化。因此会调用Transferer类的构造函数,从而对Access::ptr进行初始化

看上述代码的时候,一开始卡在了a.*Access<int A::*>::ptr这部分,后来经过跟其他技术大佬进行沟通,对这部分可以进行拆分简化:

  • • p = Access<int A::*>::ptr;

  • • a.*p

看了下面的代码示例,相信能便于理解:

class Data {
 public:
  int num_ = 0;
};

int main() {
  int Data::*ptr=&Data::num_;
  
  Data data;
  data.*ptr = 10;
  
  return 0;
}

好了,我们接着进行讨论。

在使用对象访问成员的时候,其地址实际上分为两部分的,以a.data_为例(此处忽略访问控制权限),一部分是a的this指针,另一部分是data_成员在A结构里的偏移量,这个偏移量存储在&A::data_中。在上面的代码中,这个偏移量存储在静态数据ptr里了,即上面提到的Access::ptr。

所以,a.*p相当于如下:

int A::* p = &A::data_;
int offset = *(long long *)&p;
int data = *(int *)((char *)&a + offset);

好了,截止为此,通过模板方式访问类私有成员的讨论结束了。

可能有人会有疑问,如果类有多个成员变量,又该如何访问呢,方式类似,代码如下:

#include <iostream>
#include <string>

class A {
 public:
  A(int num, std::string v) : data_(num), value_(v) {};
 private:
  int data_ = 0;
  std::string value_;
};

template <typename Tag>
class Access {
 public:
 inline static typename Tag::type ptr;
};

template <typename Tag, typename Tag::type V>
struct PtrTaker {
    struct Transferer {
        Transferer() {
            Access<Tag>::ptr = V;
        }
    };
    inline static Transferer tr;
};

struct Tag1 {
    using type = int A::*;
};

struct Tag2 {
    using type = std::string A::*;
};

template class PtrTaker<Tag1, &A::data_>; // 显示实例化
template class PtrTaker<Tag2, &A::value_>; // 显示实例化

int main() {
A a{0, "abc"};

std::cout << "123 " << a.*Access<Tag2>::ptr;
}

思路跟之前类似,在此不再赘述哈~~

今天的文章就到这,我们下期见!

你好,我是雨乐,从业十二年有余,历经过传统行业网络研发、互联网推荐引擎研发,目前在广告行业从业8年。目前任职某互联网公司高级技术专家一职,负责广告引擎的架构和研发。

 

点赞收藏
分类:标签:
高性能架构探索

公众号《高性能架构探索》

请先登录,查看6条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】聊聊C语言中的malloc申请内存的内部原理

【全网首发】聊聊C语言中的malloc申请内存的内部原理

6
6