c++ 類之間的依賴問題:impl、代理模式


參考

Effective_C++第三版.pdf
Effective_Modern_C__.pdf

描述

類似於托管的方式來解決幾個問題:

  1. 減少編譯時間
  2. 解決循環引用
  3. 屏蔽內部實現

減少編譯時間,本質降低依賴

​ 因為c++是靜態編譯語言,他看的就是文件和文件之間的依賴,如果是實例 type a,那么就一定需要include type相關頭文件,這樣導致一件事情:當多重依賴的時候,很可能基層類的小改動,導致所有包括這個類的大類都需要重新編譯,注意:(Run和Date是隨意寫的兩個類)

例子1:

#include "Run.h"
#include "Date.h"

class Test
{
public:
    Test(Run& run, Date& date);
    std::string g
    Run run;
    Date date;
};

​ 對於這種帶來的就是 當Test這個類依賴的頭文件改變,或者這些頭文件依賴的其他頭文件改變的時候,每一個含有Test的文件都要重新編譯,使用Test的文件也要重新編譯,這將帶來的是連串編譯依存關系

解決辦法 : 前置聲明

​ 首先對於標准庫無法使用類似 class string的方式,因為 string不是class,是一個typedef, 涉及到 template,但是你應該不對標准庫進行前置聲明,因為他們幾乎不會改變

​ 其次,c++這種語言編譯的時候需要知道對象的大小,也就是 sizeof你得出來准確的值,那么要求你類中的變量類型都是確定的

​ 解決辦法就是:

​ 使用引用/指針,因為引用/指針的大小是固定的 指針大小,並且對於java這些他的成員變量其實也是指向地址的類型

class Run;
class Date;
class Test
{
public:
    Test(Run& run, Date& date);
    Run& run;
    Date& date;
};
  1. 當然你使用者肯定要包含Run和Date類,但是還是推薦智能指針,想不出來不用的理由

成員變量的IMPL

​ 上面的用引用保留變量其實不常見,一般都是用指針,但是指針又有釋放的問題,那么就使用智能指針,類中定義Impl的結構體,包含所有必要的成員變量,但是這里不去體現,頭文件中僅僅進行必要的class 聲明

​ weight.h

#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:
    Weight();
private:
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

#endif // WEIGHT_H

weight.cpp

#include "Weight.h"
#include <vector>
#include <string>

struct Weight::Impl {
    std::string name;
    std::vector<double> data;
};

Weight::Weight()
    : m_impl(new Impl())
{

}

將所有需要實例化的成員變量創建一個結構體結構體指針使用unique_ptr管理!!!

但是這種方式在實例化weight的時候會出問題,因為unique_ptr內部默認析構器會對指針類型進行判斷如果是不完全的類型會進行報錯,為啥會不完全呢,因為編譯器默認的析構函數是在頭文件隱式內聯的,在頭文件中當然看不到具體類型

解決辦法是:

讓析構的時候看到完整類型唄,也就是析構實現的時候看到結構體是完成的,所以將weight的析構函數移到.cpp中

#include "Weight.h"
#include <vector>
#include <string>

struct Weight::Impl {
    std::string name;
    std::vector<double> data;
};

Weight::Weight()
    : m_impl(new Impl())
{

}
Weight::~Weight() {

}

​ 也可以使用 ~Weight() = default; 相當於實現使用默認的編譯器生成代碼

#include "Weight.h"
#include <vector>
#include <string>

struct Weight::Impl {
    std::string name;
    std::vector<double> data;
};

Weight::Weight()
    : m_impl(new Impl())
{

}

Weight::~Weight() = default;

那么析構有影響,拷貝構造和賦值操作符呢?

我們都知道,當聲明了析構函數,編譯器就不會給我們默認生成移動操作符函數,需要我們顯示聲明

那么對於下面的

#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:
    Weight();
    ~Weight();

    Weight(Weight&& rhs) = default;
    Weight& operator=(Weight&& rhs) = default;
private:
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

#endif // WEIGHT_H

因為unique_ptr的原因,我們只能使用默認的移動操作符

然而在

#include <iostream>     // std::streambuf, std::cout
#include "Weight.h"
int main () {
    Weight w;
    Weight c;
    w = std::move(c);
    return 0;
}

報錯了,原因是在 移動操作符的默認實現中 會對原有的進行delete處理,這就和析構函數相同了,不完整類型

解決辦法就是換個地方,在.h中統一聲明

#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:
    Weight();
    ~Weight();

    Weight(Weight&& rhs);
    Weight& operator=(Weight&& rhs);
private:
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

#endif // WEIGHT_H

#include "Weight.h"
#include <vector>
#include <string>

struct Weight::Impl {
    std::string name;
    std::vector<double> data;
};

Weight::Weight()
    : m_impl(new Impl())
{

}

Weight::~Weight() = default;
Weight::Weight(Weight&& rhs) = default;
Weight& Weight::operator=(Weight&& rhs) = default; 

為了保證賦值操作符可以正常使用,我們必須手工自己進行實現

Weight& Weight::operator=(const Weight& rhs) {
    if (this != &rhs) {
        *m_impl = *rhs.m_impl;
    }
    return *this;
}  

我們使用這種賦值方式,讓結構體內部進行賦值,注意的是 內存是兩塊內存,只不過現在內容是一樣的了

值得一提的shared_ptr和unique_ptr

