C++中的pimpl用法(Pointer to Implementation)

定义

一种C++的编程手法,一方面可以减少编译依赖,对于大工程来说可以减少编译时间,一方面对于库来说可以减少私有成员的暴露,减少风险。

demo

// student.h
class Student
{
	Student(int age, string name, int sex);
	~Student();
	string show();
private:
	int m_age;
	string m_name;
	int m_sex;
}
// student.cpp
string Student::show(){return m_name;};
Student::Student(int age, string name, int sex):m_age(age),\
m_name(name), m_sex(sex){};

上面是一个学生类的定义和实现,现在如果想用这个学生类的有school.h , teacher.h, parent.h三个头文件使用,我们可以看到,一旦动了学生类,那么依赖的这三个类都要重新编译。

优化

// student.h
class StudentImpl;
class Student
{
	Student(int age, string name, int sex);
	~Student();
	string show();
private:
	shared_ptr<StudentImpl> m_student;
 }
//student_impl.h
class StudentImpl
{
StudentImpl(int age, string name, int sex);
~StudentImpl();
string name(){return m_name;};

private:
	int m_age;
	string m_name;
	int m_sex;
}
//student_impl.cpp
StudentImpl::StudentImpl(int age, string name, int sex):m_age(age),m_name(name), m_sex(sex){};

// student.cpp
#include "student_impl.h"
#include "student.h"
Student::Student(int age, string name, int sex)
{
   m_student = make_shared<StudentImpl>(age,name,sex);
};
string Student::show(){return student->name();};

这样的话,你就是student.cpp接口改翻天,此外你的实现也就是成员变量student_impl.h/cpp改翻天也不影响school.h , teacher.h, parent.h,因为他们理论来说编译的时候不依赖StudentImpl,这样在大型项目中,就可以加快编译速度,此外如果代码是提供对外的库比如opencv,一般会给一个头文件和so, 这里会有一个student.h,可是student.so外人压根不知道这个类的私有成员是啥,你也很难推出库函数的逻辑,保护了算法和知识产权。

paddle案例

位置:paddle/fluid/memory/allocation/thread_local_alloctor.h

class ThreadLocalAllocatorImpl;

class ThreadLocalAllocation : public Allocation {
 public:
  ThreadLocalAllocation(void* ptr, size_t size, platform::Place place)
      : Allocation(ptr, size, place) {}

  void SetThreadLocalAllocatorImpl(
      std::shared_ptr<ThreadLocalAllocatorImpl> allocator) {
    allocator_ = allocator;
  }  //给impl指针赋值

  std::shared_ptr<ThreadLocalAllocatorImpl> GetAllocator() {
    return allocator_;
  } //获取impl指针

 private:
  std::shared_ptr<ThreadLocalAllocatorImpl> allocator_;
};

class ThreadLocalAllocatorImpl
    : public std::enable_shared_from_this<ThreadLocalAllocatorImpl> {
 public:
  ThreadLocalAllocatorImpl(Place& p);
  ThreadLocalAllocation* AllocateImpl(size_t size);//不要觉得奇怪,一个类可以在自己内部声明一个指针的,这里可以理解为AllocateImpl是在ThreadLocalAllocation类中。
  void FreeImpl(ThreadLocalAllocation* allocation);//理解同上
  uint64_t ReleaseImpl();

 private:
  unique_ptr<BuddyAllocator> buddy_allocator_;//理解在ThreadLocalAllocation类中。
  Place place_;//同上
};

上面这段代码是抽取一个allocator相关代码,整个库中有非常非常多的这种,但是有个问题是ThreadLocalAllocatorImpl和ThreadLocalAllocator在一个文件中,正确的写法是ThreadLocalAllocatorImpl应该在thread_local_alloctor.cpp,如果按当下的写法一旦改变private成员,依赖这个thread_local_alloctor.h的文件都会重新编译,所以这个IMPL的意义是什么?是有什么高深的用法吗?不知道是大佬写错了还是我会错意了,希望懂得大神能留言解释一下。

