pimpl
pointer to implementation:指向實現的指針,使用該技巧可以避免在頭文件暴露私有細節,可以促進API接口和實現保持完全分離。
Pimpl可以將類的數據成員定義為指向某個已經聲明過的類型的指針,這里的類型僅僅作為名字引入,并沒有完整地定義,因此我們可以將該類型的定義隱藏在.cpp中,這被稱為不透明指針。
下面是一個自動定時器的API,會在被銷毀時打印其生存時間。
原有api
// autotimer.h
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif#include <string>class AutoTimer
{
public:// 使用易于理解的名字創建新定時器explicit AutoTimer(const std::string& name); // xplicit避免隱式構造, 只能通過顯示(explicit)構造.// 在銷毀時定時器報告生存時間~AutoTimer();
private:// 返回對象已經存在了多久double GetElapsed() const;std::string mName;
#ifdef _WIN32DWORD mStartTime;
#elsestruct timeval mStartTime;
#endif
};
這個API的設計包含如下幾個缺點:
1、包含了與平臺相關的定義
2、暴露了定時器在不同平臺上存儲的底層細節
3、將私有成員聲明在公有頭文件中。(這是C++要求的)
設計者真正的目的是將所有的私有成員隱藏在.cpp文件中,這樣我們可以使用Pimpl慣用法了。
將所有的私有成員放置在一個實現類中,這個類在頭文件中前置聲明,在.cpp中定義,下面是效果:
autotimer.h
// autotimer.h
#include <string.h>
class AutoTimer {
public:explicit AutoTimer(const std::string& name);~AutoTimer();
private:class Impl;Impl* mImpl;
};
構造函數需要分配AutoTimer::Impl類型變量并在析構函數中銷毀。
所有私有成員必須通過mImpl指針訪問。
autotimer.cpp
// autotimer.cpp
#include "autotimer.h"
#include <iostream>
#if _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endifclass AutoTimer::Impl
{
public:double GetElapsed() const{
#ifdef _WIN32return (GetTickCount() - mStartTime) / 1e3;
#elsestruct 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 _WIN32DWORD mStartTime;
#elsestruct timeval mStartTime;
#endif
};AutoTimer::AutoTimer(const std::string& name) : mImpl(new AutoTimer::Impl())
{mImpl->mName = name;
#ifdef _WIN32mImpl->mStartTime = GetTickCount();
#elsegettimeofday(&mImpl->mStartTime, NULL);
#endif
}AutoTimer::~AutoTimer()
{std::cout << mImpl->mName << ":took" << mImpl->GetElapsed()<< " secs" << std::endl;delete mImpl;mImpl = NULL;
}
Impl的定義包含了暴露在原有頭文件中的所有私有方法和變量。
AutoTimer的構造函數分配了一個新的AutoTimer::Impl對象并初始化其成員,而析構函數負責銷毀該對象。
Impl類為AutoTimer的私有內嵌類,如果想讓.cpp文件中的其他類或者自由函數訪問Impl的話可以將其聲明為共有的類。
// autotimer.h
#include <string.h>
class AutoTimer {
public:explicit AutoTimer(const std::string& name);~AutoTimer();class Impl;
private:Impl* mImpl;
};
如何規劃Impl類中的邏輯?
一般將所有的私有成員和私有方法放置在Impl類中,可以避免再公有頭文件中聲明私有方法。
注意事項:
不能在Impl類中隱藏私有虛函數,虛函數必須出現在公有類中,從而保證任何派生類都能重寫他們
pimpl的復制語義
在c++中,如果沒有給類顯式定義復制構造函數和賦值操作符,C++編譯器默認會創建,但是這種默認的函數只能執行對象的淺復制,這不利于類中有指針成員的類。
如果客戶復制了對象,則兩個對象指針將指向同一個Impl對象,兩個對象可能在析構函數中嘗試刪除同一個對象兩次從而導致崩潰。
下面提供了兩個解決思路:
1、禁止復制類,可以將對象聲明為不可復制
2、顯式定義復制語義
#include <string>
class AutoTimer
{
public:explicit AutoTimer(const std::string& name);~AutoTimer();
private:AutoTimer(const AutoTimer&);const AutoTimer &operator=(const AutoTimer&);class Impl;Impl* mImpl;
}
智能指針優化Pimpl
借助智能指針優化對象刪除,這里采用共享指針or作用域指針。由于作用域指針定義為不可復制的,所以直接使用它還可以省掉聲明私有復制構造和操作符的代碼。
#include <string>
class AutoTimer
{
public:explicit AutoTimer(const std::string& name);~AutoTimer();
private:class Impl;boost::scoped_ptr<Impl> mImpl;// 如果使用shared_ptr就需要自己編寫復制構造和操作符
}
Pimpl優缺點總結
優點:
1、信息隱藏
2、降低耦合
3、加速編譯:實現文件移入.cpp降低了api的引用層次,直接影響編譯時間
4、二進制兼容性:任何對于成員變量的修改對于Pimpl對象指針的大小總是不變
5、惰性分配:mImpl類可以在需要時再構造
缺點:
1、增加了Impl類的分配和釋放,可能會引入性能沖突。
2、訪問所有私有成員都需要在外部套一層mImpl→,這會使得代碼變得復雜。