簡體   English   中英

你如何在 C++ 中做“真正的”封裝?

[英]How do you do “true” encapsulation in C++?

封裝(信息隱藏)是一個非常有用的概念,它確保在類的 API 中只發布最少的細節。

但我不禁想到 C++ 這樣做的方式有點不足。 以一個(基於攝氏度的)溫度等級為例,如:

class tTemp {
    private:
        double temp;
        double tempF (double);
    public:
        tTemp ();
        ~tTemp ();
        setTemp (double);
        double getTemp ();
        double getTempF ();
};

現在,這是一個非常簡單的案例,但它說明了封裝並不完美的一點。 “真實”封裝會隱藏所有不必要的信息,例如:

  • 數據在temp變量(及其類型)中內部維護的事實。
  • 事實上,華氏/攝氏度轉換有一個內部例程。

因此,理想情況下,在我看來,該類的實現者將使用上述標頭,但該類的任何客戶端只會看到公共位。

不要誤會我的意思,我不是在批評 C++,因為它達到了防止客戶端使用私有位的既定目的,但是,對於更復雜的類,您可以根據名稱、類型和簽名輕松計算出內部細節私有數據和函數。

C++ 如何允許實現者隱藏這些信息(假設這可能的)? 在 C 中,我會簡單地使用不透明類型,以便隱藏內部細節,但是在 C++ 中您將如何做到這一點?

我想我可以維護一個單獨的類,對客戶端完全隱藏並且只有我自己的代碼知道,然后在可見類中保留一個帶有void *的實例(在我的代碼中強制轉換),但這似乎相當痛苦過程。 在 C++ 中是否有更簡單的方法來達到同樣的目的?

C++ 使用稱為“pimpl”(私有實現/指向實現的指針)的習語來隱藏實現細節。 有關詳細信息,請查看這篇 MSDN 文章

簡而言之,您可以像往常一樣在頭文件中公開您的接口。 讓我們以您的代碼為例:

溫度

class tTemp {
    private:
        class ttemp_impl; // forward declare the implementation class
        std::unique_ptr<ttemp_impl> pimpl;
    public:
        tTemp ();
       ~tTemp ();
       setTemp (double);
       double getTemp (void);
       double getTempF (void);
};

公共接口仍然存在,但私有內部結構已被替換為指向私有實現類的智能指針。 該實現類僅位於頭文件對應的 .cpp 文件中,不公開。

tTemp.cpp

class tTemp::ttemp_impl
{
    // put your implementation details here
}

// use the pimpl as necessary from the public interface
// be sure to initialize the pimpl!
tTtemp::tTemp() : pimpl(new ttemp_impl) {}

這還有一個額外的好處,即允許您在不更改標題的情況下更改類的內部結構,這意味着您的類用戶可以減少重新編譯。


對於 paxdiablo 的 pre-C++11 答案中所示的完整解決方案,但使用unique_ptr而不是void * ,您可以使用以下內容。 第一個ttemp.h

#include <memory>
class tTemp {
public:
    tTemp();
    ~tTemp();
    void setTemp(double);
    double getTemp (void);
    double getTempF (void);

private:
    class impl;
    std::unique_ptr<impl> pimpl;
};

接下來, ttemp.cpp的“隱藏”實現:

#include "ttemp.h"

struct tTemp::impl {
    double temp;
    impl() { temp = 0; };
    double tempF (void) { return temp * 9 / 5 + 32; };
};

tTemp::tTemp() : pimpl (new tTemp::impl()) {};

tTemp::~tTemp() {}

void tTemp::setTemp (double t) { pimpl->temp = t; }

double tTemp::getTemp (void) { return pimpl->temp; }

double tTemp::getTempF (void) { return pimpl->tempF(); }

最后, ttemp_test.cpp

#include <iostream>
#include <cstdlib>
#include "ttemp.h"

int main (void) {
    tTemp t;
    std::cout << t.getTemp() << "C is " << t.getTempF() << "F\n";
    return 0;
}

而且,就像 paxdiablo 的解決方案一樣,輸出是:

0C is 32F

具有更多類型安全性的額外優勢。 這個答案是 C++11 的理想解決方案,如果您的編譯器是 C++11 之前的,請參閱 paxdiablo 的答案。

以為我會充實 Don Wakefield 在他的評論中提到的“接口類/工廠”技術。 首先,我們從接口中抽象出所有實現細節,並定義一個僅包含Temp接口的抽象類:

// in interface.h:
class Temp {
    public:
        virtual ~Temp() {}
        virtual void setTemp(double) = 0;
        virtual double getTemp() const = 0;
        virtual double getTempF() const = 0;

        static std::unique_ptr<Temp> factory();
};

需要Temp對象的客戶調用工廠來構建一個。 工廠可以提供一些復雜的基礎設施,在不同的條件下返回接口的不同實現,或者像這個例子中的“只給我一個臨時”工廠一樣簡單。

實現類可以通過為所有純虛函數聲明提供覆蓋來實現接口:

// in implementation.cpp:
class ConcreteTemp : public Temp {
    private:
        double temp;
        static double tempF(double t) { return t * (9.0 / 5) + 32; }
    public:
        ConcreteTemp() : temp() {}
        void setTemp(double t) { temp = t; }
        double getTemp() const { return temp; }
        double getTempF() const { return tempF(temp); }
};