​ 上面例子2中的unique_ptr的種種,換成shared_ptr后都不需要了

#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:
    Weight();   
private:
    struct Impl;
    std::shared_ptr<Impl> m_impl;
};

#endif // WEIGHT_H

#include "Weight.h"
#include <vector>
#include <string>

struct Weight::Impl {
    std::string name;
    std::vector<double> data;
};

Weight::Weight()
    : m_impl(new Impl())
{

}

但是呢,是有代價的。對於unique_ptr他的析構器是智能指針的一部分,因為一開始就可以確定下來,這讓編譯器可以快速執行代碼,這就要求編譯時候看到的指針類型是完全的;對於shared_ptr,他的內部析構器不是智能指針的一部分,屬於control Block的一部分,所以這也帶來的編譯器無法優化、減少代碼大小

注意一點,我們使用類變量托管的方式解決依賴,但是客戶端有的時候就是要Impl里面變量的具體實現,比如

循環引用

​ 這里的循環引用說的不是shared_ptr的那個,說的是頭文件的互相包含,這時候可以選擇的是 A include B,B 頭文件中 class A,進行聲明

從類之間的角度進行IMPL

​ 上面我們說了 單個成員變量使用指針減少依賴、類中定義結構體較少所有成員變量的具體依賴,那么類之間的IMPL呢?

其實這個作用在設計模式上還有 叫 代理模式,還有的叫不和陌生人說話,本質就是在 兩者之間 加入第三個類來解決兩個類的互相依賴

將類分成兩部分,一個負責提供接口一個負責提供實現,注意了我們這里說的可沒有帶繼承這種類關系

那么例子:還是上面的Weight,但是使用WeightProxy進行IMPL

WeightProxy頭文件

#ifndef WEIGHTPROXY_H
#define WEIGHTPROXY_H
#include <string>
#include <memory>
class Weight;

class WeightProxy
{
public:
    WeightProxy();
    ~WeightProxy();
    std::string GetInfo();
    std::unique_ptr<Weight> m_proxy;
};

#endif // WEIGHTPROXY_H

WeightProxy實現文件

#include "Weightproxy.h"
#include "Weight.h"
WeightProxy::WeightProxy()
    : m_proxy(new Weight())
{
}

WeightProxy::~WeightProxy() = default;

std::string WeightProxy::GetInfo() {
    return m_proxy->GetInfo();
}

Main函數

#include <iostream>     // std::streambuf, std::cout
#include "Weightproxy.h"
int main () {
    WeightProxy w;
    std::cout << w.GetInfo() << std::endl;
    return 0;
}

對於客戶端完全看不到類的具體實現,這也就 面對接口編程

基本原則是:

  1. 如果使用指針或者引用可以實現,就不要用Object,因為定義某類型的Object需要類型的定義式,而前者只需要聲明

  2. 盡量用聲明替換定義,當你聲明一個函數,並且他用到某個class時,不需要該class的定義,即使函數是 Object傳遞參數或者返回值

    class Test;
    Test getTest();
    void setTest(Test obj);
    

    注意了,變量只有是指針我們才能使用聲明,但是函數卻沒有這個限制,即使是對象也可使用聲明,本質是函數編譯不依賴於實現,但是調用函數之前,Test定義式一定要存在,重要的目的是把這種include形式傳遞到客戶調用函數的那個文件中,將類型定義和客戶端依賴去除,說白了庫的提供者一個類中會提供很多函數,因為庫的提供者選擇 class形式,那么對於客戶端只有需要 知道Test的具體定義的才去包含Test頭文件,減少不必要的依賴

  3. 為聲明式和定義式提供不同頭文件

    因為定義式里面包含的頭文件的真實實現,客戶端不應該自己手工class聲明,而是庫實現側自己提供兩種頭文件一個是聲明、一個是定義聲明文件就是給客戶端像include的形式使用聲明,也就是聲明文件的內容就僅僅是 class Test

所以 接口實現的文件中的函數要和 接口頭文件相同,什么以來具體實現都是 接口實現文件Proxy考慮的事情

構造函數中依然傳遞 Weight需要的參數(使用class聲明),但是這個依賴就丟給了客戶端,這就表示事情還是會做,只不過方式改變了,這是一種Handle class的方式

4. 另一種方式:

純虛類

在java 和 .NET中,就有專門的interface定義,里面不能有成員變量,實現的成員函數等

因為這樣,使用這個純虛類的客戶,必須用指針/引用使用應用程序,因為無法定義實例,那么這樣的話除非接口修改否則客戶也不需要重新編譯

並且,一般考慮使用工廠模式來創建這種類型的對象,這種工廠函數一般在接口類中定義為靜態,通過參數不同生成不同的函數

class Test
{
public:
    static Test* getInstanse(std::string type);
    virtual std::string getRunString() const = 0;
};

還是推薦返回智能指針,這樣返回不同的派生類實例

最后總結

  1. 使用IMPL方式來較少類之間的依賴,減少編譯時間
  2. 變量可以使用指針,一大推變量使用結構體,類可以使用一個托管類,大致這三類型來實現減少依賴
  3. 其實本質上來說,頭文件之間就不應該有定義的依賴,所以java中統一使用了指針,實現cpp中才是真正包含所有具體定義,頭文件是用來聲明這個類長什么樣子,實現cpp中用來實現這個類內部怎么實現的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM