使用元對象編譯器

  • 2019 年 10 月 4 日
  • 筆記

元對象編譯器,朋友中的moc,是處理Qt的C++擴展的程序。

元對象編譯器讀取一個C++源文件。如果它發現其中的一個或多個類的聲明中含有Q_OBJECT宏,它就會給這個使用Q_OBJECT宏的類生成另外一個包含元對象代碼的C++源文件。尤其是,元對象代碼對信號/槽機制、運行時類型信息和動態屬性系統是需要的。

一個被元對象編譯器生成的C++源文件必須和這個類的實現一起被編譯和連接(或者它被包含到(#include)這個類的源文件中)。

如果你是用qmake來生成你的Makefile文件,當需要的時候,編譯規則中需要包含調用元對象編譯器,所以你不需要直接使用元對象編譯器。

用法

元對象編譯器很典型地和包含下面這樣情況地類聲明地輸入文件一起使用:

    class MyClass : public QObject      {          Q_OBJECT      public:          MyClass( QObject * parent=0, const char * name=0 );          ~MyClass();        signals:          void mySignal();        public slots:          void mySlot();        };

除了上述提到地信號和槽,元對象編譯器在下一個例子中還將實現對象屬性。Q_PROPERTY宏聲明了一個對象屬性,而Q_ENUMS 聲明在這個類中的屬性系統中可用的枚舉類型的一個列表。在這種特殊的情況下,我們聲明了一個枚舉類型屬性Priority,也被稱為「priority」,並且讀函數為priority(),寫函數為setPriority()。

    class MyClass : public QObject      {          Q_OBJECT          Q_PROPERTY( Priority priority READ priority WRITE setPriority )          Q_ENUMS( Priority )      public:          MyClass( QObject * parent=0, const char * name=0 );          ~MyClass();            enum Priority { High, Low, VeryHigh, VeryLow };          void setPriority( Priority );          Priority priority() const;      };

屬性可以通過Q_OVERRIDE宏在子類中進行修改。Q_SETS宏聲明了枚舉變量可以進行組合操作,也就是說可以一起讀或寫。另外一個宏,Q_CLASSINFO,用來給類的元對象添加名稱/值這樣一組數據:

class MyClass : public QObject      {          Q_OBJECT          Q_CLASSINFO( "Author", "Oscar Peterson")          Q_CLASSINFO( "Status", "Very nice class")      public:          MyClass( QObject * parent=0, const char * name=0 );          ~MyClass();      };

這三個概念:信號和槽、屬性和元對象數據是可以組合在一起的。

元對象編譯器生成的輸出文件必須被編譯和連接,就像你的程序中的其它的C++代碼一樣;否則你的程序的連編將會在最後的連接階段失敗。出於習慣,這種操作是用下述兩種方式之一解決的:

  • 方法一:類的聲明放在一個頭文件(.h文件)中
  • 如果在上述的文件myclass.h中發現類的聲明,元對象編譯器的輸出文件將會被放在一個叫moc_myclass.cpp的文件中。這個文件將會像通常情況一樣被編譯,作為對象文件的結果是moc_myclass.o(在Unix下)或者moc_myclass.obj(在Windows下)。這個對象接着將會被包含到一個對象文件列表中,它們將會在程序的最後連編階段被連接在一起。
  • 方法二:類的聲明放在一個實現文件(.cpp文件)中
  • 如果上述的文件myclass.cpp中發現類的聲明,元對象編譯器的輸出文件將會被放在一個叫myclass.moc的文件中。這個文件需要被實現文件包含(#include),也就是說myclass.cpp需要包含下面這行 #include "myclass.moc" 放在所有的代碼之後。這樣,元對象編譯器生成的代碼將會和myclass.cpp中普通的類定義一起被編譯和連接,所以方法一中的分別編譯和連接就是不需要的了。

方法一是常規的方法。方法二用在你想讓實現文件自包含,或者Q_OBJECT類是內部實現的並且在頭文件中不可見的這些情況下使用。

Makefile中自動使用元對象編譯器的方法

除了最簡單的測試程序之外的任何程序,建議自動使用元對象編譯器。在你的程序的Makefile文件中加入一些規則,make就會在需要的時候運行元對象編譯器和處理元對象編譯器的輸出。

我們建議使用Trolltech的自由makefile生成工具,qmake,來生成你的Makefile。這個工具可以識別方法一和方法二風格的源文件,並建立一個可以做所有必要的元對象編譯操作的Makefile。

另一方面如果,你想自己建立你的Makefile,下面是如何包含元對象編譯操作的一些提示。

對於在頭文件中聲明了Q_OBJECT宏的類,如果你只使用GNU的make的話,這是一個很有用的makefile規則:

    moc_%.cpp: %.h              moc $< -o $@

如果你想更方便地寫makefile,你可以按下面的格式寫單獨的規則:


你必須記住要把moc_NAME.cpp添加到你的SOURCES(你可以用你喜歡的名字替代)變量中並且把moc_NAME.o或者moc_NAME.obj添加到你的OBJECTS變量中。

(當我們給我們的C++源文件命名為.cpp時,元對象編譯器並不留意,所以只要你喜歡,你可以使用.C、.cc、.CC、.cxx或者甚至.c++。)

對於在實現文件(.cpp文件)中聲明Q_OBJECT的類,我們建議你使用下面這樣的makefile規則:

    NAME.o: NAME.moc        NAME.moc: NAME.cpp              moc -i $< -o $@

這將會保證make程序會在編譯NAME.cpp之前運行元對象編譯器。然後你可以把

    #include "NAME.moc"

放在NAME.cpp的末尾,這樣在這個文件中的所有的類聲明被完全地知道。

調用元對象編譯器moc

這裡是元對象編譯器moc所支持地命令行選項:

  • -o file
  • 將輸出寫到file而不是標準輸出。
  • -f
  • 強制在輸出文件中生成#include聲明。文件的名稱必須符合正則表達式.[hH][^.]*(也就是說擴展名必須以H或h開始)。這個選項只有在你的頭文件沒有遵循標準命名法則的時候才有用。
  • -i
  • 不在輸出文件中生成#include聲明。當一個C++文件包含一個或多個類聲明的時候你也許應該這樣使用元對象編譯器。然後你應該在.cpp文件中包含(#include)元對象代碼。如果-i和-f兩個參數都出現,後出現的有效。
  • -nw
  • 不產生任何警告。不建議使用。
  • -ldbg
  • 把大量的lex調試信息寫到標準輸出。
  • -p path
  • 使元對象編譯器生成的(如果有生成的)#include聲明的文件名稱中預先考慮到path/。
  • -q path
  • 使元對象編譯器在生成的文件中的qt #include文件的名稱中預先考慮到path/。

你可以明確地告訴元對象編譯器不要解析頭文件中的成分。它可以識別包含子字符串MOC_SKIP_BEGIN或者MOC_SKIP_END的任何C++注釋(//)。它們正如你所期望的那樣工作並且你可以把它們劃分為若干層次。元對象編譯器所看到的最終結果就好像你把一個MOC_SKIP_BEGIN和一個MOC_SKIP_END當中的所有行刪除那樣。

診斷

元對象編譯器將會警告關於學多在Q_OBJECT類聲明中危險的或者不合法的構造。

如果你在你的程序的最後連編階段得到連接錯誤,說YourClass::className()是未定義的或者YourClass缺乏vtbl,某樣東西已經被做錯。絕大多數情況下,你忘記了編譯或者#include元對象編譯器產生的C++代碼,或者(在前面的情況下)沒有在連接命令中包含那個對象文件。

限制

元對象編譯器並不展開#include或者#define,它簡單地忽略它所遇到的所有預處理程序指示。這是遺憾的,但是在實踐中它通常情況下不是問題。

元對象編譯器不處理所有的C++。主要的問題是類模板不能含有信號和槽。這裡是一個例子:

    class SomeTemplate<int> : public QFrame {          Q_OBJECT          ...      signals:          void bugInMocDetected( int );      };

次重要的是,後面的構造是不合法的。所有的這些都可以替換為我們通常認為比較好的方案,所以去掉這些限制對於我們來說並不是高優先級的。

多重繼承需要把QObject放在第一個

如果你使用多重繼承,元對象編譯器假設首先繼承的類是QObject的一個子類。也就是說,確信僅僅首先繼承的類是QObject。

    class SomeClass : public QObject, public OtherClass {          ...      };

(這個限制幾乎是不可能去掉的;因為元對象編譯器並不展開#include或者#define,它不能發現基類中哪個是QObject。)

函數指針不能作為信號和槽的參數

在你考慮使用函數指針作為信號/槽的參數的大多數情況下,我們認為繼承是一個不錯的替代方法。這裡是一個不合法的語法的例子:

    class SomeClass : public QObject {          Q_OBJECT          ...      public slots:          // 不合法的          void apply( void (*apply)(List *, void *), char * );      };

你可以在這樣一個限制範圍內工作:

    typedef void (*ApplyFunctionType)( List *, void * );        class SomeClass : public QObject {          Q_OBJECT          ...      public slots:          void apply( ApplyFunctionType, char * );      };

有時用繼承和虛函數、信號和槽來替換函數指針是更好的。

友聲明不能放在信號部分或者槽部分中

有時它也許會工作,但通常情況下,友聲明不能放在信號部分或者槽部分中。把它們替換到私有的、保護的或者公有的部分中。這裡是一個不合法的語法的例子:

    class SomeClass : public QObject {          Q_OBJECT          ...      signals:          friend class ClassTemplate<char>; // 錯的      };

信號和槽不能被升級

把繼承的成員函數升級為公有狀態這一個C++特徵並不延伸到包括信號和槽。這裡是一個不合法的例子:

    class Whatever : public QButtonGroup {          ...      public slots:          void QButtonGroup::buttonPressed; // 錯的          ...      };

QButtonGroup::buttonPressed()槽是保護的。

C++測驗:如果你試圖升級一個被重載的保護成員函數將會發生什麼?

  1. 所有的函數都被重載。
  2. 這不是標準的C++。

類型宏不能被用於信號和槽的參數

因為元對象編譯器並不展開#define,在信號和槽中類型宏作為一個參數是不能工作的。這裡是一個不合法的例子:

    #ifdef ultrix      #define SIGNEDNESS(a) unsigned a      #else      #define SIGNEDNESS(a) a      #endif        class Whatever : public QObject {          ...      signals:          void someSignal( SIGNEDNESS(int) );          ...      };

不含有參數的#define將會像你所期望的那樣工作。

嵌套類不能放在信號部分或者槽部分,也不能含有信號和槽

這裡是一個例子:

   class A {          Q_OBJECT      public:          class B {          public slots:   // 錯的              void b();              ...          };      signals:          class B {       // 錯的              void b();              ...          }:      };

構造函數不能用於信號部分和槽部分

為什麼一個人會把一個構造函數放到信號部分或者槽部分,這對於我們來說都是很神秘的。你無論如何也不能這樣做(除去它偶爾能工作的情況)。請把它們放到私有的、保護的或者公有的部分中,它們本該屬於的地方。這裡是一個不合法的語法的例子:

    class SomeClass : public QObject {          Q_OBJECT      public slots:          SomeClass( QObject *parent, const char *name )              : QObject( parent, name ) { } // 錯的          ...      };

屬性的聲明應該放在含有相應的讀寫函數的公有部分之前

在包含相應的讀寫函數的公有部分之中和之後聲明屬性的話,讀寫函數就不能像所期望的那樣工作了。元對象編譯器會抱怨不能找到函數或者解析這個類型。這裡是一個不合法的語法的例子:

    class SomeClass : public QObject {          Q_OBJECT      public:          ...          Q_PROPERTY( Priority priority READ priority WRITE setPriority ) // 錯的          Q_ENUMS( Priority ) // 錯的          enum Priority { High, Low, VeryHigh, VeryLow };          void setPriority( Priority );          Priority priority() const;          ...      };

根據這個限制,你應該在Q_OBJECT之後,在這個類的聲明之前聲明所有的屬性:

    class SomeClass : public QObject {          Q_OBJECT          Q_PROPERTY( Priority priority READ priority WRITE setPriority )          Q_ENUMS( Priority )      public:          ...          enum Priority { High, Low, VeryHigh, VeryLow };          void setPriority( Priority );          Priority priority() const;          ...      };