• API设计笔记:pimpl技巧


    pimpl

    pointer to implementation:指向实现的指针,使用该技巧可以避免在头文件暴露私有细节,可以促进API接口和实现保持完全分离。

    在这里插入图片描述

    Pimpl可以将类的数据成员定义为指向某个已经声明过的类型的指针,这里的类型仅仅作为名字引入,并没有完整地定义,因此我们可以将该类型的定义隐藏在.cpp中,这被称为不透明指针。

    下面是一个自动定时器的API,会在被销毁时打印其生存时间。

    原有api

    // autotimer.h
    #ifdef _WIN32
    #include 
    #else
    #include 
    #endif
    
    #include 
    
    class AutoTimer
    {
    public:
           // 使用易于理解的名字创建新定时器
           explicit AutoTimer(const std::string& name); // xplicit避免隐式构造, 只能通过显示(explicit)构造.
           // 在销毁时定时器报告生存时间
           ~AutoTimer();
    private:
           // 返回对象已经存在了多久
           double GetElapsed() const;
    
           std::string mName;
    #ifdef _WIN32
           DWORD mStartTime;
    #else
           struct timeval mStartTime;
    #endif
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    这个API的设计包含如下几个缺点:

    1、包含了与平台相关的定义

    2、暴露了定时器在不同平台上存储的底层细节

    3、将私有成员声明在公有头文件中。(这是C++要求的)

    设计者真正的目的是将所有的私有成员隐藏在.cpp文件中,这样我们可以使用Pimpl惯用法了。

    将所有的私有成员放置在一个实现类中,这个类在头文件中前置声明,在.cpp中定义,下面是效果:

    autotimer.h

    // autotimer.h
    #include 
    class AutoTimer {
    public:
           explicit AutoTimer(const std::string& name);
           ~AutoTimer();
    private:
           class Impl;
           Impl* mImpl;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    构造函数需要分配AutoTimer::Impl类型变量并在析构函数中销毁。

    所有私有成员必须通过mImpl指针访问。

    autotimer.cpp

    // autotimer.cpp
    #include "autotimer.h"
    #include 
    #if _WIN32
    #include 
    #else 
    #include 
    #endif
    
    class AutoTimer::Impl 
    {
    public:
           double GetElapsed() const
           {
    #ifdef _WIN32
                  return (GetTickCount() - mStartTime) / 1e3;
    #else
                  struct timeval end_time;
                  gettimeofday(&end_time, NULL);
                  double t1 = mStartTime.tv_usec / 1e6 + mStartTime.tv_sec;
                  double t2 = end_time.tv_usec / 1e6 + end_time.tv_sec;
                  return t2 - t1;
    #endif
           }
           std::string mName;
    #ifdef _WIN32
           DWORD mStartTime;
    #else
           struct timeval mStartTime;
    #endif
    };
    
    AutoTimer::AutoTimer(const std::string& name) : mImpl(new AutoTimer::Impl())
    {
           mImpl->mName = name;
    #ifdef _WIN32
           mImpl->mStartTime = GetTickCount();
    #else
           gettimeofday(&mImpl->mStartTime, NULL);
    #endif
    }
    
    AutoTimer::~AutoTimer()
    {
           std::cout << mImpl->mName << ":took" << mImpl->GetElapsed()
                  << " secs" << std::endl;
           delete mImpl;
           mImpl = NULL;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    Impl的定义包含了暴露在原有头文件中的所有私有方法和变量。

    AutoTimer的构造函数分配了一个新的AutoTimer::Impl对象并初始化其成员,而析构函数负责销毁该对象。

    Impl类为AutoTimer的私有内嵌类,如果想让.cpp文件中的其他类或者自由函数访问Impl的话可以将其声明为共有的类。

    // autotimer.h
    #include 
    class AutoTimer {
    public:
           explicit AutoTimer(const std::string& name);
           ~AutoTimer();
           class Impl;
    private:
           Impl* mImpl;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如何规划Impl类中的逻辑?

    一般将所有的私有成员和私有方法放置在Impl类中,可以避免再公有头文件中声明私有方法。

    注意事项:

    不能在Impl类中隐藏私有虚函数,虚函数必须出现在公有类中,从而保证任何派生类都能重写他们

    pimpl的复制语义

    在c++中,如果没有给类显式定义复制构造函数和赋值操作符,C++编译器默认会创建,但是这种默认的函数只能执行对象的浅复制,这不利于类中有指针成员的类。

    如果客户复制了对象,则两个对象指针将指向同一个Impl对象,两个对象可能在析构函数中尝试删除同一个对象两次从而导致崩溃。

    下面提供了两个解决思路:

    1、禁止复制类,可以将对象声明为不可复制

    2、显式定义复制语义

    #include 
    class AutoTimer
    {
    public:
      explicit AutoTimer(const std::string& name);
      ~AutoTimer();
    private:
      AutoTimer(const AutoTimer&);
      const AutoTimer &operator=(const AutoTimer&);
      class Impl;
      Impl* mImpl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    智能指针优化Pimpl

    借助智能指针优化对象删除,这里采用共享指针or作用域指针。由于作用域指针定义为不可复制的,所以直接使用它还可以省掉声明私有复制构造和操作符的代码。

    #include 
    class AutoTimer
    {
    public:
      explicit AutoTimer(const std::string& name);
      ~AutoTimer();
    private:
      class Impl;
      boost::scoped_ptr<Impl> mImpl;
      // 如果使用shared_ptr就需要自己编写复制构造和操作符
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Pimpl优缺点总结

    优点:

    1、信息隐藏

    2、降低耦合

    3、加速编译:实现文件移入.cpp降低了api的引用层次,直接影响编译时间

    4、二进制兼容性:任何对于成员变量的修改对于Pimpl对象指针的大小总是不变

    5、惰性分配:mImpl类可以在需要时再构造

    缺点:

    1、增加了Impl类的分配和释放,可能会引入性能冲突。

    2、访问所有私有成员都需要在外部套一层mImpl→,这会使得代码变得复杂。

  • 相关阅读:
    小说推文和短剧推广以及电影达人带货电影票
    SoftwareTest6 - 用 Selenium 怎么点点点
    [2023.09.25]:Rust编写基于web_sys的编辑器:输入光标再次定位的小结
    第五次线上面试总结(2022.9.21 二面)
    对于非阻塞命名管道的测试 O_NONBLOCK
    在金融服务行业数字化转型中,低代码值得被关注
    Redis——Linux下安装以及命令操作
    GRS认证里 “收货人问题” 的最新解读
    fscan工具的使用
    七种 BeanDefinition,各显其能!
  • 原文地址:https://blog.csdn.net/qq_42604176/article/details/126078044