1. 程式人生 > >qt creator原始碼全方面分析(2-1-1)

qt creator原始碼全方面分析(2-1-1)

目錄

  • C++的策略/二進位制相容性問題
    • 定義
    • ABI注意事項
    • 可做與不可做
    • 庫程式設計師的技巧
      • 位標誌
      • 使用d指標
    • 故障排除
      • 在沒有d指標的情況下將新資料成員新增到類中
      • 新增已重新實現的虛擬函式
      • 使用新類
      • 向葉節點類新增新的虛擬函式
      • 使用訊號代替虛功能

C++的策略/二進位制相容性問題

我們在coding-style中提到了C++二進位制相容性問題,這裡我們也來學習下。


定義

庫是二進位制相容的,如果動態連結到該庫的舊版本的程式,無需重新編譯,就可以與該庫的新版本一起執行。

庫是原始碼相容的,如果對於庫的新版本,程式需要重新編譯才能執行,但不需要任何進一步的修改。

二進位制相容性解決了很多麻煩。 它使為特定平臺分發軟體變得更加容易。 如果不能確保發行版之間的二進位制相容性,人們將不得不提供靜態連結的二進位制檔案。 靜態二進位制檔案很糟糕,因為它們

  • 浪費資源(尤其是記憶體)
  • 程式不能從庫的bug修復或功能擴充套件中受益

在KDE專案中,對於核心庫(kdelibs,kdepimlibs)的主要發行版的生命週期,我們提供二進位制相容性。

ABI注意事項

本文適用於KDE構建用的編譯器使用的大多數C++ ABI。它主要基於Itanium C++ ABI草案,GCC C++編譯器從3.4版本開始使用。 有關Microsoft Visual C++名稱改編方案的資訊主要來自於關於呼叫約定的文章(這是迄今為止有關MSVC ABI和名稱改編的最完整資訊)。

此處指定的某些約束可能不適用於給定的編譯器。 此處的目標是列出最嚴格的一組條件,用於編寫跨平臺C++程式碼,這意味著會被多種不同的編譯器編譯。

發現新的二進位制不相容問題時,此頁面會同步更新。

可做與不可做

你可以...

  • 新增新的非虛擬功能,包括訊號和槽以及建構函式。
  • 在類中新增一個新的列舉。
  • 將新的列舉值追加到現有列舉。

    • 例題:如果這導致編譯器為列舉選擇了更大的基礎型別,則這更改是二進位制不相容的。不幸的是,編譯器有一些選擇基礎型別的餘地,因此從API設計的角度來看,建議新增一個Max....列舉值,並顯式設定一個較大的值(=255, =1<<15, 等),以建立一個保證可以適合所選基礎型別的列舉值的範圍。
  • 重新實現在類層次結構中原始基類定義的虛擬函式(該虛擬函式定義在第一個非虛基類,或該類的第一個非虛基類中,依此類推),如果程式連結到先前版本的庫,且該庫呼叫了基類中的實現而不是派生類中的實現,是安全的。這很棘手,可能很危險。三思而後行。或者,請參閱下面的解決方法。
    • 例外:如果重新實現的函式具有協變返回型別,則如果派生更多的型別始終與派生較小的型別具有相同的指標地址,則它僅是二進位制相容性的變動。如有疑問,請勿使用協變返回型別來進行覆寫。
  • 更改行內函數或使行內函數變為非內聯,如果程式連結到先前版本的庫,且該庫呼叫了舊的實現,是安全的。這很棘手,可能很危險。三思而後行。
  • 移除私有非虛擬函式,如果它們沒有被任何行內函數呼叫過(並且從未使用過)。
  • 移除私有靜態成員變數,如果它們沒有被任何行內函數呼叫過(並且從未使用過)。
  • 新增新的靜態資料成員變數。
  • 更改方法的預設引數。但是,它需要重新編譯才能使用實際的新的預設引數值。
  • 新增新類。
  • 匯出以前未匯出的類。
  • 在類中新增或刪除友元宣告。
  • 重新命名保留的成員型別。
  • 擴充套件保留的位欄位,前提是這不會導致位欄位超出其基礎型別的邊界(8 bits for char & bool, 16 bits for short, 32 bits for int, etc.)
  • 將Q_OBJECT巨集新增到類中,如果該類已經從QObject繼承。
  • 新增Q_PROPERTY,Q_ENUMS或Q_FLAGS巨集,因為它僅修改由moc生成的元物件,而不修改類本身。

你不可以...

  • 對於現有類:
    • 取消匯出或刪除已匯出的類。
    • 以任何方式更改類層次結構(新增,刪除或重新排序基類)。
    • 移除finality。
  • 對於模板類:
    • 以任何方式更改模板引數(新增,刪除或重新排序)。
  • 對於任意型別的現有函式:
    • 取消匯出。
    • 移除。
      • 移除已宣告函式的實現。符號來自函式的實現,因此實際上就是函式。
    • 變為內聯(這包括將成員函式的主體移至類定義,即使沒有inline關鍵字也是如此)
    • 新增一個新的過載函式(BC,而不是SC:使&func變得模稜兩可),將過載函式新增到已經被過載的函式中是可以的(對&func的任何使用都已經需要強制轉換)。
    • 更改其簽名。這包括:
      • 更改引數列表中引數的任意型別,包括更改現有引數的const/volatile限定符(代替,新增新方法)
      • 更改函式的const/volatile限定符
      • 更改某些函式或資料成員變數的訪問許可權,例如從私有公有。對於某些編譯器,此資訊可能是簽名的一部分。如果需要使私有函式變為受保護的甚至是共有的,則必須新增一個新函式,來呼叫此私有函式。
      • 更改成員函式的CV限定符:應用於函式本身的const和/或volatile。
      • 使用其他引數擴充套件函式,即使此引數具有預設引數。請參閱以下有關如何避免此問題的建議。
      • 以任何方式更改返回型別
      • 例外:用extern "C"宣告的非成員函式可以更改引數型別(請小心)。
  • 對於虛成員函式:
    • 向沒有任何虛擬函式或虛基類的類中新增虛擬函式。
    • 向非葉節點的類新增新的虛擬函式,因為這會破壞子類。請注意,設計為用於應用程式子類化的類始終是非葉類。請參閱下面的一些解決方法,或在郵件列表中詢問。
    • 以任何理由新增新的虛擬函式,甚至在葉節點類中,如果該類旨在Windows上保持二進位制相容性。這樣做可能會重排現有的虛擬函式並破壞二進位制相容性。
    • 在類宣告中更改虛擬函式的順序。
    • 覆寫已存在的虛擬函式,如果該函式不在原始基類中(第一個非虛基類,或原始基類的原始基類,一路往上)。
    • 覆寫已存在的虛擬函式,如果過載函式具有協變數返回型別,而其高派生型別的指標地址與低派生的指標地址不同(通常發生在,在低派生和高派生之間,有多重繼承或虛繼承)。
    • 移除虛擬函式,即使它是基類虛擬函式的重新實現。
  • 對於靜態非私有成員或非靜態非成員公有資料:
    - 移除或取消匯出
    - 更改其型別
    - 更改其CV限定符
  • 對於非靜態成員變數:
    - 新增新的資料成員到現有的類中。
    - 更改類中非靜態資料成員的順序。
    - 更改成員的型別(變數名符號除外)
    - 從已有的類中刪除已有的非靜態資料成員。

如果需要新增擴充套件/修改現有函式的引數列表,則需要新增新函式,而不是新引數。在這種情況下,您可能想新增簡短說明,在庫的更高版本中,這兩個函式應通過預設引數進行合併:

void functionname( int a );
void functionname( int a, int b ); //BCI: merge with int b = 0

你應該...

為了類在將來可擴充套件,您應該遵循以下規則:

  • 新增d指標。見下文。
  • 新增非內聯虛解構函式,即使主體為空。
  • 在QObject派生的類中重新實現event,即使新函式的主體只是呼叫基類的實現。這是專門為避免,因新增已重新實現的虛擬函式而引起的問題,如下所述。
  • 使所有建構函式非內聯。
  • 編寫拷貝建構函式和賦值運算子的非內聯實現,除非類無法進行值拷貝(例如,從QObject繼承的類是不能的)

庫程式設計師的技巧

編寫庫時最大的問題是,不能安全地新增資料成員,因為這會改變每個class類,struct結構,或者物件型別陣列的大小和佈局。

位標誌

位標誌是一種例外。 如果對列舉或布林使用位標誌,則至少在下一個位元組減去1bit之前是安全的。具有下面成員的類

uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
uint m4 : 2; // new member

不會破壞二進位制相容性。 請四捨五入到最多7位(如果已經大於8,則為15位)。使用最後一位可能會在某些編譯器上引起問題。

使用d指標

位標記和預定義的保留變數很好,但遠遠不夠。這就是d指標技術發揮作用的地方。"d指標"的名稱源於Trolltech's Arnt Gulbrandsen,他首先將該技術引入到Qt,使其成為最早的C++ GUI庫之一,用於在更大的發行版之間保持二進位制相容性。看到它的每個人都迅速將該技術用作KDE庫的通用程式設計模式。這是一個絕妙的技巧,能夠在不破壞二進位制相容性的情況下將新的私有資料成員新增到類中。