<think>好的,我现在需要帮用户解决在C++开发中使用PimplPointer to Implementation)惯用法并结合类继承的问题。让我先理清楚用户的需求。用户可能是在设计一个类库,希望隐藏实现细节,同时还要支持继承,这样派生类可以扩展基类的功能。但Pimpl通常用于隐藏实现,而继承又需要暴露一些接口,这两者结合可能会遇到问题,比如派生类如何访问基类的实现对象,或者如何在保持封装的同时实现多态。 首先,我需要回忆Pimpl的基本结构。Pimpl通过将类的私有成员封装到一个实现类中,并通过指针来间接访问,从而减少编译依赖和隐藏实现细节。基类通常会有一个指向实现类的unique_ptr,而实现类则定义在.cpp文件中。但引入继承后,派生类可能需要访问基类的实现,或者重写虚函数,这时候如何处理呢? 接下来,考虑基类和派生类的关系。如果基类有虚函数,并且希望派生类能够重写,那么Pimpl的结构可能需要调整。通常,基类的接口会声明虚函数,但实现可能放在Impl类中。这里可能存在矛盾,因为虚函数的重写是在派生类中,而Impl类可能属于基类的实现细节,派生类无法直接访问。 可能的解决方案是,在基类中保留虚函数的声明,但将具体实现委托给Impl类。派生类在实现自己的Pimpl时,可能需要包含基类的Impl,或者有自己的派生Impl类。这里可能需要使用桥接模式,将接口和实现分离,同时允许派生类扩展实现。 另一个问题是,如何让派生类的Impl能够访问基类的Impl。可能需要让基类的Impl作为派生类Impl的成员,或者在Impl层次上也使用继承。例如,基类有一个BaseImpl,派生类有DerivedImpl继承自BaseImpl。这样,在派生类的Pimpl中,可以持有DerivedImpl的指针,而Base类的方法通过BaseImpl的虚函数调用,实现多态。 但这里需要注意,Impl类的继承和外部类的继承需要同步。也就是说,外部类继承层次和Impl的继承层次需要对应。例如,Derived类继承Base类,同时DerivedImpl继承BaseImpl。这样,在Base类中调用的Impl方法可以是虚函数,DerivedImpl可以重写这些方法。 然后考虑具体的代码结构。基类Base将有一个指向BaseImpl的指针,而派生类Derived将有一个指向DerivedImpl的指针,后者继承自BaseImpl。Base类中的虚函数可能通过Impl的虚函数来实现,这样当派生类的Impl重写这些函数时,基类的接口调用就会正确分派到派生类的实现。 还需要考虑构造和析构的问题。由于Pimpl使用unique_ptr,基类需要声明析构函数,即使它是默认的,因为Impl是前向声明的,否则编译器生成的析构函数可能无法正确销毁Impl对象。同样,派生类在析构时也需要正确处理Impl对象的销毁。 另外,关于头文件的包含和前置声明。基类和派生类的头文件应该只包含必要的声明,而Impl类的具体定义放在.cpp文件中。这样,用户包含头文件时不会看到实现细节。 可能的代码示例结构如下: 基类Base的头文件: ```cpp // Base.h class Base { public: Base(); virtual ~Base(); void publicMethod(); virtual void virtualMethod(); protected: struct BaseImpl; std::unique_ptr<BaseImpl> pImpl; }; ``` 基类实现文件: ```cpp // Base.cpp struct Base::BaseImpl { virtual ~BaseImpl() = default; virtual void doWork() { /* 基类实现 */ } void someMethod() { /* ... */ } }; Base::Base() : pImpl(std::make_unique<BaseImpl>()) {} Base::~Base() = default; void Base::publicMethod() { pImpl->someMethod(); } void Base::virtualMethod() { pImpl->doWork(); } ``` 派生类Derived的头文件: ```cpp // Derived.h class Derived : public Base { public: Derived(); void virtualMethod() override; private: struct DerivedImpl; // 可能需要重新定义pImpl为DerivedImpl类型,或者通过基类指针管理 }; ``` 这里遇到一个问题:派生类Derived的pImpl如果继续使用BaseImpl类型,那么无法访问DerivedImpl的特定方法。因此,可能需要将基类的pImpl改为使用基类Impl的指针,并且派生类的Impl继承自基类的Impl。这样,派生类的pImpl可以持有DerivedImpl对象,但通过基类Impl指针访问。 修改后的基类Base头文件: ```cpp // Base.h class Base { public: Base(); virtual ~Base(); void publicMethod(); virtual void virtualMethod(); protected: struct BaseImpl; std::unique_ptr<BaseImpl> pImpl; }; ``` 基类实现: ```cpp // Base.cpp struct Base::BaseImpl { virtual ~BaseImpl() = default; virtual void doWork() { /* 基类实现 */ } void someMethod() { /* ... */ } }; Base::Base() : pImpl(std::make_unique<BaseImpl>()) {} Base::~Base() = default; void Base::publicMethod() { pImpl->someMethod(); } void Base::virtualMethod() { pImpl->doWork(); } ``` 派生类Derived的实现: ```cpp // Derived.cpp struct Derived::DerivedImpl : public Base::BaseImpl { void doWork() override { /* 派生类实现 */ } }; Derived::Derived() : Base() { pImpl = std::make_unique<DerivedImpl>(); } void Derived::virtualMethod() { // 可能需要调用pImpl->doWork(),但此时pImpl已经是DerivedImpl类型 pImpl->doWork(); // 这里能否正确调用到DerivedImpl的doWork? } ``` 这里需要注意的是,基类Base中的pImpl类型是BaseImpl,而在派生类Derived中,我们将其替换为DerivedImpl,但由于unique_ptr的类型是BaseImpl,直接赋值可能需要类型转换。或者,我们需要在基类中使用某种方式允许派生类替换Impl的类型。 可能的解决方法是,在基类中提供一个函数来重置pImpl,或者在派生类的构造函数中重新构造pImpl为DerivedImpl类型。由于unique_ptr的模板类型是BaseImpl,所以需要DerivedImpl继承自BaseImpl,这样就可以将DerivedImpl指针赋值给基类的unique_ptr<BaseImpl>。 这样,当基类的virtualMethod调用pImpl->doWork()时,由于doWork是虚函数,实际调用的是DerivedImpl的doWork,这样就实现了多态。 但这里有一个问题:基类Base的virtualMethod的实现是调用pImpl->doWork(),而如果派生类Derived重写了virtualMethod,是否需要再次调用pImpl->doWork()?或者是否应该让基类的virtualMethod直接调用Impl的虚函数,而派生类只需要重写Impl的虚函数? 如果采用后者,那么基类的virtualMethod可能不需要被重写,而是通过Impl的虚函数实现多态。这样,派生类只需要在构造时替换pImpl为DerivedImpl,并重写Impl的doWork方法即可。这种情况下,基类的virtualMethod可以是非虚的,或者保持虚函数但实现不变。 这可能更符合Pimpl的设计,将实现细节完全隐藏在Impl类中,外部类的虚函数通过Impl的虚函数来实现多态。这样,派生类只需要提供不同的Impl实现即可。 例如,基类Base的virtualMethod可能不是虚函数,而Impl的doWork是虚函数。或者,基类的virtualMethod是虚函数,但实现中调用Impl的非虚函数。这需要进一步分析。 可能更好的方式是,外部类(Base和Derived)处理公共接口和继承关系,而Impl类处理具体实现,Impl类之间也可以有继承关系以支持多态。这样,当Derived类继承Base类时,其Impl类可以继承自Base的Impl类,重写其中的虚函数,而外部类的虚函数可以调用Impl的虚函数,实现多态。 总结步骤: 1. 基类定义Impl结构体,包含虚函数。 2. 基类的成员函数通过Impl的虚函数实现功能。 3. 派生类定义自己的Impl结构体,继承自基类的Impl,并重写虚函数。 4. 派生类在构造函数中初始化pImpl为派生类Impl的实例。 5. 外部类的虚函数根据需要,可以调用Impl的虚函数,利用多态实现不同行为。 需要注意的是,外部类(如Base和Derived)的虚函数是否需要重写,取决于设计需求。如果希望外部类也支持多态,可能需要同时使用外部类的虚函数和Impl的虚函数。 可能还需要处理对象切割的问题,确保Impl的指针正确管理派生类对象。 最后,编写示例代码,验证这种结构是否可行,并检查是否存在问题,比如析构函数是否正确调用,unique_ptr的管理是否正确,以及多态是否按预期工作。</think>在C++中结合Pimpl用法和类继承时,需分层处理接口和实现。以下是分步实现方案: --- ### 一、基础结构设计 #### 1. 基类定义 ```cpp // Base.h #include <memory> class Base { public: Base(); virtual ~Base(); // 虚析构保证多态正确性 void publicMethod(); virtual void virtualMethod(); // 需被子类重写的接口 protected: struct BaseImpl; // 前置声明 std::unique_ptr<BaseImpl> pImpl; // 统一通过基类Impl指针操作 }; ``` #### 2. 基类实现 ```cpp // Base.cpp struct Base::BaseImpl { virtual ~BaseImpl() = default; // 基类Impl虚析构 virtual void doWork() { /* 默认实现 */ } void helper() { /* 工具方法 */ } }; Base::Base() : pImpl(std::make_unique<BaseImpl>()) {} Base::~Base() = default; void Base::publicMethod() { pImpl->helper(); } void Base::virtualMethod() { pImpl->doWork(); } // 通过Impl虚函数实现多态 ``` --- ### 二、派生类实现 #### 1. 派生类定义 ```cpp // Derived.h class Derived : public Base { public: Derived(); void virtualMethod() override; // 重写基类接口 private: struct DerivedImpl; // 无需暴露实现细节 }; ``` #### 2. 派生类实现 ```cpp // Derived.cpp struct Derived::DerivedImpl : public Base::BaseImpl { void doWork() override { /* 自定义实现 */ } }; Derived::Derived() { pImpl = std::make_unique<DerivedImpl>(); // 替换Impl为派生类实现 } void Derived::virtualMethod() { pImpl->doWork(); // 直接调用已重写的Impl方法 // 可添加派生类特有逻辑 } ``` --- ### 三、关键点解析 1. **Impl继承体系** - 派生类`DerivedImpl`继承自基类`BaseImpl`,通过重写`doWork()`实现多态。 - 外部类`Derived`的`pImpl`实际指向`DerivedImpl`对象,但通过基类指针操作。 2. **构造过程** - 派生类构造函数中需显式初始化`pImpl`为`DerivedImpl`对象。 - 基类构造函数先初始化`pImpl`为`BaseImpl`,派生类通过赋值覆盖。 3. **多态行为** - 用户调用`virtualMethod()`时,实际调用`pImpl->doWork()`。 - 由于`DerivedImpl`重写了`doWork()`,多态行为正确触发。 4. **内存管理** - `unique_ptr`自动管理内存,基类需显式声明析构函数(因`BaseImpl`是不完整类型)。 - `virtual ~Base()`确保派生类析构函数被正确调用。 --- ### 四、使用示例 ```cpp int main() { Base* obj = new Derived(); obj->virtualMethod(); // 调用DerivedImpl::doWork() delete obj; return 0; } ``` --- ### 五、潜在问题及解决 1. **跨层级访问** - 若派生类Impl需访问基类Impl的成员,需将基类Impl数据声明为`protected`。 2. **性能考量** - 虚函数调用增加间接性,但对大多数场景影响可忽略。 3. **扩展性** - 新增派生类时只需继承基类并实现自己的Impl,符合开闭原则。 --- 通过此结构,Pimpl用法与类继承和谐共存,既隐藏了实现细节,又支持了多态扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值