查看原文
其他

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

程序喵大人 程序喵大人 2022-08-22

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


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


这是今天的问题:


我这里再详细描述一下:


首先有一个类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;

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



往期推荐



C++服务性能优化的道与术-道篇:阿姆达尔定律

压箱底的音视频学习资料以及面经整理

C++ Best Practices (C++最佳实践)翻译与阅读笔记

万字长文 |  STL 算法总结

2021程序喵技术年货

【性能优化】lock-free在召回引擎中的实现

SDK开发的一些思考

定下来了!

Linux中对【库函数】的调用进行跟踪的 3 种【插桩】技巧

【线上问题】P1级公司故障,年终奖不保

探索CPU的调度原理

咳咳,说下开源项目这件事的进展

想拉着大家搞点事!

防御性编程技巧

C++的全链路追踪方案,稍微有点高端

喵哥吐血整理:软件开发的51条建议


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存