在某處(可能在同一個implementation.cpp )我們需要定義工廠:

std::unique_ptr<Temp> Temp::factory() {
    return std::unique_ptr<Temp>(new ConcreteTemp);
}

這種方法比 pimpl 更容易擴展:任何想要實現Temp接口的人都可以實現,而不是只有一個“秘密”實現。 還有一點樣板,因為它使用語言的內置機制進行虛擬分派來分派接口函數調用到實現。

我從 pugixml 庫中看到pugi::xml_document使用了一種非正統的方法,它沒有 pimpl 或抽象類的開銷。 它是這樣的:

您在公開公開的類中保留一個char數組:

class tTemp {
public:
    tTemp();
    ~tTemp();
    void setTemp(double);
    double getTemp();
    double getTempF();

    alignas(8) char _[8]; // reserved for private use.
};

注意

  • 本例中的對齊方式和大小是硬編碼的。 對於實際應用程序,您將使用表達式根據機器字的大小來估計它,例如sizeof(void*)*8或類似的。
  • 添加private不會提供任何額外的保護,因為對_任何訪問都可以用轉換為char*來替換。 提供封裝的是頭文件中缺少實現細節。

接下來,在翻譯單元中,您可以按如下方式實現tTemp

struct tTempImpl {
    double temp;
};
static_assert(sizeof(tTempImpl) <= sizeof(tTemp::_), "reserved memory is too small");

static double tempF(tTemp &that) {
    tTempImpl *p = (tTempImpl*)&that._[0];
    return p->temp * 9 / 5 + 32;
}

tTemp::tTemp() {
    tTempImpl *p = new(_) tTempImpl();
}

tTemp::~tTemp() {
    ((tTempImpl*)_)->~tTempImpl();
}

tTemp::tTemp(const tTemp& orig) {
    new(_) tTempImpl(*(const tTempImpl*)orig._);
}

void tTemp::setTemp(double t) {
    tTempImpl *p = (tTempImpl*)_;
    p->temp = t;
}

double tTemp::getTemp() {
    tTempImpl *p = (tTempImpl*)_;
    return p->temp;
}

double tTemp::getTempF() {
    return tempF(*this);
}

與其他提出的方法相比,這無疑更加冗長。 但這是我所知道的唯一一種零開銷方法,可以真正隱藏頭文件中的所有編譯時依賴項。 請注意,它還提供了一定程度的 ABI 穩定性——您可以更改tTempImpl只要其大小不超過保留內存。

有關 C++ 中封裝的更詳細討論,請參閱我的True encapsulation in C++博客文章。

私有實現 (PIMPL) 是 C++ 提供此功能的方式。 由於我無法使用 CygWin g++ 4.3.4 編譯unique_ptr變體,因此另一種方法是在可見類中使用void * ,如下所示。 這將允許您使用 C++11 之前的編譯器,以及前面提到的 gcc 之類的編譯器,這些編譯器只對 C++11 提供實驗性支持。

首先,頭文件ttemp.h ,客戶端包含的那個。 這不透明地聲明了內部實現結構,以便完全隱藏這些內部結構。 您可以看到,唯一顯示的細節是內部類和變量的名稱,它們都不需要顯示有關內部工作原理的任何信息:

struct tTempImpl;
class tTemp {
public:
    tTemp();
    ~tTemp();
    tTemp (const tTemp&);
    void setTemp(double);
    double getTemp (void);
    double getTempF (void);
private:
    tTempImpl *pimpl;
};

接下來,實現文件ttemp.cpp既聲明和定義了不透明的東西,也定義了用戶可見的細節。 由於用戶從未見過此代碼,因此他們不知道它是如何實現的:

#include "ttemp.h"

struct tTempImpl {
    double temp;
    tTempImpl() { temp = 0; };
    double tempF (void) { return temp * 9 / 5 + 32; };
};

tTemp::tTemp() : pimpl (new tTempImpl()) {
};

tTemp::~tTemp() {
    delete pimpl;
}

tTemp::tTemp (const tTemp& orig) {
    pimpl = new tTempImpl;
    pimpl->temp = orig.pimpl->temp;
}

void tTemp::setTemp (double t) {
    pimpl->temp = t;
}

double tTemp::getTemp (void) {
    return pimpl->temp;
}

double tTemp::getTempF (void) {
    return pimpl->tempF();
}

請注意,內部實現細節不受可見類本身的任何保護。 可以將內部定義為具有訪問器和修改器的類,但這似乎沒有必要,因為在這種情況下它應該是緊密耦合的。

上面的一句話:因為你使用一個指針來控制隱藏的方面,默認的淺拷貝構造函數會導致兩個可見對象引用同一個私有成員(導致析構函數中的雙重刪除) . 所以你需要(就像我一樣)提供一個深拷貝復制構造函數來防止這種情況。

最后,一個測試程序顯示了整個事情是如何結合在一起的:

#include <iostream>
#include "ttemp.h"

int main (void) {
    tTemp t;
    std::cout << t.getTemp() << "C is " << t.getTempF() << "F\n";
    return 0;
}

該代碼的輸出當然是:

0C is 32F

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM