有没有比友元更优雅的方式

302次阅读  |  发布于2年以前

今天开始,「每日一题」栏目正式上线,我争取每天在朋友圈发一个问题,邀请大家讨论,然后在公众号里公布答案。

特此声明:这里的每日一题,是指每日最多一题,而不是每日至少一题

这是今天的问题:

我这里再详细描述一下:

首先有一个类A:


struct A {
private:
  struct AImpl;
  AImpl *impl;
};

还有一个类B:


struct B {
  void func(A *a) {
    impl->func(a);
  }
private:
  struct BImpl;
  BImpl *impl;
};

在源文件中还有个BImpl类:


struct BImpl {
  void func(A *a) {
    ///< 这里想要拿到a->impl,该怎么做
  }
};

在上面的代码中,用到了pimpl模式,关于此模式的介绍可以看我的这篇文章[pimpl设计模式] 。

其中A类和B类是暴露给外部的文件,只暴露一些应该给外部调用的接口,然后所有实现都放在内部的Impl类中。而且Impl中还包含了很多没有对外暴露的功能。

所以这里的大体逻辑是:对外暴露一个指针,对内使用它的impl指针。

但这里有个问题:在上面的代码中,impl指针是private变量,一个对象指针传递出去,再传回内部,怎么拿到它的impl指针呢?

说到这里,可能有些朋友会说,不会有这个问题,出现这种问题肯定是设计的不合理,然而这里我们暂且不讨论设计层面的东西,单纯的看看怎么能够解决这个问题。(由于业务的需求复杂性,导致我不得不采用这种设计,当然,更多的应该是因为我水平有限,没有想到更好的设计。)

我发完这个问题后,有很多朋友都私聊我一起讨论,最终总结出了三种答案。

方案1:友元。

把B类设置为A类的友元,然后在B中可以获得A类的private成员,然后改变一下BImpl中func的参数,变成这样:

struct B {
  void func(A *a) {
    impl->func(a->impl);
  }
private:
  struct BImpl;
  BImpl *impl;
};

struct BImpl {
  void func(AImpl *a) {
    ///<
  }
};

Impl侧接收Impl类型的指针,这样似乎更合理一些。

方案2:强转

A::AImpl* impl = (A::AImpl)((long)a);

因为A的impl是类A的第一个成员变量,所以类A的对象地址和成员变量impl的地址相同,利用此原则就可以直接把a指针强转成Impl指针。

这里可能大家还会有些疑问,如果类A中有虚函数表怎么办?

其实不会出现这个问题,因为pimpl模式本质就是为了隐藏实现而生。隐藏实现大体就两种方式,接口继承或者pimpl,用pimpl就没必要用继承,而用了继承就没必要用pimpl。

我们再假设A有虚函数表,也没啥太大问题,大不了判断一下A是否有虚函数(可研究下type_traits),然后改变一下指针偏移量再去做强转,这样也行。

方案3:还是强转,利用二维指针转成一维指针

原理和方案2类似。

贴一个大佬的原话:

考虑到机器有可能是32位或64位字长,long类型与机器字长是相同,所以先转为long指针,目的是为了取其值。

而此方案更妙。

A::AImpl* impl = *(A::AImpl**)a;

其实个人认为后两种方案没啥区别,大家怎么看?

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8