備註:d指標模式在計算機科學歷史上已經以不同的名稱被多次描述過,例如pimpl,handle/body或cheshire cat。Google可以幫助您找到其中任何一種的線上論文,只需將C++新增到搜尋詞中即可。

在類Foo的定義中,定義一個前向宣告

class FooPrivate;

和私有成員中的d指標:

private:
    FooPrivate* d;

FooPrivate類本身完全定義在類實現檔案(通常為*.cpp)中,例如:

class FooPrivate {
public:
    FooPrivate()
        : m1(0), m2(0)
    {}
    int m1;
    int m2;
    QString s;
};

您現在要做的就是,在建構函式或初始化函式中使用以下方法建立私有資料:

d = new FooPrivate;

並在解構函式中將其刪除

delete d;

在大多數情況下,您將需要使d指標為const,以捕獲意外修改或拷貝它的情況,這時將失去對私有物件的所有權,並造成記憶體洩漏:

private:
    FooPrivate* const d;

這使您可以修改d指向的物件,但不能在初始化後修改d的值。

但是,您可能不希望所有成員變數都存在於私有資料物件中。對於經常使用的成員,將它們直接放入類中會更快,因為行內函數無法訪問d指標資料。還要注意,儘管在d指標本身中已宣告為公有,但d指標所涵蓋的所有資料都是私有的。對於公有或受保護的訪問,請同時提供set和get函式。例如

QString Foo::string() const
{
    return d->s;
}

void Foo::setString( const QString& s )
{
    d->s = s;
}

也可以將d指標的私有類宣告為巢狀的私有類。如果使用此技術,請記住,巢狀的私有類將繼承包含的匯出類的公有符號可見性。這將導致私有類的函式在動態庫的符號表中被命名。您可以在巢狀私有類的實現中使用Q_DECL_HIDDEN來手動重新隱藏符號。從技術上講,這是ABI變動,但不會影響KDE開發人員支援的公共ABI,因此私有符號錯誤可能重新隱藏,而不會發出進一步的警告。

故障排除

在沒有d指標的情況下將新資料成員新增到類中

如果您沒有自由的位標誌,保留的變數並且也沒有d指標,但是您必須新增一個新的私有成員變數,那麼仍然存在一些可能性。如果您的類繼承自QObject,則可以例如將其他資料放在一個特殊的子物件中,並通過遍歷子物件列表來查詢它們。您可以使用QObject::children()訪問子列表。但是,更簡便,通常更快的方法是使用雜湊表儲存物件與額外資料之間的對映。為此,Qt提供了一個基於指標的字典,稱為QHash(或Qt3中的Templat::Qt3)。

在Foo類的實現中的基本技巧是:

  • 建立一個私有資料類FooPrivate。

  • 建立一個靜態QHash<Foo , FooPrivate >。

  • 請注意,有些編譯器/連結器(不幸的是,幾乎所有的)都無法在動態庫中建立靜態物件。他們只是忘了呼叫建構函式。因此,您應該使用Q_GLOBAL_STATIC巨集來建立和訪問該物件:

    // BCI: Add a real d-pointer
    typedef QHash<Foo *, FooPrivate *> FooPrivateHash;
    Q_GLOBAL_STATIC(FooPrivateHash, d_func)
    static FooPrivate *d(const Foo *foo)
    {
        FooPrivate *ret = d_func()->value(foo);
        if ( ! ret ) {
            ret = new FooPrivate;
            d_func()->insert(foo, ret);
        }
        return ret;
    }
    static void delete_d(const Foo *foo)
    {
        FooPrivate *ret = d_func()->value(foo);
        delete ret;
        d_func()->remove(foo);
    }
  • 現在,您可以像以前的程式碼一樣簡單地在類中使用d指標,只需呼叫d(this)即可。例如:

    d(this)->m1 = 5;
  • 在解構函式中新增一行:

    delete_d(this);
  • 不要忘記新增一個BCI註釋,以便可以在庫的下一版本中刪除該hack。

  • 不要忘記在下一個類中新增d指標。

新增已重新實現的虛擬函式

正如已經說明的,你可以安全的重新實現定義在其中一個基類中的虛擬函式,如果程式連結到先前版本的庫,且該庫呼叫了基類中的實現而不是派生類中的實現,是安全的。這是因為如果編譯器可以確定要呼叫哪個虛擬函式,則有時會直接呼叫該虛擬函式。例如,如果您有

void C::foo()
{
    B::foo();
}

那麼B::foo()直接被呼叫。如果類B繼承自實現了foo()函式的類A,而B本身未重新實現,則 C::foo() 實際上將呼叫A::foo()。如果該庫的較新版本添加了B::foo(),則C::foo() 僅在重新編譯後才呼叫B::foo() 。

另一個更常見的示例是:

B b;        // B derives from A
b.foo();

那麼對foo()的呼叫將不會使用虛擬表。這意味著如果庫中以前不存在B::foo(),但現在存在了,則使用較早版本庫進行編譯的程式碼仍將呼叫A::foo()。

如果不能保證無需重新編譯就能繼續工作,請將函式功能從A::foo()移至新的受保護函式A::foo2(),並使用以下程式碼:

void A::foo()
{
    if( B* b = dynamic_cast< B* >( this ))
        b->B::foo(); // B:: is important
    else
        foo2();
}
void B::foo()
{
    // added functionality
    A::foo2(); // call base function with real functionality
}

型別B(或繼承)的物件對A::foo()的所有呼叫將導致呼叫B::foo()。唯一無法正常工作的情況是對A::foo()的呼叫,該呼叫顯式指定了A::foo(),但B::foo()則呼叫了A::foo2(),其他地方別這樣做。

使用新類

一種相對簡單的“擴充套件”類的方法是編寫一個替換類,該替換類還將包括新功能(可能從舊類繼承程式碼以重複利用)。當然,這需要使用該庫來適應和重新編譯應用程式,因此這種方法不可能用來修復或擴充套件類的功能,該類是應用程式編譯用的舊版本庫中的類。 但是,特別是對於小型的和/或效能至關重要的類,編寫它們可能會更簡單,而不必確保它們將來會易於擴充套件;如果以後需要,可編寫一個新的替代類,以提供新的功能或更好的效能。

向葉節點類新增新的虛擬函式

這種技術是使用新類的一種情況,這對向類中新增新的虛擬函式有幫助,該類必須保持二進位制相容性,而該類的繼承類沒必要繼續保持二進位制相容性(即所有的繼承類都在應用程式中)。在這種情況下,可以新增一個繼承自原始類的新類,並將其新增進來。當然,使用新功能的應用程式必須進行修改以使用新類。

class A {
public:
    virtual void foo();
};
class B : public A { // newly added class
public:
    virtual void bar(); // newly added virtual function
};
void A::foo()
{
    // here it's needed to call a new virtual function
    if( B* this2 = dynamic_cast< B* >( this ))
        this2->bar();
}

當還有其他的繼承類也必須保持二進位制相容性時,則無法使用此技術,因為它們不得不從新類繼承。

使用訊號代替虛功能

Qt的訊號和槽設計由Q_OBJECT巨集建立的特殊的虛擬函式呼叫,它存在於從QObject繼承的每個類中。因此,新增新的訊號和槽不會影響二進位制相容性,並且可以使用訊號/槽機制來模擬虛擬函式。

class A : public QObject {
Q_OBJECT
public:
    A();
    virtual void foo();
signals:
    void bar( int* ); // added new "virtual" function
protected slots:
    // implementation of the virtual function in A
    void barslot( int* );
};

A::A()
{
    connect(this, SIGNAL( bar(int*)), this, SLOT( barslot(int*)));
}

void A::foo()
{
    int ret;
    emit bar( &ret );
}

void A::barslot( int* ret )
{
    *ret = 10;
}

函式bar()的作用類似於虛擬函式,barslot()實現了函式的實際功能。由於訊號的返回值為void,因此必須使用引數來返回資料。 由於只有一個槽函式連線從槽中返回資料的訊號,因此這種方式可以正常工作。 注意,要使Qt4起作用,連線型別必須為Qt::DirectConnection。

如果繼承類要重新實現bar()的功能,則它必須提供自己的槽函式:

class B : public A {
Q_OBJECT
public:
    B();
protected slots: // necessary to specify as a slot again
    void barslot( int* ); // reimplemented functionality of bar()
};

B::B()
{
    disconnect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
    connect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
}

void B::barslot( int* ret )
{
    *ret = 20;
}

現在,B::barslot()將像重新實現虛擬函式A::bar()一樣。請注意,有必要再次將barlot()指定為B中的槽,並且在建構函式中,有必要先斷開連線,然後再次連線,這將斷開A::barslot()並連線B::barslot() 。

注意:可以通過實現虛槽函式來實現相同目